fix(procurement): improve mobile layout for QuoteCard and EditModal
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user