Files
ERP/frontend/src/pages/Wizard/Step5Review.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

145 lines
6.5 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 { Paperclip } from "lucide-react";
import { faNum } from "../../lib/formatters";
import { groupCommas } from "../../lib/numutil";
import { JalaliDatePicker } from "../../components/JalaliDatePicker";
export interface Step5Props {
quoteDate: string | null;
onQuoteDate: (iso: string | null) => void;
expiryDate: string | null;
onExpiryDate: (iso: string | null) => void;
notes: string;
onNotes: (v: string) => void;
proforma: File | null;
onProforma: (f: File | null) => void;
// خلاصه‌ی بازبینی
summary: {
supplierName: string;
topicName: string;
projects: string[];
itemCount: number;
tsCount: number;
grandTotal: number;
isOfficial: boolean;
orphanCount: number;
};
}
// مرحله ۵ — تاریخ، یادداشت، پیش‌فاکتور و بازبینی نهایی
export function Step5Review({
quoteDate,
onQuoteDate,
expiryDate,
onExpiryDate,
notes,
onNotes,
proforma,
onProforma,
summary,
}: Step5Props) {
const rows: [string, string][] = [
["تأمین‌کننده", summary.supplierName],
["موضوع", summary.topicName],
["پروژه‌ها", summary.projects.join("، ") || "—"],
["تعداد اقلام", groupCommas(summary.itemCount, true)],
["تعداد termsheet", groupCommas(summary.tsCount, true)],
["جمع کل تقریبی", groupCommas(summary.grandTotal, true) + " تومان"],
["فاکتور رسمی", summary.isOfficial ? "بله" : "خیر"],
["پیش‌فاکتور", proforma ? proforma.name : "—"],
];
return (
<div>
<h3 className="mb-3 text-[15px] font-bold">
<span className="me-1 inline-flex h-6 w-6 items-center justify-center rounded-full bg-blue-600 text-[12px] font-bold text-white">۵</span>
تاریخ، یادداشت و بازبینی نهایی
</h3>
<div className="grid max-w-2xl grid-cols-12 gap-3">
{/* تاریخ پیشنهاد: فضای بزرگتر (۷ ستون از ۱۲) به همراه ساعت */}
<div className="field col-span-7 [&>div]:w-full [&_input]:w-full" data-field="quote_date">
<label>تاریخ پیشنهاد</label>
<JalaliDatePicker value={quoteDate} onChange={onQuoteDate} placeholder="۱۴۰۳/۰۵/۱۲" withTime />
<div className="hint">پیش‌فرض امروز</div>
</div>
{/* تاریخ انقضای پیشنهاد: فضای کوچکتر (۵ ستون از ۱۲) */}
<div className="field col-span-5 [&>div]:w-full [&_input]:w-full" data-field="expiry_date">
<label>تاریخ انقضای پیشنهاد</label>
<JalaliDatePicker value={expiryDate} onChange={onExpiryDate} placeholder="۱۴۰۳/۰۶/۱۲" />
<div className="hint">تا چه زمانی این قیمت معتبر است؟</div>
</div>
<div className="field col-span-12" data-field="notes">
<label>یادداشت (اختیاری)</label>
<textarea
rows={2}
className="w-full rounded-lg border border-line bg-panel px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
placeholder="توضیحات اضافی درباره تماس یا توافق..."
value={notes}
onChange={(e) => onNotes(e.target.value)}
/>
</div>
<div className="field col-span-12" data-field="proforma">
<label>پیش‌فاکتور (اختیاری)</label>
{proforma ? (
/* وضعیت فایل انتخاب‌شده با دکمه حذف تمیز */
<div className="flex items-center justify-between rounded-xl border border-gray-200 bg-gray-50/70 px-3 py-2 text-xs">
<div className="flex items-center gap-2 truncate text-gray-700">
<Paperclip className="h-4 w-4 text-blue-600 shrink-0" />
<span className="font-medium truncate">{proforma.name}</span>
<span className="text-[11px] text-gray-400 shrink-0">
({Math.round(proforma.size / 1024)} کیلوبایت)
</span>
</div>
<button
type="button"
className="flex h-7 w-7 items-center justify-center rounded-lg text-gray-400 hover:bg-red-50 hover:text-red-500 transition-colors"
title="حذف فایل"
onClick={() => onProforma(null)}
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
) : (
/* دکمه آپلود مینیمال با حاشیه خط‌چین ملایم */
<label className="inline-flex cursor-pointer items-center gap-2 rounded-xl border border-dashed border-gray-300 bg-gray-50/50 px-4 py-2 text-xs font-medium text-gray-600 hover:border-blue-500 hover:bg-blue-50/40 hover:text-blue-600 transition-all">
<Paperclip className="h-4 w-4 text-gray-400" />
<span>انتخاب یا پیوست فایل پیش‌فاکتور</span>
<input
type="file"
className="hidden"
accept=".pdf,.png,.jpg,.jpeg,.xlsx,.xls,.docx"
onChange={(e) => {
const f = e.target.files?.[0];
if (f) onProforma(f);
}}
/>
</label>
)}
</div>
</div>
<div className="mt-4 rounded-xl border border-line bg-panel p-4 shadow-sm">
<h4 className="mb-2 text-[13.5px] font-bold">خلاصه‌ی پیشنهاد پیش از ذخیره</h4>
{rows.map(([label, val]) => (
<div key={label} className="review-row">
<span className="r-label">{label}</span>
<span className="r-val">{val}</span>
</div>
))}
{summary.orphanCount > 0 && (
<div className="review-row">
<span className="r-label">هشدار</span>
<span className="r-val review-warn">
{faNum(summary.orphanCount)} قلم به هیچ termsheet وصل نیست -- در ذخیره‌سازی به‌طور خودکار به اولین termsheet وصل می‌شود
</span>
</div>
)}
</div>
</div>
);
}