feat(quotes-accept): single-project select per line item

Replaces the MultiSelectCombobox project picker in the accept modal with a
single-select dropdown styled like the payment-terms select. Each kept item
defaults to its previous project (or the quote's first), carries exactly one
project into item_projects AND into the decision payload on accept —
matching the backend's single purchased-project model.
This commit is contained in:
2026-09-08 14:42:24 +03:30
parent dbe53a727c
commit eb1043cc3a
+36 -23
View File
@@ -24,7 +24,6 @@ import {
import { MilestoneRow } from "../../components/MilestoneRow"; import { MilestoneRow } from "../../components/MilestoneRow";
import { FileList } from "../../components/FileList"; import { FileList } from "../../components/FileList";
import { JalaliDatePicker } from "../../components/JalaliDatePicker"; import { JalaliDatePicker } from "../../components/JalaliDatePicker";
import { MultiSelectCombobox } from "../../components/MultiSelectCombobox";
interface CardDraft { interface CardDraft {
domId: string; domId: string;
@@ -54,7 +53,7 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) {
const [removedItems, setRemovedItems] = useState<Set<string>>(new Set()); const [removedItems, setRemovedItems] = useState<Set<string>>(new Set());
const [assignments, setAssignments] = useState<Record<string, string>>({}); const [assignments, setAssignments] = useState<Record<string, string>>({});
// تخصیص هر قلم به پروژه‌ها (نام‌ها) // تخصیص هر قلم به پروژه‌ها (نام‌ها)
const [itemProjectsSel, setItemProjectsSel] = useState<Record<string, string[]>>({}); const [itemProjectsSel, setItemProjectsSel] = useState<Record<string, string>>({});
// تاریخ و زمان پذیرش — پیش‌فرض الان // تاریخ و زمان پذیرش — پیش‌فرض الان
const [acceptDate, setAcceptDate] = useState<string | null>(() => new Date().toISOString()); const [acceptDate, setAcceptDate] = useState<string | null>(() => new Date().toISOString());
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
@@ -72,17 +71,15 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) {
}); });
setRemovedItems(rem); setRemovedItems(rem);
setAssignments({}); setAssignments({});
// پروژه‌های هر قلم — از تخصیص‌های قبلی، وگرنه پروژه‌های سطح پیشنهاد // پروژه‌ی هر قلم — تخصیصِ تکی: از تخصیص قبلی قلم، وگرنه اولین پروژه‌ی پیشنهاد
const projSel: Record<string, string[]> = {}; const projSel: Record<string, string> = {};
q.line_items.forEach((li) => { q.line_items.forEach((li) => {
const names = (li.projects || []).map((p) => p.name).filter(Boolean); const names = (li.projects || []).map((p) => p.name).filter(Boolean);
projSel[li.id] = names.length projSel[li.id] = names.length
? names ? names[0]
: q.projects && q.projects.length : q.projects && q.projects.length
? [...q.projects] ? q.projects[0]
: q.project : q.project || "";
? [q.project]
: [];
}); });
setItemProjectsSel(projSel); setItemProjectsSel(projSel);
setReady(true); setReady(true);
@@ -221,14 +218,14 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) {
if (t.status === "accepted" && !usedIds.has(t.id)) t.status = "offered"; if (t.status === "accepted" && !usedIds.has(t.id)) t.status = "offered";
}); });
// پروژه‌های هر قلم + همگام‌سازی پروژه‌های سطح پیشنهاد // پروژه‌ی هر قلم (تکی) + همگام‌سازی پروژه‌های سطح پیشنهاد
const projUnion = new Set<string>(); const projUnion = new Set<string>();
next.line_items.forEach((li) => { next.line_items.forEach((li) => {
if (li.rejected) return; if (li.rejected) return;
const names = itemProjectsSel[li.id]; const name = itemProjectsSel[li.id];
if (names === undefined) return; if (name === undefined) return;
li.projects = names.map((n) => ({ id: "", name: n })); li.projects = name ? [{ id: "", name }] : [];
li.projects.forEach((p) => projUnion.add(p.name)); if (name) projUnion.add(name);
}); });
next.projects = [...projUnion]; next.projects = [...projUnion];
next.project = next.projects[0] || ""; next.project = next.projects[0] || "";
@@ -342,11 +339,11 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) {
</div> </div>
</div> </div>
{/* تخصیص قلم‌ها به پروژه‌ها */} {/* تخصیص قلم‌ها به پروژه */}
<div className="border-t border-line pt-3"> <div className="border-t border-line pt-3">
<div className="mb-1 text-[13px] font-bold">تخصیص قلم‌ها به پروژه‌ها:</div> <div className="mb-1 text-[13px] font-bold">پروژه‌ی هر قلم:</div>
<div className="mb-2 text-[11.5px] text-muted"> <div className="mb-2 text-[11.5px] text-muted">
برای هر متریال مشخص کنید به کدام پروژه (ها) تعلق می‌گیرد. برای هر متریال فقط یک پروژه انتخاب می‌شود — مثل شرایط پرداخت.
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{allItems {allItems
@@ -365,12 +362,28 @@ export function AcceptModal({ quoteId, onClose }: AcceptModalProps) {
{fmtNum(li.quantity)} {li.unit} {fmtNum(li.quantity)} {li.unit}
</span> </span>
</div> </div>
<MultiSelectCombobox {projectOptions.length ? (
values={itemProjectsSel[li.id] || []} <select
onChange={(names) => setItemProjectsSel((s) => ({ ...s, [li.id]: names }))} className="min-w-[180px] rounded-md border border-line bg-panel px-2 py-1.5 font-inherit text-[12px]"
options={projectOptions} value={itemProjectsSel[li.id] ?? ""}
placeholder="انتخاب پروژه..." onChange={(e) =>
/> setItemProjectsSel((s) => ({ ...s, [li.id]: e.target.value }))
}
>
{(itemProjectsSel[li.id] ? [] : [{ name: "" }]).map((_, i) => (
<option key={i} value="">
انتخاب پروژه...
</option>
))}
{projectOptions.map((n) => (
<option key={n} value={n}>
{n}
</option>
))}
</select>
) : (
<div className="text-[12px] text-muted">پروژه‌ای ثبت نشده است.</div>
)}
</div> </div>
); );
})} })}