import { useEffect, useState } from "react"; import { Package, Scale, CreditCard, CalendarDays, Paperclip, Building2, Ban } from "lucide-react"; import type { Attachment, Quote, Store } from "../../lib/types"; import { api } from "../../lib/api"; import { formatMaterialTitle, itemTermsheetIds, lineItemTotal, materialById, refConverted, } from "../../lib/quoteUtils"; import { fmtDate, fmtDateTime, fmtNum } from "../../lib/formatters"; import { groupCommas } from "../../lib/numutil"; import { FlowDiagram, type FlowRefs } from "./FlowDiagram"; import { itemProjects } from "../../lib/quoteUtils"; export interface QuoteDetailProps { store: Store; q: Quote; } // بخش expanded یک پیشنهاد: اقلام (راست) ↔ ترم‌شیت‌ها (چپ) با خطوط SVG رنگی export function QuoteDetail({ store, q }: QuoteDetailProps) { const [flowEl, setFlowEl] = useState(null); const [flowRefs] = useState(() => ({ items: new Map(), termsheets: new Map() })); const [bump, setBump] = useState(0); useEffect(() => { // بعد از نشستن layout (فونت/تصویر) خطوط دوباره رسم شوند const t = setTimeout(() => setBump((b) => b + 1), 350); return () => clearTimeout(t); }, []); void bump; // پیوست‌ها (فاکتور/پیش‌فاکتور) مستقیماً از سرور (MinIO) خوانده می‌شوند const [attachments, setAttachments] = useState<{ invoices: Attachment[]; proformas: Attachment[] }>({ invoices: [], proformas: [], }); useEffect(() => { let alive = true; void Promise.all([ api.files.list(q.id, "invoice").catch(() => [] as Attachment[]), api.files.list(q.id, "proforma").catch(() => [] as Attachment[]), ]).then(([invoices, proformas]) => { if (alive) setAttachments({ invoices, proformas }); }); return () => { alive = false; }; }, [q.id]); const tsList = q.termsheets || []; const allItems = q.line_items || []; const items = allItems.filter((li) => !li.rejected); const rejectedItems = allItems.filter((li) => li.rejected); // وقتی داده‌ی جریان عوض شد (پذیرش/رسید/ویرایش) خطوط دوباره رسم شوند const redrawKey = JSON.stringify({ items: allItems.map((li) => [li.id, li.accepted_ts_id, !!li.rejected, !!li.arrived_date]), ts: tsList.map((t) => [t.id, t.status]), }); const itemBox = (li: (typeof allItems)[number], isRejected: boolean) => { const m = materialById(store.materials, li.material_id); const tsIds = itemTermsheetIds(li).filter((tid) => tsList.some((t) => t.id === tid)); const total = lineItemTotal(li); const projs = itemProjects(q, li); const conv = isRejected ? null : refConverted(store, li); // رسید جزیی → قلم به دو بخش «رسید شده» و «در راه» شکسته می‌شود (نمایشی): // مجموع دو بخش همیشه با تعداد/مبلغ اصلی برابر است (عدم تغییر جمع کل پیشنهاد). const qty = li.quantity ?? 0; const recv = !isRejected ? li.received_qty || 0 : 0; const remaining = Math.max(0, qty - recv); const whNames = (li.received_warehouses || []).map((w) => w.name); const showSplit = !isRejected && q.status !== "offered" && qty > 0; const fullyReceived = showSplit && recv >= qty; return (
{ if (el) { flowRefs.items.set(li.id, el); el.dataset.tsIds = tsIds.join(","); el.dataset.acceptedTs = li.accepted_ts_id || ""; } else { flowRefs.items.delete(li.id); } }} className={`flow-box flow-item${isRejected ? " rejected" : ""}${li.arrived_date ? " deal" : ""}${ fullyReceived ? " fb-fully-received" : "" }`} >
{formatMaterialTitle(m)}
{projs.length ? projs.join("، ") : "—"}
{showSplit ? (
{recv > 0 && ( رسید شده ✓ {fmtNum(recv)} {li.unit || ""} )} {remaining > 0 && ( در راه ⏳ {fmtNum(remaining)} {li.unit || ""} )} × {groupCommas(li.unit_price, true)} = {total != null ? groupCommas(total, true) + " تومان" : "—"}
) : (
{fmtNum(li.quantity)} {li.unit || ""} × {groupCommas(li.unit_price, true)} = {total != null ? groupCommas(total, true) + " تومان" : "—"}
)} {showSplit && recv > 0 && (
✓ {fmtNum(recv)} {li.unit || ""} {li.arrived_date && تاریخ رسید: {fmtDateTime(li.arrived_date)}} {whNames.length > 0 && به انبار: {whNames.join("، ")}}
)} {conv && (
≈ {groupCommas(conv.qty, true)} {conv.unit} {conv.perRef != null ? ` · ${groupCommas(conv.perRef, true)} تومان/${conv.unit}` : ""}
)}
); }; const attachParts: React.ReactNode[] = []; attachments.proformas.forEach((pf, i) => { if (!pf.id) return; attachParts.push( پیش‌فاکتور: {pf.name} , ); }); attachments.invoices.forEach((inv, i) => { if (!inv.id) return; attachParts.push( فاکتور اصلی: {inv.name} , ); }); return (
اقلام ({fmtNum(items.length)})
{items.length ? items.map((li) => itemBox(li, false)) :
—
} {rejectedItems.length > 0 && ( <>
قلم‌های رد شده (از معامله خارج‌اند)
{rejectedItems.map((li) => itemBox(li, true))}
با بازگشت وضعیت به مرحله‌ی پیشین (یا ویرایش)، این قلم‌ها برمی‌گردند.
)}
شرایط پرداخت ({fmtNum(tsList.length)})
{tsList.length ? ( tsList.map((t) => { const isAccepted = t.status === "accepted"; const arrived = q.status === "arrived" && isAccepted; const badge = arrived ? ( رسیده ) : isAccepted ? ( ✓ پذیرفته ) : ( پیشنهاد ); return (
{ if (el) flowRefs.termsheets.set(t.id, el); else flowRefs.termsheets.delete(t.id); }} className={`flow-box flow-ts${isAccepted ? (arrived ? " ts-arrived" : " ts-accepted") : ""}`} >
{t.label} {badge}
{(t.milestones || []).length ? ( t.milestones.map((m, i) => (
{m.when_iso ? fmtDate(m.when_iso) : "—"} ·{" "} {m.amount != null ? groupCommas(m.amount, true) + " تومان" : "—"}
)) ) : (
—
)}
); }) ) : (
—
)}
{attachParts.length > 0 && (
{attachParts.map((p, i) => ( {p} ))}
)}
); }