Wizard: - failed saves now route to the exact step+field: a new data-field attribute on every required section (supplier_id, line_items, termsheets, project, quote_date, expiry_date, notes, proforma) plus resolveErrorTarget() that maps ApiError.field / message keywords (supplier, line item/material/unit/quantity, termsheet/schedule/milestone, project, dates, notes, file) to the offending step — the «رفع خطا» toast action scrolls/focuses the right control instead of guessing - material records are no longer created on the fly while typing (rememberMaterial no longer fires addMaterial per blur); materials/specs/units are written once during the final save, avoiding half-created catalog rows Accept flow: - quote_service no longer rewrites LineItemProjects on accept — those candidate-project rows stay 100% untouched and only item_decisions.project_id is written, matching the per-item target-project model and the UI payload (item_projects: []) Dates/time: - JalaliDatePicker: hour/minute are now editable text fields (Persian digits, clamping, 3-digit retype handling) with a controlled useEffect that only rewrites the inputs when the numeric value actually changes — no more wiping in-progress typing; date-only detection tightened to a full YYYY-MM-DD match so datetime strings are not misread Also: /api validation logging (RequestValidationError) now includes the path and error list for faster diagnosis. Verified: api + frontend image builds (python, tsc, vite) clean on top of origin/production (incl. the team's mobile layout merges).
71 lines
3.0 KiB
TypeScript
71 lines
3.0 KiB
TypeScript
import type { Milestone } from "../../lib/types";
|
||
import { TermsheetCard } from "../../components/TermsheetCard";
|
||
import { itemRowLabel, type ItemDraft } from "./Step2LineItems";
|
||
|
||
export interface TsCardDraft {
|
||
key: string;
|
||
milestones: Milestone[];
|
||
}
|
||
|
||
export interface Step3Props {
|
||
cards: TsCardDraft[];
|
||
onCardsChange: (cards: TsCardDraft[]) => void;
|
||
items: ItemDraft[];
|
||
coverage: Record<string, string[]>; // cardKey → tempIdهای تیکخورده
|
||
onCoverageChange: (cardKey: string, ids: string[]) => void;
|
||
}
|
||
|
||
// پوشش مؤثر هر کارت: تیکهای صریح، یا (کارت اول بدون تیک) همهی اقلام
|
||
export function effectiveCovered(cards: TsCardDraft[], coverage: Record<string, string[]>, items: ItemDraft[], cardKey: string): string[] {
|
||
const explicit = coverage[cardKey];
|
||
if (explicit && explicit.length) return explicit;
|
||
if (cards[0]?.key === cardKey) return items.map((i) => i.tempId);
|
||
return [];
|
||
}
|
||
|
||
// مرحله ۳ — شرایط پرداخت (termsheetها) با چکلیست اقلام
|
||
export function Step3Termsheets({ cards, onCardsChange, items, coverage, onCoverageChange }: Step3Props) {
|
||
const itemOptions = items.map((it) => ({ tempId: it.tempId, label: itemRowLabel(it) }));
|
||
|
||
const addCard = () => {
|
||
onCardsChange([
|
||
...cards,
|
||
{ key: "new-ts-" + Date.now() + Math.random().toString(36).slice(2, 6), milestones: [{ when_iso: null, amount: null }] },
|
||
]);
|
||
};
|
||
|
||
const removeCard = (key: string) => {
|
||
onCardsChange(cards.filter((c) => c.key !== key));
|
||
onCoverageChange(key, []);
|
||
};
|
||
|
||
return (
|
||
<div>
|
||
<h3 className="mb-1 text-[15px] font-bold">
|
||
<span className="me-1 inline-flex h-6 w-6 items-center justify-center rounded-full bg-accent text-[12px] font-bold text-white">۳</span>
|
||
شرایط پرداخت — termsheet ها
|
||
</h3>
|
||
<div className="mb-3 text-[12px] text-muted" dangerouslySetInnerHTML={{ __html: 'هر <b>termsheet</b> یک روش پرداخت است (نقد، چک، اقساط...). داخل هر termsheet مشخص کنید کدام قلمها را پوشش میدهد — یک termsheet میتواند چند قلم را بگیرد، و یک قلم هم میتواند زیر چند termsheet مختلف (مثلاً هم نقد هم چک) پیشنهاد شود.' }} />
|
||
|
||
<div data-field="termsheets" className="flex flex-col gap-3">
|
||
{cards.map((card, idx) => (
|
||
<TermsheetCard
|
||
key={card.key}
|
||
index={idx + 1}
|
||
milestones={card.milestones}
|
||
onMilestonesChange={(ms) => onCardsChange(cards.map((c) => (c.key === card.key ? { ...c, milestones: ms } : c)))}
|
||
items={itemOptions}
|
||
coveredIds={effectiveCovered(cards, coverage, items, card.key)}
|
||
onCoverageChange={(ids) => onCoverageChange(card.key, ids)}
|
||
onRemove={() => removeCard(card.key)}
|
||
/>
|
||
))}
|
||
</div>
|
||
|
||
<button type="button" className="btn-line-file mt-3" onClick={addCard}>
|
||
+ افزودن termsheet
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|