import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import type { Attachment, Milestone, Quote } from "../../lib/types"; import { api, nextDashedId } from "../../lib/api"; import { useMaterials, useProjects, useQuotes, useUpdateQuote, useSuppliers } from "../../hooks"; import { activeItems, formatMaterialTitle, itemTermsheetIds, materialById, supplierById, } from "../../lib/quoteUtils"; import { faNum, fmtNum } from "../../lib/formatters"; import { groupCommas } from "../../lib/numutil"; import { Button } from "../../components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "../../components/ui/dialog"; import { MilestoneRow } from "../../components/MilestoneRow"; import { FileList } from "../../components/FileList"; import { JalaliDatePicker } from "../../components/JalaliDatePicker"; interface CardDraft { domId: string; isNew: boolean; milestones: Milestone[]; label: string | null; // null = پیش‌فرض «گزینه N» } export interface AcceptModalProps { quoteId: string | null; onClose: () => void; } export function AcceptModal({ quoteId, onClose }: AcceptModalProps) { const quotes = useQuotes(); const suppliers = useSuppliers(); const materials = useMaterials(); const projects = useProjects(); const updateQuote = useUpdateQuote(); const q = useMemo(() => quotes.find((x) => x.id === quoteId) || null, [quotes, quoteId]); const [cards, setCards] = useState([]); const [invoices, setInvoices] = useState([]); const [proformas, setProformas] = useState([]); const [removedItems, setRemovedItems] = useState>(new Set()); const [assignments, setAssignments] = useState>({}); const [itemProjectsSel, setItemProjectsSel] = useState>({}); const [acceptDate, setAcceptDate] = useState(() => new Date().toISOString()); const [ready, setReady] = useState(false); useMemo(() => { if (!q) return; const offered = q.termsheets.filter((t) => t.status === "offered"); setCards( offered.map((t) => ({ domId: t.id, isNew: false, milestones: JSON.parse(JSON.stringify(t.milestones)), label: t.label })), ); const rem = new Set(); q.line_items.forEach((li) => { if (li.rejected) rem.add(li.id); }); setRemovedItems(rem); setAssignments({}); const projSel: Record = {}; q.line_items.forEach((li) => { const names = (li.projects || []).map((p) => p.name).filter(Boolean); projSel[li.id] = li.target_project?.name ? li.target_project.name : names.length ? names[0] : q.projects && q.projects.length ? q.projects[0] : q.project || ""; }); setItemProjectsSel(projSel); setReady(true); }, [q]); useEffect(() => { if (!quoteId) return; let alive = true; void api.files .list(quoteId, "invoice") .then((list) => alive && setInvoices(list)) .catch(() => alive && setInvoices([])); void api.files .list(quoteId, "proforma") .then((list) => alive && setProformas(list)) .catch(() => alive && setProformas([])); return () => { alive = false; }; }, [quoteId]); if (!q || !ready) return null; const sup = supplierById(suppliers, q.supplier_id); const allItems = q.line_items || []; const active = activeItems(q); const projectOptions = (() => { const set = new Set(); projects.forEach((p) => p.name && set.add(p.name)); (q.projects || []).forEach((n) => set.add(n)); if (q.project) set.add(q.project); allItems.forEach((li) => (li.projects || []).forEach((p) => p.name && set.add(p.name))); return [...set].sort((a, b) => a.localeCompare(b, "fa")); })(); const cardLabel = (c: CardDraft, idx: number) => c.label || `گزینه ${faNum(idx + 1)}`; const updateCard = (domId: string, patch: Partial) => { setCards((cs) => cs.map((c) => (c.domId === domId ? { ...c, ...patch } : c))); }; const addNewCard = () => { setCards((cs) => [...cs, { domId: "new-ts-" + Date.now(), isNew: true, milestones: [{ when_iso: null, amount: null }], label: null }]); }; const removeCard = (domId: string) => { setCards((cs) => cs.filter((c) => c.domId !== domId)); setAssignments((a) => { const next: Record = {}; Object.entries(a).forEach(([liId, dom]) => { if (dom !== domId) next[liId] = dom; }); return next; }); }; const assign = (liId: string, domId: string) => { setAssignments((a) => ({ ...a, [liId]: domId })); }; const confirm = () => { if (!q) return; const next: Quote = JSON.parse(JSON.stringify(q)); const cardMeta = new Map(); cards.forEach((c, idx) => { const milestones = c.milestones.filter((m) => m.when_iso || m.amount != null); if (!milestones.length) return; const existing = next.termsheets.find((t) => t.id === c.domId); cardMeta.set(c.domId, { milestones, label: c.label || `گزینه ${faNum(idx + 1)}`, existing, }); }); if (!cardMeta.size) { toast.error("حداقل یک شرایط پرداخت با حداقل یک مرحله پرداخت لازم است."); return; } next.line_items.forEach((li) => { if (removedItems.has(li.id)) li.rejected = true; }); const active = activeItems(next); if (!active.length) { toast.error("همه‌ی قلم‌ها رد شده‌اند؛ حداقل یک قلم لازم است."); return; } const fallbackDom = [...cardMeta.keys()][0]; const chosenDomIds = new Set(); active.forEach((li) => { const dom = assignments[li.id] || fallbackDom; if (dom && cardMeta.has(dom)) chosenDomIds.add(dom); }); const domToFinal: Record = {}; chosenDomIds.forEach((dom) => { const meta = cardMeta.get(dom)!; if (meta.existing) { meta.existing.milestones = meta.milestones; meta.existing.status = "accepted"; domToFinal[dom] = dom; } else { const newId = nextDashedId("ts", next.termsheets); next.termsheets.push({ id: newId, label: meta.label, milestones: meta.milestones, status: "accepted" }); domToFinal[dom] = newId; } }); active.forEach((li) => { const dom = assignments[li.id] || fallbackDom; const finalId = domToFinal[dom]; const current = itemTermsheetIds(li).filter((tid) => next.termsheets.some((t) => t.id === tid)); if (finalId && !current.includes(finalId)) current.push(finalId); li.termsheet_ids = current.length ? current : finalId ? [finalId] : current; li.accepted_ts_id = finalId || null; }); const usedIds = new Set(); active.forEach((li) => { if (li.accepted_ts_id) usedIds.add(li.accepted_ts_id); }); next.termsheets.forEach((t) => { if (t.status === "accepted" && !usedIds.has(t.id)) t.status = "offered"; }); // پروژه‌ی هر قلم (تکی هدف خرید) — فقط target_project تنظیم می‌شود؛ // li.projects و next.projects دست‌نخورده باقی می‌مانند تا پروژه‌های کاندیدای اولیه حفظ شوند next.line_items.forEach((li) => { if (li.rejected) return; const name = itemProjectsSel[li.id]; if (name === undefined) return; li.target_project = name ? { id: "", name } : null; }); const now = new Date().toISOString(); next.accepted_date = acceptDate || now; next.status = "accepted"; next.events.push({ type: "accepted", occurred_at: now }); next.invoices = invoices; next.proformas = proformas; void updateQuote.mutateAsync({ id: q.id, patch: next }); setReady(false); onClose(); }; return ( !o && onClose()}>
{/* هدر ثابت و هم‌تراز */}
پذیرش پیشنهاد — {sup.name} کد: {q.id.startsWith("PR-") ? q.id : `PR-${q.id}`}

شرایط پرداخت را ویرایش کنید. سپس هر قلم را به بخش و پروژه دلخواه تخصیص دهید.

{/* بدنه اسکرول‌پذیر */}
{/* بخش ۱: تنظیم شرایط پرداخت */}

۱. تنظیم شرایط پرداخت

{cards.map((c, idx) => (
{cardLabel(c, idx)}
{c.milestones.map((m, i) => ( { const ms = [...c.milestones]; ms[i] = nextM; updateCard(c.domId, { milestones: ms }); }} onRemove={() => updateCard(c.domId, { milestones: c.milestones.filter((_, j) => j !== i) })} /> ))}
))}
{/* بخش ۲: تخصیص اقلام */}

۲. تخصیص اقلام به شرایط پرداخت

قلمی که می‌خرید را تیک بزنید؛ قلمی که نمی‌خرید را بدون تیک رها کنید تا از معامله حذف شود.
{allItems.map((li) => { const removed = removedItems.has(li.id); const m = materialById(materials, li.material_id); return (
{ const next = new Set(removedItems); if (e.target.checked) next.delete(li.id); else next.add(li.id); setRemovedItems(next); }} />
{formatMaterialTitle(m)}
{fmtNum(li.quantity)} {li.unit} × {groupCommas(li.unit_price, true)}
); })} {active.length === 0 && (
⚠️ همه‌ی قلم‌ها رد شده‌اند — حداقل یک قلم را تیک بزنید.
)}
{/* بخش ۳: تخصیص اقلام به پروژه */}

۳. تخصیص اقلام به پروژه‌ها

برای هر متریال خریداری شده، می‌توانید پروژه مربوط به آن را مشخص کنید.
{allItems .filter((li) => !removedItems.has(li.id)) .map((li) => { const m = materialById(materials, li.material_id); return (
{formatMaterialTitle(m)} {fmtNum(li.quantity)} {li.unit}
{projectOptions.length ? ( ) : (
پروژه‌ای در سیستم ثبت نشده است.
)}
); })}
{/* بخش ۴: فاکتورها، پیش‌فاکتورها و تاریخ */}

فاکتورهای اصلی

api.files.upload(q.id, "invoice", f)} onDelete={(a) => a.id && void api.files.remove(q.id, "invoice", a.id).catch((err) => toast.error(err instanceof Error ? err.message : "حذف فاکتور ناموفق بود"), ) } onDownload={(a) => api.files.download(q.id, "invoice", a.id!, a.name)} loadPreview={(a) => api.files.previewUrl(q.id, "invoice", a.id!)} />

پیش‌فاکتورها

api.files.upload(q.id, "proforma", f)} onDelete={(a) => a.id && void api.files.remove(q.id, "proforma", a.id).catch((err) => toast.error(err instanceof Error ? err.message : "حذف پیش‌فاکتور ناموفق بود"), ) } onDownload={(a) => api.files.download(q.id, "proforma", a.id!, a.name)} loadPreview={(a) => api.files.previewUrl(q.id, "proforma", a.id!)} label="افزودن پیش‌فاکتور" />
{/* فوتر ثابت */}
); }