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:
@@ -3,13 +3,11 @@ import { Package, Scale, CreditCard, CalendarDays, Paperclip, Building2, Ban } f
|
||||
import type { Attachment, Quote, Store } from "../../lib/types";
|
||||
import { api } from "../../lib/api";
|
||||
import {
|
||||
formatMaterialTitle,
|
||||
itemTermsheetIds,
|
||||
lineItemTotal,
|
||||
materialById,
|
||||
materialLabel,
|
||||
materialSpecs,
|
||||
refConverted,
|
||||
specsLabel,
|
||||
} from "../../lib/quoteUtils";
|
||||
import { fmtDate, fmtDateTime, fmtNum } from "../../lib/formatters";
|
||||
import { groupCommas } from "../../lib/numutil";
|
||||
@@ -69,7 +67,6 @@ export function QuoteDetail({ store, q }: QuoteDetailProps) {
|
||||
const total = lineItemTotal(li);
|
||||
const projs = itemProjects(q, li);
|
||||
const conv = isRejected ? null : refConverted(store, li);
|
||||
const specText = specsLabel(materialSpecs(m));
|
||||
|
||||
// رسید جزیی → قلم به دو بخش «رسید شده» و «در راه» شکسته میشود (نمایشی):
|
||||
// مجموع دو بخش همیشه با تعداد/مبلغ اصلی برابر است (عدم تغییر جمع کل پیشنهاد).
|
||||
@@ -97,8 +94,7 @@ export function QuoteDetail({ store, q }: QuoteDetailProps) {
|
||||
>
|
||||
<div className="fb-name">
|
||||
<Package className="me-1 inline h-3 w-3 text-muted" />
|
||||
{materialLabel(m)}
|
||||
{specText ? ` · ${specText}` : ""}
|
||||
{formatMaterialTitle(m)}
|
||||
</div>
|
||||
<div className="fb-meta">
|
||||
<Building2 className="me-1 inline h-3 w-3" /> {projs.length ? projs.join("، ") : "—"}
|
||||
|
||||
Reference in New Issue
Block a user