Files
ERP/frontend/src/pages/Wizard/Step3Termsheets.tsx
T
whqreza 43ff048899 fix(wizard,quotes,dates): reliable error routing, single-project accept, Persian date/time input
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).
2026-09-17 16:13:27 +03:30

71 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}