feat(quotes): lock quotes with posted receipts; per-item target project + title dedup

Quotes that already have warehouse receipts can no longer be edited or rolled back:
- backend: quote_has_receipts() guard — replace_quote_children and every backward status transition now return 409 with «این پیشنهاد به‌دلیل ثبت رسید در انبار، غیرقابل ویرایش یا بازگشت است.»
- reopening to «offered» additionally clears item_decisions.project_id (the purchased-project link belongs to acceptance, not to a reopened offer)
- frontend EditModal: hasReceipts locks the whole form — disabled inputs and date pickers (JalaliDatePicker/NumberInput/MilestoneRow/MultiSelectCombobox gained disabled support), add/remove controls for items, termsheets and milestones hidden, status change blocked, amber warning banner; quote deletion now goes through a confirmation dialog (useDeleteQuote)
- date-only semantics: JalaliDatePicker keeps/emits YYYY-MM-DD when withTime=false (typing, «امروز», day selection) and toDateOnly no longer mangles pure date strings

Per-item target project (candidate projects stay intact):
- expand_service exposes the decision target project (decision.project + line_item.target_project)
- the accept payload derives item_decisions.project_id from that target project and sends item_projects: [] so line_item_projects keeps the original candidates untouched
- AcceptModal/EditModal select the target project per line item

Material titles:
- quoteUtils.formatMaterialTitle() composes one normalized, deduplicated «name + brand + specs» title, replacing materialLabel/materialSpecs/specsLabel usage across dashboard cards, quote detail and the modals

Verified: python compile + api/frontend image builds (tsc + vite) clean.
This commit is contained in:
2026-09-16 19:14:06 +03:30
parent 7ead60cd5c
commit 5084623be5
14 changed files with 444 additions and 158 deletions
+9 -4
View File
@@ -6,10 +6,11 @@ export interface MilestoneRowProps {
milestone: Milestone;
onChange: (m: Milestone) => void;
onRemove: () => void;
disabled?: boolean;
}
// یک ردیف مرحله‌ی پرداخت: تاریخ + مبلغ + دکمه‌ی حذف
export function MilestoneRow({ milestone, onChange, onRemove }: MilestoneRowProps) {
export function MilestoneRow({ milestone, onChange, onRemove, disabled = false }: MilestoneRowProps) {
return (
<div className="milestone-row">
<JalaliDatePicker
@@ -17,6 +18,7 @@ export function MilestoneRow({ milestone, onChange, onRemove }: MilestoneRowProp
onChange={(iso) => onChange({ ...milestone, when_iso: iso })}
placeholder="تاریخ"
className="w-36 shrink-0"
disabled={disabled}
/>
<div className="ms-amount-wrap">
<NumberInput
@@ -24,11 +26,14 @@ export function MilestoneRow({ milestone, onChange, onRemove }: MilestoneRowProp
onChange={(amount) => onChange({ ...milestone, amount })}
placeholder="مبلغ (تومان)"
showWords
disabled={disabled}
/>
</div>
<button type="button" className="ms-x" title="حذف" onClick={onRemove}>
×
</button>
{!disabled && (
<button type="button" className="ms-x" title="حذف" onClick={onRemove}>
×
</button>
)}
</div>
);
}