style(dashboard): redesign EditModal with solid light theme and tabs
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Trash2, AlertCircle } from "lucide-react";
|
||||
import { Trash2, AlertCircle, Plus, Package, CreditCard, Calendar } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import type { Attachment, LineItem, Milestone, Quote, Termsheet } from "../../lib/types";
|
||||
import { api, nextDashedId, nextId } from "../../lib/api";
|
||||
@@ -52,7 +52,7 @@ interface EditItemRow {
|
||||
|
||||
interface EditTsCard {
|
||||
key: string;
|
||||
domId: string; // "" یا "new-ts-..." برای کارت جدید
|
||||
domId: string;
|
||||
milestones: Milestone[];
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ export interface EditModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// مودال ویرایش کامل پیشنهاد — بهینهشده برای صفحات موبایل
|
||||
export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
const quotes = useQuotes();
|
||||
const suppliers = useSuppliers();
|
||||
@@ -83,6 +82,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
}, [q]);
|
||||
|
||||
const [ready, setReady] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<"items" | "terms" | "dates">("items");
|
||||
const [supplierName, setSupplierName] = useState("");
|
||||
const [selectedProjects, setSelectedProjects] = useState<string[]>([]);
|
||||
const [notes, setNotes] = useState("");
|
||||
@@ -112,7 +112,6 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
}
|
||||
};
|
||||
|
||||
// گزینههای مشخصه
|
||||
const [globalSpecs, setGlobalSpecs] = useState<string[]>([]);
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
@@ -133,7 +132,6 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
return [...set].sort((a, b) => a.localeCompare(b, "fa"));
|
||||
}, [materials, itemRows, globalSpecs]);
|
||||
|
||||
// گزینههای پروژه
|
||||
const projectOptions = useMemo(() => {
|
||||
const set = new Set<string>();
|
||||
projects.forEach((p) => p.name && set.add(p.name));
|
||||
@@ -185,7 +183,6 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
setReady(true);
|
||||
}, [q, suppliers, materials]);
|
||||
|
||||
// بارگذاری فاکتورها
|
||||
useEffect(() => {
|
||||
if (!quoteId) return;
|
||||
let alive = true;
|
||||
@@ -221,7 +218,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
unit: "",
|
||||
price: null,
|
||||
total: null,
|
||||
tsDomIds: [],
|
||||
tsDomIds: tsCards[0] ? [tsCards[0].domId || tsCards[0].key] : [],
|
||||
acceptedTsId: null,
|
||||
rejected: false,
|
||||
restore: true,
|
||||
@@ -252,7 +249,6 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
|
||||
const next: Quote = JSON.parse(JSON.stringify(q));
|
||||
|
||||
// تأمینکننده
|
||||
if (supplierName.trim()) {
|
||||
const norm = normalize(supplierName);
|
||||
const exist = suppliers.find((s) => normalize(s.name) === norm);
|
||||
@@ -270,7 +266,6 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
next.notes = notes.trim();
|
||||
next.invoices = invoices;
|
||||
|
||||
// termsheetها
|
||||
const tsIdMap: Record<string, string> = {};
|
||||
const newTermsheets: Termsheet[] = [];
|
||||
tsCards.forEach((card, i) => {
|
||||
@@ -287,7 +282,6 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
});
|
||||
next.termsheets = newTermsheets;
|
||||
|
||||
// اقلام
|
||||
const newItems: LineItem[] = [];
|
||||
const materialsCopy = [...materials];
|
||||
for (const row of itemRows) {
|
||||
@@ -376,7 +370,6 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
}
|
||||
});
|
||||
|
||||
// تاریخها و وضعیت
|
||||
next.quote_date = quoteDate || q.quote_date;
|
||||
next.expiry_date = expiryDate ? (expiryDate.length > 10 ? expiryDate.slice(0, 10) : expiryDate) : "";
|
||||
next.accepted_date = acceptedDate;
|
||||
@@ -416,23 +409,103 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
return (
|
||||
<>
|
||||
<Dialog open={!!quoteId} onOpenChange={(o) => !o && onClose()}>
|
||||
<DialogContent className="max-h-[92vh] w-[95vw] max-w-3xl overflow-y-auto p-3.5 sm:p-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base font-bold sm:text-lg">ویرایش پیشنهاد</DialogTitle>
|
||||
<DialogContent className="flex h-[88vh] max-h-[850px] w-[95vw] max-w-3xl flex-col p-0 overflow-hidden rounded-2xl border border-gray-200 bg-white text-gray-900 shadow-2xl">
|
||||
{/* هدر سفید و تمیز */}
|
||||
<DialogHeader className="border-b border-gray-100 bg-white px-4 py-3 sm:px-6 sm:py-4 shrink-0 text-right">
|
||||
<DialogTitle className="text-base font-bold text-gray-900 sm:text-lg">ویرایش پیشنهاد خرید</DialogTitle>
|
||||
<div className="text-[12px] text-gray-500 truncate mt-0.5">
|
||||
{supplierName ? `تأمینکننده: ${supplierName}` : "تنظیم اقلام، مبالغ و شرایط پرداخت"}
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
{hasReceipts && (
|
||||
<div className="flex items-center gap-2 rounded-lg border border-amber/30 bg-amber-bg px-3 py-2 text-[12px] font-medium text-amber">
|
||||
<AlertCircle className="h-4 w-4 shrink-0" />
|
||||
<div className="flex items-center gap-2 border-b border-amber-200 bg-amber-50 px-4 py-2 text-[11.5px] font-medium text-amber-800 shrink-0">
|
||||
<AlertCircle className="h-4 w-4 shrink-0 text-amber-600" />
|
||||
<span>این پیشنهاد بهدلیل ثبت رسید در انبار، غیرقابل ویرایش یا بازگشت است.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* اطلاعات بالایی — تکستونه در موبایل، ۲ ستونه در دسکتاپ */}
|
||||
{/* تببار سفید و استاندارد */}
|
||||
<div className="flex items-center border-b border-gray-100 bg-gray-50/70 px-3 py-2 sm:px-6 gap-2 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("items")}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-xs font-semibold transition-all",
|
||||
activeTab === "items"
|
||||
? "bg-white text-blue-600 shadow-xs border border-gray-200"
|
||||
: "text-gray-600 hover:text-gray-900 hover:bg-white/60",
|
||||
)}
|
||||
>
|
||||
<Package className="h-3.5 w-3.5" />
|
||||
<span>اقلام و سفارش</span>
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-1.5 py-0.2 text-[10.5px] font-mono",
|
||||
activeTab === "items" ? "bg-blue-50 text-blue-600" : "bg-gray-200/70 text-gray-600",
|
||||
)}
|
||||
>
|
||||
{faNum(itemRows.length)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("terms")}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-xs font-semibold transition-all",
|
||||
activeTab === "terms"
|
||||
? "bg-white text-blue-600 shadow-xs border border-gray-200"
|
||||
: "text-gray-600 hover:text-gray-900 hover:bg-white/60",
|
||||
)}
|
||||
>
|
||||
<CreditCard className="h-3.5 w-3.5" />
|
||||
<span>شرایط پرداخت</span>
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-1.5 py-0.2 text-[10.5px] font-mono",
|
||||
activeTab === "terms" ? "bg-blue-50 text-blue-600" : "bg-gray-200/70 text-gray-600",
|
||||
)}
|
||||
>
|
||||
{faNum(tsCards.length)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab("dates")}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 rounded-lg px-3.5 py-1.5 text-xs font-semibold transition-all",
|
||||
activeTab === "dates"
|
||||
? "bg-white text-blue-600 shadow-xs border border-gray-200"
|
||||
: "text-gray-600 hover:text-gray-900 hover:bg-white/60",
|
||||
)}
|
||||
>
|
||||
<Calendar className="h-3.5 w-3.5" />
|
||||
<span>وضعیت و اسناد</span>
|
||||
{invoices.length > 0 && (
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-1.5 py-0.2 text-[10.5px] font-mono",
|
||||
activeTab === "dates" ? "bg-blue-50 text-blue-600" : "bg-gray-200/70 text-gray-600",
|
||||
)}
|
||||
>
|
||||
{faNum(invoices.length)}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* محتوای مودال — پسزمینه کاملاً سفید */}
|
||||
<div className="flex-1 overflow-y-auto p-3.5 sm:p-5 space-y-4 bg-white text-gray-800">
|
||||
{/* تب ۱: اقلام و مشخصات */}
|
||||
{activeTab === "items" && (
|
||||
<div className="space-y-4">
|
||||
{/* کارت تأمینکننده و پروژه */}
|
||||
<div className="rounded-xl border border-gray-200 bg-gray-50/50 p-3.5 sm:p-4 space-y-3">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div className="flex min-w-0 flex-col gap-1.5">
|
||||
<label className="text-xs font-semibold text-muted">تأمینکننده</label>
|
||||
<div className="w-full min-w-0">
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<label className="text-xs font-semibold text-gray-600">تأمینکننده</label>
|
||||
<Combobox
|
||||
value={supplierName}
|
||||
onChange={(it: { id: string; label: string }) => setSupplierName(it.label)}
|
||||
@@ -443,11 +516,9 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
onInputChange={(v) => setSupplierName(v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 flex-col gap-1.5">
|
||||
<label className="text-xs font-semibold text-muted">پروژه</label>
|
||||
<div className="w-full min-w-0">
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<label className="text-xs font-semibold text-gray-600">پروژه</label>
|
||||
<MultiSelectCombobox
|
||||
values={selectedProjects}
|
||||
onChange={setSelectedProjects}
|
||||
@@ -457,67 +528,82 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-1 flex min-w-0 flex-col gap-1.5 sm:col-span-2">
|
||||
<label className="text-xs font-semibold text-muted">یادداشت</label>
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<label className="text-xs font-semibold text-gray-600">یادداشت</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
className="w-full rounded-lg border border-line bg-panel px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
|
||||
className="w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs sm:text-sm text-gray-800 shadow-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/30"
|
||||
value={notes}
|
||||
placeholder="یادداشتهای داخلی این معامله..."
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* اقلام */}
|
||||
<div className="border-t border-line pt-3">
|
||||
<div className="mb-2 text-[13px] font-bold">اقلام سفارش:</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{itemRows.map((row) => (
|
||||
{/* لیست اقلام */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-bold text-gray-800">اقلام سفارش ({faNum(itemRows.length)})</span>
|
||||
</div>
|
||||
|
||||
{itemRows.map((row, idx) => (
|
||||
<div
|
||||
key={row.key}
|
||||
className={`rounded-xl border p-3 sm:rounded-lg ${row.rejected ? "border-dashed border-line opacity-60" : "border-line bg-panel/50"}`}
|
||||
className={cn(
|
||||
"rounded-xl border p-3.5 sm:p-4 transition-all bg-white",
|
||||
row.rejected ? "border-dashed border-gray-300 opacity-60 bg-gray-50/40" : "border-gray-200 shadow-xs",
|
||||
)}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-2.5 sm:grid-cols-3 md:grid-cols-4">
|
||||
{/* متریال — تمامعرض در موبایل */}
|
||||
<div className="col-span-2 flex min-w-0 flex-col gap-1 sm:col-span-1">
|
||||
<label className="text-[11px] font-semibold text-muted">متریال</label>
|
||||
<div className="flex items-center justify-between pb-2 mb-3 border-b border-gray-100">
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-blue-50 text-[11px] font-bold text-blue-600">
|
||||
{faNum(idx + 1)}
|
||||
</span>
|
||||
<span className="text-xs font-bold text-gray-900 truncate">
|
||||
{row.cat || "قلم جدید"} {row.brand ? `• ${row.brand}` : ""}
|
||||
</span>
|
||||
</div>
|
||||
{!hasReceipts && (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-xs text-red-600 hover:bg-red-50 transition-colors"
|
||||
title="حذف قلم"
|
||||
onClick={() => setItemRows((rows) => rows.filter((r) => r.key !== row.key))}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
<span className="text-[11px]">حذف</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[11px] font-semibold text-gray-600">متریال / دسته</label>
|
||||
<input
|
||||
disabled={hasReceipts}
|
||||
className="h-9 w-full rounded-md border border-line bg-panel px-2.5 text-[13px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
placeholder="متریال"
|
||||
className="h-9 w-full rounded-md border border-gray-200 bg-white px-2.5 text-xs text-gray-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/30 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:opacity-60"
|
||||
placeholder="نام متریال"
|
||||
value={row.cat}
|
||||
onChange={(e) => patchItem(row.key, { cat: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* برند */}
|
||||
<div className="col-span-1 flex min-w-0 flex-col gap-1">
|
||||
<label className="text-[11px] font-semibold text-muted">برند</label>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[11px] font-semibold text-gray-600">برند</label>
|
||||
<input
|
||||
disabled={hasReceipts}
|
||||
className="h-9 w-full rounded-md border border-line bg-panel px-2.5 text-[13px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
className="h-9 w-full rounded-md border border-gray-200 bg-white px-2.5 text-xs text-gray-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/30 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:opacity-60"
|
||||
placeholder="برند"
|
||||
value={row.brand}
|
||||
onChange={(e) => patchItem(row.key, { brand: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* واحد */}
|
||||
<div className="col-span-1 flex min-w-0 flex-col gap-1">
|
||||
<label className="text-[11px] font-semibold text-muted">واحد</label>
|
||||
<input
|
||||
disabled={hasReceipts}
|
||||
className="h-9 w-full rounded-md border border-line bg-panel px-2.5 text-[13px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
placeholder="واحد"
|
||||
value={row.unit}
|
||||
onChange={(e) => patchItem(row.key, { unit: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* مشخصه — تمامعرض در موبایل */}
|
||||
<div className="col-span-2 flex min-w-0 flex-col gap-1 sm:col-span-1 md:col-span-1">
|
||||
<label className="text-[11px] font-semibold text-muted">مشخصه</label>
|
||||
<div className="w-full min-w-0">
|
||||
<div className="grid grid-cols-2 gap-2.5">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[11px] font-semibold text-gray-600">مشخصه</label>
|
||||
<MultiSelectCombobox
|
||||
disabled={hasReceipts}
|
||||
values={row.specs}
|
||||
@@ -526,11 +612,23 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
placeholder="مشخصه"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[11px] font-semibold text-gray-600">واحد</label>
|
||||
<input
|
||||
disabled={hasReceipts}
|
||||
className="h-9 w-full rounded-md border border-gray-200 bg-white px-2.5 text-xs text-gray-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/30 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:opacity-60"
|
||||
placeholder="واحد سنجش"
|
||||
value={row.unit}
|
||||
onChange={(e) => patchItem(row.key, { unit: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* تعداد */}
|
||||
<div className="col-span-1 flex min-w-0 flex-col gap-1">
|
||||
<label className="text-[11px] font-semibold text-muted">تعداد</label>
|
||||
{/* جعبه ۳ عددی مقادیر */}
|
||||
<div className="grid grid-cols-3 gap-2 rounded-lg bg-gray-50 p-2.5 border border-gray-200">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10.5px] font-semibold text-gray-600">تعداد</label>
|
||||
<NumberInput
|
||||
disabled={hasReceipts}
|
||||
value={row.qty}
|
||||
@@ -544,9 +642,8 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* قیمت واحد */}
|
||||
<div className="col-span-1 flex min-w-0 flex-col gap-1">
|
||||
<label className="text-[11px] font-semibold text-muted">قیمت واحد</label>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10.5px] font-semibold text-gray-600">قیمت واحد</label>
|
||||
<NumberInput
|
||||
disabled={hasReceipts}
|
||||
value={row.price}
|
||||
@@ -556,14 +653,13 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
total: row.qty != null && price != null ? Math.round(row.qty * price * 100) / 100 : row.total,
|
||||
});
|
||||
}}
|
||||
placeholder="قیمت واحد"
|
||||
placeholder="واحد"
|
||||
maxDecimals={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* قیمت کل */}
|
||||
<div className="col-span-1 flex min-w-0 flex-col gap-1">
|
||||
<label className="text-[11px] font-semibold text-muted">قیمت کل</label>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-[10.5px] font-semibold text-gray-600">قیمت کل</label>
|
||||
<NumberInput
|
||||
disabled={hasReceipts}
|
||||
value={row.total}
|
||||
@@ -574,48 +670,45 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
t != null && row.qty != null && row.qty > 0 ? Math.round((t / row.qty) * 100) / 100 : row.price,
|
||||
});
|
||||
}}
|
||||
placeholder="قیمت کل"
|
||||
placeholder="جمع"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* دکمه حذف */}
|
||||
<div className="col-span-1 flex items-end justify-end pb-0.5">
|
||||
{!hasReceipts && (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-9 items-center gap-1 rounded-md px-2.5 text-xs text-red hover:bg-red-bg/50 transition-colors"
|
||||
title="حذف قلم"
|
||||
onClick={() => setItemRows((rows) => rows.filter((r) => r.key !== row.key))}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
<span>حذف قلم</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* تخصیص termsheet */}
|
||||
<div className="mt-2.5 border-t border-line/60 pt-2">
|
||||
{/* شرایط پرداخت برای این قلم */}
|
||||
<div className="pt-2 border-t border-gray-100">
|
||||
{row.rejected ? (
|
||||
<label className="ts-cb-label e-restore">
|
||||
<label className="inline-flex items-center gap-1.5 text-xs font-semibold text-blue-600 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="accent-blue-600 h-4 w-4 rounded"
|
||||
checked={row.restore}
|
||||
onChange={(e) => patchItem(row.key, { restore: e.target.checked })}
|
||||
/>
|
||||
بازگرداندن به معامله
|
||||
بازگرداندن این قلم به معامله
|
||||
</label>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-1 text-[10.5px] font-semibold text-muted">این قلم تحت کدام termsheet هاست؟</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span className="text-[10.5px] font-semibold text-gray-500">شرایط پرداخت مرتبط:</span>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{tsCards.map((card, idx) => (
|
||||
<label key={card.key} className={cn("chip cursor-pointer text-xs", hasReceipts && "cursor-not-allowed opacity-60")}>
|
||||
{tsCards.map((card, cIdx) => {
|
||||
const isSelected = row.tsDomIds.includes(card.domId || card.key);
|
||||
return (
|
||||
<label
|
||||
key={card.key}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-xs cursor-pointer select-none transition-colors",
|
||||
isSelected
|
||||
? "border-blue-500 bg-blue-50 font-semibold text-blue-700"
|
||||
: "border-gray-200 bg-white text-gray-600 hover:text-gray-900",
|
||||
hasReceipts && "cursor-not-allowed opacity-60",
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={hasReceipts}
|
||||
className="me-1 accent-accent disabled:cursor-not-allowed"
|
||||
checked={row.tsDomIds.includes(card.domId || card.key)}
|
||||
className="accent-blue-600 h-3.5 w-3.5 rounded"
|
||||
checked={isSelected}
|
||||
onChange={(e) => {
|
||||
const next = new Set(row.tsDomIds);
|
||||
const domId = card.domId || card.key;
|
||||
@@ -624,73 +717,170 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
patchItem(row.key, { tsDomIds: [...next] });
|
||||
}}
|
||||
/>
|
||||
{tsLabel(card, idx)}
|
||||
<span>{tsLabel(card, cIdx)}</span>
|
||||
</label>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{!hasReceipts && (
|
||||
<Button variant="line" size="sm" className="mt-2.5 w-full sm:w-auto" onClick={addItemRow}>
|
||||
+ افزودن قلم
|
||||
<Button
|
||||
type="button"
|
||||
variant="line"
|
||||
size="sm"
|
||||
className="w-full border-dashed border-gray-300 py-2 text-xs font-medium text-gray-700 gap-1 hover:border-blue-500 hover:text-blue-600 hover:bg-blue-50/50"
|
||||
onClick={addItemRow}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
<span>افزودن قلم جدید</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* تب ۲: شرایط پرداخت */}
|
||||
{activeTab === "terms" && (
|
||||
<div className="space-y-3.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs font-bold text-gray-800">گزینههای شرایط پرداخت</div>
|
||||
{!hasReceipts && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="line"
|
||||
size="sm"
|
||||
className="gap-1 text-xs text-gray-700"
|
||||
onClick={addTsCard}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
<span>افزودن گزینه جدید</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* termsheetها */}
|
||||
<div className="border-t border-line pt-3">
|
||||
<div className="mb-2 text-[13px] font-bold">termsheet ها (شرایط پرداخت):</div>
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<div className="flex flex-col gap-3">
|
||||
{tsCards.map((card, idx) => (
|
||||
<div key={card.key} className="variant-card">
|
||||
<div className="variant-head">
|
||||
<span className="variant-num">{tsLabel(card, idx)}</span>
|
||||
{!hasReceipts && (
|
||||
<button type="button" className="variant-x" title="حذف" onClick={() => removeTsCard(card.key)}>
|
||||
×
|
||||
<div
|
||||
key={card.key}
|
||||
className="rounded-xl border border-gray-200 bg-white p-3.5 sm:p-4 shadow-xs"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-gray-100 pb-2 mb-2.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-blue-50 text-[11px] font-bold text-blue-600">
|
||||
{faNum(idx + 1)}
|
||||
</span>
|
||||
<span className="text-xs font-bold text-gray-900">{tsLabel(card, idx)}</span>
|
||||
<span className="text-[11px] text-gray-500 font-medium">
|
||||
({faNum(card.milestones.length)} مرحله)
|
||||
</span>
|
||||
</div>
|
||||
{!hasReceipts && tsCards.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-xs text-red-600 hover:bg-red-50 transition-colors"
|
||||
title="حذف گزینه"
|
||||
onClick={() => removeTsCard(card.key)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
<span className="text-[11px]">حذف</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-1 flex flex-col gap-1.5">
|
||||
|
||||
<div className="flex flex-col gap-2 mb-3">
|
||||
{card.milestones.map((ms, i) => (
|
||||
<MilestoneRow
|
||||
key={i}
|
||||
milestone={ms}
|
||||
disabled={hasReceipts}
|
||||
onChange={(nextMs) =>
|
||||
updateTs(card.key, { milestones: card.milestones.map((x, j) => (j === i ? nextMs : x)) })
|
||||
updateTs(card.key, {
|
||||
milestones: card.milestones.map((x, j) => (j === i ? nextMs : x)),
|
||||
})
|
||||
}
|
||||
onRemove={() =>
|
||||
updateTs(card.key, {
|
||||
milestones: card.milestones.filter((_, j) => j !== i),
|
||||
})
|
||||
}
|
||||
onRemove={() => updateTs(card.key, { milestones: card.milestones.filter((_, j) => j !== i) })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!hasReceipts && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="line"
|
||||
size="sm"
|
||||
className="mt-1"
|
||||
onClick={() => updateTs(card.key, { milestones: [...card.milestones, { when_iso: null, amount: null }] })}
|
||||
className="w-full sm:w-auto text-xs text-gray-700"
|
||||
onClick={() =>
|
||||
updateTs(card.key, {
|
||||
milestones: [...card.milestones, { when_iso: null, amount: null }],
|
||||
})
|
||||
}
|
||||
>
|
||||
+ مرحله پرداخت
|
||||
<Plus className="h-3.5 w-3.5 me-1" />
|
||||
افزودن مرحله پرداخت
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{!hasReceipts && (
|
||||
<Button variant="line" size="sm" className="mt-2.5 w-full sm:w-auto" onClick={addTsCard}>
|
||||
+ افزودن termsheet
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* تب ۳: وضعیت و اسناد */}
|
||||
{activeTab === "dates" && (
|
||||
<div className="space-y-3.5">
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-3.5 sm:p-4 space-y-2 shadow-xs">
|
||||
<label className="text-xs font-bold text-gray-900 block">وضعیت فعلی پیشنهاد</label>
|
||||
<select
|
||||
className="h-9 w-full rounded-lg border border-gray-200 bg-white px-3 text-xs sm:text-sm text-gray-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/30 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:opacity-60"
|
||||
value={status}
|
||||
disabled={hasReceipts}
|
||||
onChange={(e) => setStatus(e.target.value)}
|
||||
>
|
||||
{statusOptionsFor(q.status).map((st) => (
|
||||
<option key={st} value={st}>
|
||||
{STATUS_LABEL[st]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="text-[11px] text-gray-500">
|
||||
* تغییر وضعیت برای عقب بردن مرحله معامله کاربرد دارد.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* فاکتورها */}
|
||||
<div className="border-t border-line pt-3">
|
||||
<div className="mb-1 text-[13px] font-bold">فاکتورهای اصلی (چند فایل):</div>
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-3.5 sm:p-4 space-y-2.5 shadow-xs">
|
||||
<div className="text-xs font-bold text-gray-900">تاریخهای چرخه خرید</div>
|
||||
<div className="grid grid-cols-1 gap-2.5 sm:grid-cols-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-semibold text-gray-600">تاریخ پیشنهاد</label>
|
||||
<JalaliDatePicker value={quoteDate} onChange={setQuoteDate} placeholder="تاریخ" withTime />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-semibold text-gray-600">تاریخ انقضا</label>
|
||||
<JalaliDatePicker value={expiryDate} onChange={setExpiryDate} placeholder="تاریخ" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-semibold text-gray-600">تاریخ پذیرش</label>
|
||||
<JalaliDatePicker value={acceptedDate} onChange={setAcceptedDate} placeholder="تاریخ" withTime />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs font-semibold text-gray-600">تاریخ رسید</label>
|
||||
<JalaliDatePicker value={arrivedDate} onChange={setArrivedDate} placeholder="تاریخ" withTime />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-3.5 sm:p-4 space-y-2 shadow-xs">
|
||||
<div className="text-xs font-bold text-gray-900">فاکتورهای اصلی و پیوستها</div>
|
||||
<FileList
|
||||
files={invoices}
|
||||
onChange={setInvoices}
|
||||
@@ -704,70 +894,32 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
urlOf={(a) => (a.id ? api.files.downloadUrl(q.id, "invoice", a.id) : null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* تاریخها و وضعیت */}
|
||||
<div className="border-t border-line pt-3">
|
||||
<div className="mb-2 text-[12px] font-bold text-muted">تاریخها و وضعیت:</div>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div className="flex min-w-0 flex-col gap-1.5">
|
||||
<label className="text-xs font-semibold text-muted">تاریخ پیشنهاد</label>
|
||||
<JalaliDatePicker value={quoteDate} onChange={setQuoteDate} placeholder="تاریخ" withTime />
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-col gap-1.5">
|
||||
<label className="text-xs font-semibold text-muted">تاریخ انقضا</label>
|
||||
<JalaliDatePicker value={expiryDate} onChange={setExpiryDate} placeholder="تاریخ" />
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-col gap-1.5">
|
||||
<label className="text-xs font-semibold text-muted">تاریخ پذیرش</label>
|
||||
<JalaliDatePicker value={acceptedDate} onChange={setAcceptedDate} placeholder="تاریخ" withTime />
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-col gap-1.5">
|
||||
<label className="text-xs font-semibold text-muted">تاریخ رسید</label>
|
||||
<JalaliDatePicker value={arrivedDate} onChange={setArrivedDate} placeholder="تاریخ" withTime />
|
||||
</div>
|
||||
<div className="col-span-1 flex min-w-0 flex-col gap-1.5 sm:col-span-2">
|
||||
<label className="text-xs font-semibold text-muted">وضعیت (فقط بازگشت به مرحلههای قبل)</label>
|
||||
<select
|
||||
className="h-10 w-full rounded-lg border border-line bg-panel px-3 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
value={status}
|
||||
disabled={hasReceipts}
|
||||
onChange={(e) => setStatus(e.target.value)}
|
||||
>
|
||||
{statusOptionsFor(q.status).map((st) => (
|
||||
<option key={st} value={st}>
|
||||
{STATUS_LABEL[st]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{hasReceipts && (
|
||||
<div className="mt-1.5 rounded-md bg-amber-bg px-2.5 py-1.5 text-[11.5px] font-medium text-amber">
|
||||
این پیشنهاد بهدلیل ثبت رسید در انبار، غیرقابل ویرایش یا بازگشت است.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* فوتر و دکمههای اقدام */}
|
||||
<DialogFooter className="flex flex-col-reverse gap-2 border-t border-line/60 pt-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
{/* فوتر سفید، محکم و چسبان */}
|
||||
<DialogFooter className="border-t border-gray-200 bg-white px-4 py-2.5 sm:px-6 sm:py-3 shrink-0 flex flex-row items-center justify-between gap-2">
|
||||
{q.status === "offered" ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
className="w-full gap-1.5 sm:w-auto"
|
||||
className="gap-1 px-2.5 sm:px-3 text-xs shrink-0"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span>حذف پیشنهاد</span>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
<span className="hidden sm:inline">حذف</span>
|
||||
</Button>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<div className="flex w-full items-center gap-2 sm:w-auto sm:justify-end">
|
||||
<Button variant="slate" onClick={onClose} className="flex-1 sm:flex-initial">
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="slate" size="sm" onClick={onClose} className="px-3.5 text-xs">
|
||||
انصراف
|
||||
</Button>
|
||||
<Button onClick={() => void confirm()} className="flex-1 sm:flex-initial">
|
||||
<Button size="sm" onClick={() => void confirm()} className="px-4 text-xs">
|
||||
ذخیره تغییرات
|
||||
</Button>
|
||||
</div>
|
||||
@@ -775,19 +927,20 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* مودال تأیید حذف */}
|
||||
{/* مودال تایید حذف سفید */}
|
||||
<Dialog open={showDeleteConfirm} onOpenChange={(open) => !deleting && setShowDeleteConfirm(open)}>
|
||||
<DialogContent className="z-[60] max-w-md p-4 sm:p-6">
|
||||
<DialogContent className="z-[60] max-w-md p-4 sm:p-6 bg-white border border-gray-200 text-gray-900">
|
||||
<DialogHeader>
|
||||
<DialogTitle>حذف پیشنهاد خرید</DialogTitle>
|
||||
<DialogTitle className="text-gray-900">حذف پیشنهاد خرید</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-2 text-sm text-muted">
|
||||
<div className="py-2 text-xs sm:text-sm text-gray-600">
|
||||
آیا از حذف کامل این پیشنهاد اطمینان دارید؟ این عملیات غیرقابل بازگشت است.
|
||||
</div>
|
||||
<DialogFooter className="flex items-center justify-end gap-2 pt-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="slate"
|
||||
size="sm"
|
||||
onClick={() => setShowDeleteConfirm(false)}
|
||||
disabled={deleting}
|
||||
>
|
||||
@@ -796,6 +949,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
<Button
|
||||
type="button"
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={() => void handleDelete()}
|
||||
disabled={deleting}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user