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
+4 -1
View File
@@ -10,6 +10,7 @@ export interface NumberInputProps {
showWords?: boolean; // نمایش معادل فارسی زیر input
inputMode?: "numeric" | "decimal" | undefined;
maxDecimals?: number; // حداکثر رقم اعشار (مثلاً ۲ برای قیمت واحد)
disabled?: boolean;
}
// ورودی عددی با فرمت‌دهی خودکار (groupCommas) و نمایش کلمات فارسی
@@ -21,6 +22,7 @@ export function NumberInput({
showWords = false,
inputMode = "numeric",
maxDecimals,
disabled = false,
}: NumberInputProps) {
// فرمت با رعایت حداکثر اعشار (گرد کردن)
const formatValue = (n: number): string => {
@@ -62,7 +64,8 @@ export function NumberInput({
<div className={cn("min-w-0", className)}>
<input
type="text"
className="num-input h-9 w-full rounded-lg border border-line bg-panel px-3 py-1 text-sm text-ink shadow-sm transition-colors placeholder:text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
disabled={disabled}
className="num-input h-9 w-full rounded-lg border border-line bg-panel px-3 py-1 text-sm text-ink shadow-sm transition-colors placeholder:text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 disabled:cursor-not-allowed disabled:opacity-60"
value={text}
placeholder={placeholder}
inputMode={inputMode}