diff --git a/frontend/src/components/MilestoneRow.tsx b/frontend/src/components/MilestoneRow.tsx index 995f234..9561b43 100644 --- a/frontend/src/components/MilestoneRow.tsx +++ b/frontend/src/components/MilestoneRow.tsx @@ -12,15 +12,19 @@ export interface MilestoneRowProps { // یک ردیف مرحله‌ی پرداخت: تاریخ + مبلغ + دکمه‌ی حذف export function MilestoneRow({ milestone, onChange, onRemove, disabled = false }: MilestoneRowProps) { return ( -
- onChange({ ...milestone, when_iso: iso })} - placeholder="تاریخ" - className="w-36 shrink-0" - disabled={disabled} - /> -
+
+ {/* کادر انتخاب تاریخ */} +
+ onChange({ ...milestone, when_iso: iso })} + placeholder="تاریخ" + disabled={disabled} + /> +
+ + {/* کادر ورود مبلغ و حروف زیر آن */} +
onChange({ ...milestone, amount })} @@ -29,11 +33,18 @@ export function MilestoneRow({ milestone, onChange, onRemove, disabled = false } disabled={disabled} />
+ + {/* دکمه حذف */} {!disabled && ( - )}
); -} +} \ No newline at end of file diff --git a/frontend/src/pages/Dashboard/AcceptModal.tsx b/frontend/src/pages/Dashboard/AcceptModal.tsx index 9a40ff2..8b4382d 100644 --- a/frontend/src/pages/Dashboard/AcceptModal.tsx +++ b/frontend/src/pages/Dashboard/AcceptModal.tsx @@ -36,7 +36,6 @@ export interface AcceptModalProps { onClose: () => void; } -// مودال پذیرش: انتخاب/ویرایش شرایط پرداخت + تخصیص قلم‌ها + فاکتورها export function AcceptModal({ quoteId, onClose }: AcceptModalProps) { const quotes = useQuotes(); const suppliers = useSuppliers(); @@ -51,13 +50,10 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) { 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); - // وقتی quote باز شد، state اولیه را بساز useMemo(() => { if (!q) return; const offered = q.termsheets.filter((t) => t.status === "offered"); @@ -70,7 +66,6 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) { }); setRemovedItems(rem); setAssignments({}); - // پروژه‌ی هر قلم — تخصیصِ تکی: از تخصیص قبلی قلم، وگرنه اولین پروژه‌ی پیشنهاد const projSel: Record = {}; q.line_items.forEach((li) => { const names = (li.projects || []).map((p) => p.name).filter(Boolean); @@ -86,7 +81,6 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) { setReady(true); }, [q]); - // پیوست‌ها مستقیماً از سرور (MinIO) خوانده می‌شوند — نه از state محلی useEffect(() => { if (!quoteId) return; let alive = true; @@ -109,7 +103,6 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) { const allItems = q.line_items || []; const active = activeItems(q); - // گزینه‌های پروژه — پروژه‌های ثبت‌شده + پروژه‌های این پیشنهاد + تخصیص‌های قبلی قلم‌ها const projectOptions = (() => { const set = new Set(); projects.forEach((p) => p.name && set.add(p.name)); @@ -147,10 +140,8 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) { const confirm = () => { if (!q) return; - // پچ نهایی — همه‌ی تغییرات روی همین کلون اعمال می‌شود (نه روی آبجکت store) 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); @@ -164,11 +155,10 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) { }); if (!cardMeta.size) { - toast.error("حداقل یک conditions با حداقل یک مرحله پرداخت لازم است."); + toast.error("حداقل یک شرایط پرداخت با حداقل یک مرحله پرداخت لازم است."); return; } - // رد کردن قلم‌ها — در داده می‌مانند تا با بازگشت وضعیت برگردند next.line_items.forEach((li) => { if (removedItems.has(li.id)) li.rejected = true; }); @@ -178,7 +168,6 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) { return; } - // فقط termsheetهایی که حداقل یک قلم انتخاب کرده پذیرفته می‌شوند const fallbackDom = [...cardMeta.keys()][0]; const chosenDomIds = new Set(); active.forEach((li) => { @@ -200,7 +189,6 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) { } }); - // قلم‌ها: اتصال‌های offered حفظ می‌شوند + جفت پذیرفته‌شده ثبت می‌شود active.forEach((li) => { const dom = assignments[li.id] || fallbackDom; const finalId = domToFinal[dom]; @@ -210,7 +198,6 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) { li.accepted_ts_id = finalId || null; }); - // termsheet پذیرفته‌شده‌ای که هیچ قلمی انتخابش نکرده → برمی‌گردد به offered const usedIds = new Set(); active.forEach((li) => { if (li.accepted_ts_id) usedIds.add(li.accepted_ts_id); @@ -242,147 +229,215 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) { return ( !o && onClose()}> - - - پذیرش پیشنهاد — {sup.name} - + +
+ + {/* هدر ثابت و هم‌تراز */} +
+ + + پذیرش پیشنهاد — {sup.name} + + +

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

+
-
- شرایط پرداخت را انتخاب یا ویرایش کنید. سپس هر قلم را به conditions دلخواه تخصیص دهید. -
- - {/* کارت‌های termsheet */} -
- {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) })} - /> + {/* بدنه اسکرول‌پذیر */} +
+ + {/* بخش ۱: تنظیم شرایط پرداخت */} +
+

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

+
+ {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) })} + /> + ))} +
+ +
))}
- -
- ))} -
- + - {/* تخصیص قلم‌ها */} -
-
تخصیص قلم‌ها به conditions:
-
- قلمی که می‌خرید را تیک بزنید؛ قلمی که نمی‌خرید را بدون تیک رها کنید تا از معامله حذف شود. -
-
- {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 ? ( + {/* بخش ۲: تخصیص اقلام */} +
+

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

+
+ قلمی که می‌خرید را تیک بزنید؛ قلمی که نمی‌خرید را بدون تیک رها کنید تا از معامله حذف شود. +
+
+ {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 : "حذف فاکتور ناموفق بود"), + ) + } + urlOf={(a) => (a.id ? api.files.downloadUrl(q.id, "invoice", a.id) : null)} + /> +
+ +
+ +
+

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

+ 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 : "حذف پیش‌فاکتور ناموفق بود"), + ) + } + urlOf={(a) => (a.id ? api.files.downloadUrl(q.id, "proforma", a.id) : null)} + label="افزودن پیش‌فاکتور" + /> +
+ +
+ +
+ + +
+
+
-
{/* فاکتورهای اصلی */}
@@ -427,13 +482,20 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) {
- - - - + {/* فوتر ثابت */} +
+ + + + +
+ +
); -} +} \ No newline at end of file