fix(procurement): improve mobile layout for QuoteCard and EditModal

This commit is contained in:
Behrad
2026-09-17 13:19:14 +03:30
parent 7b3ae3cb60
commit 435e6cbb21
2 changed files with 433 additions and 379 deletions
+106 -76
View File
@@ -61,7 +61,7 @@ export interface EditModalProps {
onClose: () => void; onClose: () => void;
} }
// مودال ویرایش کامل پیشنهاد // مودال ویرایش کامل پیشنهاد — بهینه‌شده برای صفحات موبایل
export function EditModal({ quoteId, onClose }: EditModalProps) { export function EditModal({ quoteId, onClose }: EditModalProps) {
const quotes = useQuotes(); const quotes = useQuotes();
const suppliers = useSuppliers(); const suppliers = useSuppliers();
@@ -112,7 +112,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
} }
}; };
// گزینه‌های مشخصه — همه‌ی مشخصه‌های ثبت‌شده (کش) + رجیستری سراسری // گزینه‌های مشخصه
const [globalSpecs, setGlobalSpecs] = useState<string[]>([]); const [globalSpecs, setGlobalSpecs] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
let alive = true; let alive = true;
@@ -120,7 +120,9 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
.specsList() .specsList()
.then((list) => alive && setGlobalSpecs(list)) .then((list) => alive && setGlobalSpecs(list))
.catch(() => {}); .catch(() => {});
return () => { alive = false; }; return () => {
alive = false;
};
}, []); }, []);
const specOptions = useMemo(() => { const specOptions = useMemo(() => {
@@ -131,7 +133,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
return [...set].sort((a, b) => a.localeCompare(b, "fa")); return [...set].sort((a, b) => a.localeCompare(b, "fa"));
}, [materials, itemRows, globalSpecs]); }, [materials, itemRows, globalSpecs]);
// گزینه‌های پروژه — پروژه‌های ثبت‌شده + پروژه‌های خود پیشنهاد // گزینه‌های پروژه
const projectOptions = useMemo(() => { const projectOptions = useMemo(() => {
const set = new Set<string>(); const set = new Set<string>();
projects.forEach((p) => p.name && set.add(p.name)); projects.forEach((p) => p.name && set.add(p.name));
@@ -183,7 +185,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
setReady(true); setReady(true);
}, [q, suppliers, materials]); }, [q, suppliers, materials]);
// فاکتورها مستقیماً از سرور (MinIO) خوانده می‌شوند — نه از state محلی // بارگذاری فاکتورها
useEffect(() => { useEffect(() => {
if (!quoteId) return; if (!quoteId) return;
let alive = true; let alive = true;
@@ -250,7 +252,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
const next: Quote = JSON.parse(JSON.stringify(q)); const next: Quote = JSON.parse(JSON.stringify(q));
// تأمین‌کننده — موجود یا جدید // تأمین‌کننده
if (supplierName.trim()) { if (supplierName.trim()) {
const norm = normalize(supplierName); const norm = normalize(supplierName);
const exist = suppliers.find((s) => normalize(s.name) === norm); const exist = suppliers.find((s) => normalize(s.name) === norm);
@@ -259,7 +261,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
} else { } else {
const newSup = { id: nextId("s", suppliers), name: supplierName.trim(), location: "", phone: "", contact: "", topic: "", categories: [] }; const newSup = { id: nextId("s", suppliers), name: supplierName.trim(), location: "", phone: "", contact: "", topic: "", categories: [] };
const created = await addSupplier.mutateAsync(newSup); const created = await addSupplier.mutateAsync(newSup);
next.supplier_id = created.id; // UUID واقعی از بک‌اند next.supplier_id = created.id;
} }
} }
@@ -268,7 +270,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
next.notes = notes.trim(); next.notes = notes.trim();
next.invoices = invoices; next.invoices = invoices;
// termsheetها — id موجود حفظ می‌شود، جدیدها ساخته می‌شوند // termsheetها
const tsIdMap: Record<string, string> = {}; const tsIdMap: Record<string, string> = {};
const newTermsheets: Termsheet[] = []; const newTermsheets: Termsheet[] = [];
tsCards.forEach((card, i) => { tsCards.forEach((card, i) => {
@@ -296,7 +298,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
const qty = row.qty; const qty = row.qty;
const price = row.price; const price = row.price;
const total = row.total; const total = row.total;
if (!cat && !brand && qty == null && price == null) continue; // ردیف خالی if (!cat && !brand && qty == null && price == null) continue;
let material = materialsCopy.find( let material = materialsCopy.find(
(m) => (m) =>
@@ -318,7 +320,6 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
base_unit: unit, base_unit: unit,
}; };
materialsCopy.push(nm); materialsCopy.push(nm);
// منتظر ساخت متریال می‌مانیم تا id واقعی (UUID) به قلم وصل شود
const created = await addMaterial.mutateAsync(nm); const created = await addMaterial.mutateAsync(nm);
materialId = created.id; materialId = created.id;
} }
@@ -366,7 +367,6 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
} }
next.line_items = newItems.length ? newItems : next.line_items; next.line_items = newItems.length ? newItems : next.line_items;
// ترم‌شیت بدون قلم بی‌معنی است — حذف
const referencedTs = new Set<string>(); const referencedTs = new Set<string>();
next.line_items.forEach((li) => (li.termsheet_ids || []).forEach((tid) => referencedTs.add(tid))); next.line_items.forEach((li) => (li.termsheet_ids || []).forEach((tid) => referencedTs.add(tid)));
next.termsheets = next.termsheets.filter((t) => referencedTs.has(t.id)); next.termsheets = next.termsheets.filter((t) => referencedTs.has(t.id));
@@ -383,7 +383,6 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
next.arrived_date = arrivedDate; next.arrived_date = arrivedDate;
next.status = status as Quote["status"]; next.status = status as Quote["status"];
// پاک‌سازی خودکار در بازگشت وضعیت
if (next.status === "offered") { if (next.status === "offered") {
next.accepted_date = null; next.accepted_date = null;
next.arrived_date = null; next.arrived_date = null;
@@ -417,21 +416,23 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
return ( return (
<> <>
<Dialog open={!!quoteId} onOpenChange={(o) => !o && onClose()}> <Dialog open={!!quoteId} onOpenChange={(o) => !o && onClose()}>
<DialogContent className="max-w-3xl"> <DialogContent className="max-h-[92vh] w-[95vw] max-w-3xl overflow-y-auto p-3.5 sm:p-6">
<DialogHeader> <DialogHeader>
<DialogTitle>ویرایش پیشنهاد</DialogTitle> <DialogTitle className="text-base font-bold sm:text-lg">ویرایش پیشنهاد</DialogTitle>
</DialogHeader> </DialogHeader>
{hasReceipts && ( {hasReceipts && (
<div className="flex items-center gap-2 rounded-lg border border-amber/30 bg-amber-bg px-3 py-2 text-[12.5px] font-medium text-amber"> <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" /> <AlertCircle className="h-4 w-4 shrink-0" />
<span>این پیشنهاد به‌دلیل ثبت رسید در انبار، غیرقابل ویرایش یا بازگشت است.</span> <span>این پیشنهاد به‌دلیل ثبت رسید در انبار، غیرقابل ویرایش یا بازگشت است.</span>
</div> </div>
)} )}
<div className="grid grid-cols-2 gap-3 max-md:grid-cols-1"> {/* اطلاعات بالایی — تک‌ستونه در موبایل، ۲ ستونه در دسکتاپ */}
<div className="field"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<label>تأمین‌کننده</label> <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">
<Combobox <Combobox
value={supplierName} value={supplierName}
onChange={(it: { id: string; label: string }) => setSupplierName(it.label)} onChange={(it: { id: string; label: string }) => setSupplierName(it.label)}
@@ -442,8 +443,11 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
onInputChange={(v) => setSupplierName(v)} onInputChange={(v) => setSupplierName(v)}
/> />
</div> </div>
<div className="field"> </div>
<label>پروژه</label>
<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">
<MultiSelectCombobox <MultiSelectCombobox
values={selectedProjects} values={selectedProjects}
onChange={setSelectedProjects} onChange={setSelectedProjects}
@@ -451,8 +455,10 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
placeholder="انتخاب پروژه..." placeholder="انتخاب پروژه..."
/> />
</div> </div>
<div className="field col-span-2"> </div>
<label>یادداشت</label>
<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>
<textarea <textarea
rows={2} 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-line bg-panel px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
@@ -467,10 +473,14 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
<div className="mb-2 text-[13px] font-bold">اقلام سفارش:</div> <div className="mb-2 text-[13px] font-bold">اقلام سفارش:</div>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{itemRows.map((row) => ( {itemRows.map((row) => (
<div key={row.key} className={`rounded-lg border p-3 ${row.rejected ? "border-dashed border-line opacity-60" : "border-line"}`}> <div
<div className="grid grid-cols-2 gap-3 md:grid-cols-4"> key={row.key}
<div className="field"> className={`rounded-xl border p-3 sm:rounded-lg ${row.rejected ? "border-dashed border-line opacity-60" : "border-line bg-panel/50"}`}
<label>متریال</label> >
<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>
<input <input
disabled={hasReceipts} 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-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"
@@ -479,8 +489,10 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
onChange={(e) => patchItem(row.key, { cat: e.target.value })} onChange={(e) => patchItem(row.key, { cat: e.target.value })}
/> />
</div> </div>
<div className="field">
<label>برند</label> {/* برند */}
<div className="col-span-1 flex min-w-0 flex-col gap-1">
<label className="text-[11px] font-semibold text-muted">برند</label>
<input <input
disabled={hasReceipts} 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-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"
@@ -489,18 +501,10 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
onChange={(e) => patchItem(row.key, { brand: e.target.value })} onChange={(e) => patchItem(row.key, { brand: e.target.value })}
/> />
</div> </div>
<div className="field">
<label>مشخصه</label> {/* واحد */}
<MultiSelectCombobox <div className="col-span-1 flex min-w-0 flex-col gap-1">
disabled={hasReceipts} <label className="text-[11px] font-semibold text-muted">واحد</label>
values={row.specs}
onChange={(specs) => patchItem(row.key, { specs })}
options={specOptions}
placeholder="مشخصه"
/>
</div>
<div className="field">
<label>واحد</label>
<input <input
disabled={hasReceipts} 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-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"
@@ -509,8 +513,24 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
onChange={(e) => patchItem(row.key, { unit: e.target.value })} onChange={(e) => patchItem(row.key, { unit: e.target.value })}
/> />
</div> </div>
<div className="field">
<label>تعداد</label> {/* مشخصه — تمام‌عرض در موبایل */}
<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">
<MultiSelectCombobox
disabled={hasReceipts}
values={row.specs}
onChange={(specs) => patchItem(row.key, { specs })}
options={specOptions}
placeholder="مشخصه"
/>
</div>
</div>
{/* تعداد */}
<div className="col-span-1 flex min-w-0 flex-col gap-1">
<label className="text-[11px] font-semibold text-muted">تعداد</label>
<NumberInput <NumberInput
disabled={hasReceipts} disabled={hasReceipts}
value={row.qty} value={row.qty}
@@ -523,8 +543,10 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
placeholder="تعداد" placeholder="تعداد"
/> />
</div> </div>
<div className="field">
<label>قیمت واحد</label> {/* قیمت واحد */}
<div className="col-span-1 flex min-w-0 flex-col gap-1">
<label className="text-[11px] font-semibold text-muted">قیمت واحد</label>
<NumberInput <NumberInput
disabled={hasReceipts} disabled={hasReceipts}
value={row.price} value={row.price}
@@ -538,8 +560,10 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
maxDecimals={2} maxDecimals={2}
/> />
</div> </div>
<div className="field">
<label>قیمت کل</label> {/* قیمت کل */}
<div className="col-span-1 flex min-w-0 flex-col gap-1">
<label className="text-[11px] font-semibold text-muted">قیمت کل</label>
<NumberInput <NumberInput
disabled={hasReceipts} disabled={hasReceipts}
value={row.total} value={row.total}
@@ -553,22 +577,25 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
placeholder="قیمت کل" placeholder="قیمت کل"
/> />
</div> </div>
<div className="flex items-end justify-end">
{/* دکمه حذف */}
<div className="col-span-1 flex items-end justify-end pb-0.5">
{!hasReceipts && ( {!hasReceipts && (
<button <button
type="button" type="button"
className="inline-flex items-center gap-1 rounded-md px-2 py-1.5 text-[12px] text-muted transition-colors hover:bg-red-bg hover:text-red" 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="حذف قلم" title="حذف قلم"
onClick={() => setItemRows((rows) => rows.filter((r) => r.key !== row.key))} onClick={() => setItemRows((rows) => rows.filter((r) => r.key !== row.key))}
> >
<Trash2 className="h-3.5 w-3.5" /> <Trash2 className="h-3.5 w-3.5" />
حذف <span>حذف قلم</span>
</button> </button>
)} )}
</div> </div>
</div> </div>
{/* تخصیص termsheet / بازگرداندن */}
<div className="mt-2.5 border-t border-line pt-2"> {/* تخصیص termsheet */}
<div className="mt-2.5 border-t border-line/60 pt-2">
{row.rejected ? ( {row.rejected ? (
<label className="ts-cb-label e-restore"> <label className="ts-cb-label e-restore">
<input <input
@@ -583,7 +610,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
<div className="mb-1 text-[10.5px] font-semibold text-muted">این قلم تحت کدام termsheet هاست؟</div> <div className="mb-1 text-[10.5px] font-semibold text-muted">این قلم تحت کدام termsheet هاست؟</div>
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
{tsCards.map((card, idx) => ( {tsCards.map((card, idx) => (
<label key={card.key} className={cn("chip cursor-pointer", hasReceipts && "cursor-not-allowed opacity-60")}> <label key={card.key} className={cn("chip cursor-pointer text-xs", hasReceipts && "cursor-not-allowed opacity-60")}>
<input <input
type="checkbox" type="checkbox"
disabled={hasReceipts} disabled={hasReceipts}
@@ -608,7 +635,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
))} ))}
</div> </div>
{!hasReceipts && ( {!hasReceipts && (
<Button variant="line" size="sm" className="mt-2" onClick={addItemRow}> <Button variant="line" size="sm" className="mt-2.5 w-full sm:w-auto" onClick={addItemRow}>
+ افزودن قلم + افزودن قلم
</Button> </Button>
)} )}
@@ -617,7 +644,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
{/* termsheetها */} {/* termsheetها */}
<div className="border-t border-line pt-3"> <div className="border-t border-line pt-3">
<div className="mb-2 text-[13px] font-bold">termsheet ها (شرایط پرداخت):</div> <div className="mb-2 text-[13px] font-bold">termsheet ها (شرایط پرداخت):</div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2.5">
{tsCards.map((card, idx) => ( {tsCards.map((card, idx) => (
<div key={card.key} className="variant-card"> <div key={card.key} className="variant-card">
<div className="variant-head"> <div className="variant-head">
@@ -655,7 +682,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
))} ))}
</div> </div>
{!hasReceipts && ( {!hasReceipts && (
<Button variant="line" size="sm" className="mt-2" onClick={addTsCard}> <Button variant="line" size="sm" className="mt-2.5 w-full sm:w-auto" onClick={addTsCard}>
+ افزودن termsheet + افزودن termsheet
</Button> </Button>
)} )}
@@ -680,28 +707,28 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
{/* تاریخ‌ها و وضعیت */} {/* تاریخ‌ها و وضعیت */}
<div className="border-t border-line pt-3"> <div className="border-t border-line pt-3">
<div className="mb-2 text-[12px] text-muted">تاریخ‌ها و وضعیت:</div> <div className="mb-2 text-[12px] font-bold text-muted">تاریخ‌ها و وضعیت:</div>
<div className="grid grid-cols-2 gap-3 max-md:grid-cols-1"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div className="field"> <div className="flex min-w-0 flex-col gap-1.5">
<label>تاریخ پیشنهاد</label> <label className="text-xs font-semibold text-muted">تاریخ پیشنهاد</label>
<JalaliDatePicker value={quoteDate} onChange={setQuoteDate} placeholder="تاریخ" withTime /> <JalaliDatePicker value={quoteDate} onChange={setQuoteDate} placeholder="تاریخ" withTime />
</div> </div>
<div className="field"> <div className="flex min-w-0 flex-col gap-1.5">
<label>تاریخ انقضا</label> <label className="text-xs font-semibold text-muted">تاریخ انقضا</label>
<JalaliDatePicker value={expiryDate} onChange={setExpiryDate} placeholder="تاریخ" /> <JalaliDatePicker value={expiryDate} onChange={setExpiryDate} placeholder="تاریخ" />
</div> </div>
<div className="field"> <div className="flex min-w-0 flex-col gap-1.5">
<label>تاریخ پذیرش</label> <label className="text-xs font-semibold text-muted">تاریخ پذیرش</label>
<JalaliDatePicker value={acceptedDate} onChange={setAcceptedDate} placeholder="تاریخ" withTime /> <JalaliDatePicker value={acceptedDate} onChange={setAcceptedDate} placeholder="تاریخ" withTime />
</div> </div>
<div className="field"> <div className="flex min-w-0 flex-col gap-1.5">
<label>تاریخ رسید</label> <label className="text-xs font-semibold text-muted">تاریخ رسید</label>
<JalaliDatePicker value={arrivedDate} onChange={setArrivedDate} placeholder="تاریخ" withTime /> <JalaliDatePicker value={arrivedDate} onChange={setArrivedDate} placeholder="تاریخ" withTime />
</div> </div>
<div className="field col-span-2"> <div className="col-span-1 flex min-w-0 flex-col gap-1.5 sm:col-span-2">
<label>وضعیت (فقط بازگشت به مرحله‌های قبل)</label> <label className="text-xs font-semibold text-muted">وضعیت (فقط بازگشت به مرحله‌های قبل)</label>
<select <select
className="h-9 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" 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} value={status}
disabled={hasReceipts} disabled={hasReceipts}
onChange={(e) => setStatus(e.target.value)} onChange={(e) => setStatus(e.target.value)}
@@ -721,13 +748,14 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
</div> </div>
</div> </div>
<DialogFooter className="flex items-center justify-between gap-2 max-sm:flex-col-reverse"> {/* فوتر و دکمه‌های اقدام */}
<DialogFooter className="flex flex-col-reverse gap-2 border-t border-line/60 pt-3 sm:flex-row sm:items-center sm:justify-between">
{q.status === "offered" ? ( {q.status === "offered" ? (
<Button <Button
type="button" type="button"
variant="danger" variant="danger"
onClick={() => setShowDeleteConfirm(true)} onClick={() => setShowDeleteConfirm(true)}
className="gap-1.5" className="w-full gap-1.5 sm:w-auto"
> >
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
<span>حذف پیشنهاد</span> <span>حذف پیشنهاد</span>
@@ -735,26 +763,28 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
) : ( ) : (
<div /> <div />
)} )}
<div className="flex items-center gap-2"> <div className="flex w-full items-center gap-2 sm:w-auto sm:justify-end">
<Button variant="slate" onClick={onClose}> <Button variant="slate" onClick={onClose} className="flex-1 sm:flex-initial">
انصراف انصراف
</Button> </Button>
<Button onClick={() => void confirm()}>ذخیره تغییرات</Button> <Button onClick={() => void confirm()} className="flex-1 sm:flex-initial">
ذخیره تغییرات
</Button>
</div> </div>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* مدال تأیید حذف پیشنهاد */} {/* مودال تأیید حذف */}
<Dialog open={showDeleteConfirm} onOpenChange={(open) => !deleting && setShowDeleteConfirm(open)}> <Dialog open={showDeleteConfirm} onOpenChange={(open) => !deleting && setShowDeleteConfirm(open)}>
<DialogContent className="z-[60] max-w-md"> <DialogContent className="z-[60] max-w-md p-4 sm:p-6">
<DialogHeader> <DialogHeader>
<DialogTitle>حذف پیشنهاد خرید</DialogTitle> <DialogTitle>حذف پیشنهاد خرید</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="py-2 text-sm text-muted"> <div className="py-2 text-sm text-muted">
آیا از حذف کامل این پیشنهاد اطمینان دارید؟ این عملیات غیرقابل بازگشت است. آیا از حذف کامل این پیشنهاد اطمینان دارید؟ این عملیات غیرقابل بازگشت است.
</div> </div>
<DialogFooter className="flex items-center justify-end gap-2"> <DialogFooter className="flex items-center justify-end gap-2 pt-2">
<Button <Button
type="button" type="button"
variant="slate" variant="slate"
+52 -28
View File
@@ -176,29 +176,60 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit
</div> </div>
</div> </div>
{/* اطلاعات کلیدی (گرید ۲ ستونه موبایل / ۴ ستونه دسکتاپ) */} {/* اطلاعات کلیدی — چیدمان بهینه‌شده موبایل */}
<div className="grid grid-cols-2 gap-x-3 gap-y-2.5 p-3 sm:gap-x-5 sm:gap-y-3 sm:p-4 md:grid-cols-4"> <div className="grid grid-cols-2 gap-x-3 gap-y-3 p-3 sm:gap-x-5 sm:gap-y-3.5 sm:p-4 md:grid-cols-4">
<InfoCell label="اقلام"> {/* ۱. پروژه — تمام‌عرض در موبایل */}
<InfoCell label="پروژه" className="col-span-2 md:col-span-1">
<div className="flex items-center gap-1.5 text-xs font-semibold text-ink sm:text-[13px]">
<Building2 className="h-3.5 w-3.5 shrink-0 text-muted" />
<span className="truncate">
{q.projects && q.projects.length > 0 ? q.projects.join("، ") : projectCell(q)}
</span>
</div>
</InfoCell>
{/* ۲. اقلام — تمام‌عرض زیر پروژه با شکستن متن بدون سه‌نقطه */}
<InfoCell label="اقلام" className="col-span-2 md:col-span-1">
{summary.count > 1 ? ( {summary.count > 1 ? (
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-1.5">
<span className="font-semibold text-xs sm:text-sm">{fmtNum(summary.count)} قلم کالا</span> <span className="font-bold text-xs text-ink sm:text-sm">
<div className="flex flex-col gap-0.5 text-[11px] leading-4 text-muted sm:text-[11.5px] sm:leading-5"> {fmtNum(summary.count)} قلم کالا
{store.quotes.find((x) => x.id === q.id)?.line_items?.filter((li) => !li.rejected).slice(0, 3).map((li) => { </span>
<div className="flex flex-col gap-1.5 rounded-lg border border-line/60 bg-bg/50 p-2.5 text-[11px] leading-5 text-muted sm:text-[11.5px]">
{store.quotes
.find((x) => x.id === q.id)
?.line_items?.filter((li) => !li.rejected)
.slice(0, 3)
.map((li) => {
const m = materialById(store.materials, li.material_id); const m = materialById(store.materials, li.material_id);
return ( return (
<span key={li.id} className="truncate"> <div
• {materialLabel(m)} ({fmtNum(li.quantity)} {li.unit}) key={li.id}
className="flex items-start justify-between gap-2 border-b border-line/30 pb-1.5 last:border-0 last:pb-0"
>
<span className="min-w-0 flex-1 font-medium text-ink leading-snug break-words">
• {materialLabel(m)}
</span> </span>
<span className="shrink-0 font-semibold tabular-nums text-ink text-[10.5px] pt-0.5">
{fmtNum(li.quantity)}{" "}
<span className="font-normal text-muted text-[9.5px]">{li.unit}</span>
</span>
</div>
); );
})} })}
{(q.line_items?.length || 0) > 3 && ( {(q.line_items?.length || 0) > 3 && (
<span className="text-[10px] text-muted">و {(q.line_items?.length || 0) - 3} قلم دیگر...</span> <span className="pt-0.5 text-[10px] text-muted">
و {(q.line_items?.length || 0) - 3} قلم دیگر...
</span>
)} )}
</div> </div>
</div> </div>
) : ( ) : (
<span className="truncate">{summary.text}</span> <div className="flex items-start gap-1.5 text-xs font-medium leading-5 text-ink sm:text-[13px]">
<span className="break-words">{summary.text}</span>
</div>
)} )}
{unitWarns.map((w) => ( {unitWarns.map((w) => (
<Link <Link
key={w.unit} key={w.unit}
@@ -206,25 +237,17 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit
className="unit-warn mt-1 inline-flex items-center gap-1 text-[10px]" className="unit-warn mt-1 inline-flex items-center gap-1 text-[10px]"
title={`ضریب تبدیل «${esc(w.unit)}» را در داشبورد واحدها ثبت کنید`} title={`ضریب تبدیل «${esc(w.unit)}» را در داشبورد واحدها ثبت کنید`}
> >
<TriangleAlert className="h-2.5 w-2.5" /> <TriangleAlert className="h-2.5 w-2.5 shrink-0" />
واحد جدید: {w.unit} واحد جدید: {w.unit}
</Link> </Link>
))} ))}
</InfoCell> </InfoCell>
<InfoCell label="پروژه"> {/* ۳. مجموع مبلغ */}
<span className="inline-flex items-center gap-1 text-xs sm:text-[13px]"> <InfoCell label="مجموع مبلغ" className="col-span-1">
<Building2 className="h-3.5 w-3.5 shrink-0 text-muted" />
<span className="truncate">
{q.projects && q.projects.length > 0 ? q.projects.join("، ") : projectCell(q)}
</span>
</span>
</InfoCell>
<InfoCell label="مجموع مبلغ">
{pc ? ( {pc ? (
<div> <div>
<div className="text-sm font-extrabold text-ink tabular-nums sm:text-base"> <div className="text-sm font-extrabold tabular-nums text-ink sm:text-base">
{pc.num} <span className="text-[10px] font-normal text-muted sm:text-xs">تومان</span> {pc.num} <span className="text-[10px] font-normal text-muted sm:text-xs">تومان</span>
</div> </div>
<div className="hidden text-[11px] text-muted sm:block">({pc.words})</div> <div className="hidden text-[11px] text-muted sm:block">({pc.words})</div>
@@ -234,10 +257,11 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit
)} )}
</InfoCell> </InfoCell>
<InfoCell label="شرایط معامله"> {/* ۴. شرایط معامله */}
<InfoCell label="شرایط معامله" className="col-span-1">
{ts.title !== "—" ? ( {ts.title !== "—" ? (
<div> <div>
<div className="font-semibold text-xs sm:text-[13px]" style={ts.color ? { color: ts.color } : undefined}> <div className="text-xs font-semibold sm:text-[13px]" style={ts.color ? { color: ts.color } : undefined}>
{ts.title} {ts.title}
</div> </div>
{ts.sub && <div className="truncate text-[10px] text-muted sm:text-[11px]">{ts.sub}</div>} {ts.sub && <div className="truncate text-[10px] text-muted sm:text-[11px]">{ts.sub}</div>}
@@ -248,12 +272,12 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit
</InfoCell> </InfoCell>
</div> </div>
{/* تاریخ‌ها و اکشن‌ها (۲ ستونه در موبایل، نواری در دسکتاپ) */} {/* تاریخ‌ها و اکشن‌ها */}
<div className="grid grid-cols-2 gap-2 border-t border-line bg-bg/40 p-2.5 text-[11px] text-muted sm:flex sm:flex-wrap sm:items-center sm:gap-x-5 sm:gap-y-2 sm:px-4 sm:py-2 sm:text-[11.5px]"> <div className="grid grid-cols-2 gap-2 border-t border-line bg-bg/40 p-2.5 text-[11px] text-muted sm:flex sm:flex-wrap sm:items-center sm:gap-x-5 sm:gap-y-2 sm:px-4 sm:py-2 sm:text-[11.5px]">
<span className="inline-flex items-center gap-1.5 truncate"> <span className="inline-flex items-center gap-1.5 truncate">
<Package className="h-3 w-3 shrink-0" /> <Package className="h-3 w-3 shrink-0" />
<span>تاریخ:</span> <span>تاریخ:</span>
<b className="text-ink tabular-nums">{fmtDate(q.quote_date)}</b> <b className="tabular-nums text-ink">{fmtDate(q.quote_date)}</b>
</span> </span>
<span className="inline-flex items-center gap-1.5 truncate"> <span className="inline-flex items-center gap-1.5 truncate">
@@ -262,7 +286,7 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit
<b className={cn("tabular-nums", expired ? "text-amber" : "text-ink")}> <b className={cn("tabular-nums", expired ? "text-amber" : "text-ink")}>
{fmtDate(q.expiry_date)} {fmtDate(q.expiry_date)}
</b> </b>
{expired && <AlarmClock className="h-3 w-3 text-amber shrink-0" aria-label="منقضی" />} {expired && <AlarmClock className="h-3 w-3 shrink-0 text-amber" aria-label="منقضی" />}
</span> </span>
<span className="inline-flex items-center gap-1.5"> <span className="inline-flex items-center gap-1.5">