feat(frontend): add React UI — procurement office, wizard, suppliers, inventory panel (dash port), role-based login

This commit is contained in:
ali_mohaghegh
2026-08-25 19:08:15 +03:30
parent 170aed66ea
commit 4e105c5cd5
103 changed files with 16352 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
import { Paperclip } from "lucide-react";
import { faNum } from "../../lib/formatters";
import { groupCommas } from "../../lib/numutil";
import { JalaliDatePicker } from "../../components/JalaliDatePicker";
import { FileUpload } from "../../components/FileUpload";
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-accent text-[12px] font-bold text-white">۵</span>
تاریخ، یادداشت و بازبینی نهایی
</h3>
<div className="grid max-w-2xl grid-cols-2 gap-3">
<div className="field">
<label>تاریخ پیشنهاد</label>
<JalaliDatePicker value={quoteDate} onChange={onQuoteDate} placeholder="۱۴۰۳/۰۵/۱۲" withTime />
<div className="hint">پیش‌فرض امروز</div>
</div>
<div className="field">
<label>تاریخ انقضای پیشنهاد</label>
<JalaliDatePicker value={expiryDate} onChange={onExpiryDate} placeholder="۱۴۰۳/۰۶/۱۲" />
<div className="hint">تا چه زمانی این قیمت معتبر است؟</div>
</div>
<div className="field col-span-2">
<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-2">
<label>پیش‌فاکتور (اختیاری)</label>
{proforma ? (
<div className="inv-row">
<span className="inv-name">
<Paperclip className="me-1 inline h-3.5 w-3.5" />
{proforma.name}
</span>
<button type="button" className="inv-x" title="حذف" onClick={() => onProforma(null)}>
×
</button>
</div>
) : (
<FileUpload onUpload={onProforma} 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>
);
}