feat(quotes,units): official-invoice badge, conversion text wrapping, seeded fixed conversions, universal proforma, EditModal fixes
1) Quote card header: an official/unofficial badge (رسمی / غیررسمی) renders next to the supplier name from quotes.is_official (the «فاکتور رسمی» checkbox) — green for official, muted slate for unofficial. 2) Conversion Factors sidebar: the material group and variant labels no longer truncate with ellipsis — whitespace-normal + break-words so long Persian names wrap cleanly to the next line. 3) Global fixed conversions are now seeded in the unified seeder (app/db/init_db.py — the same file that seeds categories, base units and the superuser path), NOT hardcoded in the frontend: تن→کیلوگرم (1000), کیلوگرم→گرم (1000), هزار عدد→عدد (1000). The supporting units (گرم, هزار عدد) were added to the idempotent BASE_UNITS seed; inserts use ON CONFLICT (from_unit, to_unit) DO NOTHING against the uq_standard_conversion key so every startup is safe. Verified live: 3 rows after first boot AND still 3 rows after an api restart. 4) Universal proforma attachments: the Edit modal now has a پیشفاکتورها section (list/upload/delete via the existing MinIO-backed files API) alongside the invoices section, active at every quote lifecycle stage — including accepted/arrived and receipts-locked quotes (the section is deliberately not gated by hasReceipts; the backend already permits uploads in any status). View/delete of existing proformas unchanged elsewhere. 5) EditModal fixes: (a) the supplier Combobox no longer opens on mount — Radix Dialog autofocus was triggering its onFocus; Combobox gains an openOnFocus prop (default true) and the modal passes false, so the dropdown now opens only on explicit click or typing; (b) the line-item material field binds to the material's own name (m.name) instead of its category relation, and the save path matches materials by name+brand+specs accordingly. Validated: python compile, api + frontend image builds (tsc + vite) clean; seeding verified against the live local DB.
This commit is contained in:
@@ -88,6 +88,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
const [selectedProjects, setSelectedProjects] = useState<string[]>([]);
|
||||
const [notes, setNotes] = useState("");
|
||||
const [invoices, setInvoices] = useState<Attachment[]>([]);
|
||||
const [proformas, setProformas] = useState<Attachment[]>([]);
|
||||
const [itemRows, setItemRows] = useState<EditItemRow[]>([]);
|
||||
const [tsCards, setTsCards] = useState<EditTsCard[]>([]);
|
||||
const [quoteDate, setQuoteDate] = useState<string | null>(null);
|
||||
@@ -168,7 +169,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
key: li.id,
|
||||
origLi: li,
|
||||
projects: li.projects && li.projects.length ? [...li.projects] : [],
|
||||
cat: m.category || "",
|
||||
cat: m.name || "", // نام خود متریال — نه دسته/برند
|
||||
brand: m.brand || "",
|
||||
specs: materialSpecs(m),
|
||||
qty: li.quantity,
|
||||
@@ -197,7 +198,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
setReady(true);
|
||||
}, [q, suppliers, materials]);
|
||||
|
||||
// بارگذاری فاکتورها
|
||||
// بارگذاری فاکتورها و پیشفاکتورها
|
||||
useEffect(() => {
|
||||
if (!quoteId) return;
|
||||
let alive = true;
|
||||
@@ -205,6 +206,10 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
.list(quoteId, "invoice")
|
||||
.then((list) => alive && setInvoices(list))
|
||||
.catch(() => alive && setInvoices([]));
|
||||
void api.files
|
||||
.list(quoteId, "proforma")
|
||||
.then((list) => alive && setProformas(list))
|
||||
.catch(() => alive && setProformas([]));
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
@@ -314,7 +319,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
|
||||
let material = materialsCopy.find(
|
||||
(m) =>
|
||||
normalize(m.category) === normalize(cat) &&
|
||||
normalize(m.name) === normalize(cat) &&
|
||||
normalize(m.brand) === normalize(brand) &&
|
||||
specsKey(m.specs) === specsKey(specs),
|
||||
);
|
||||
@@ -460,6 +465,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
addSuffix="به عنوان تأمینکنندهٔ جدید"
|
||||
placeholder="نام تأمینکننده"
|
||||
onInputChange={(v) => setSupplierName(v)}
|
||||
openOnFocus={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -724,6 +730,23 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* پیشفاکتورها — در تمامی مراحل چرخه عمر پیشنهاد قابل افزودن/ویرایش است */}
|
||||
<div className="border-t border-line pt-3">
|
||||
<div className="mb-1 text-[13px] font-bold">پیشفاکتورها (چند فایل):</div>
|
||||
<FileList
|
||||
files={proformas}
|
||||
onChange={setProformas}
|
||||
upload={(f) => api.files.upload(q.id, "proforma", f)}
|
||||
onDelete={(a) =>
|
||||
a.id &&
|
||||
void api.files.remove(q.id, "proforma", a.id).catch((err) =>
|
||||
toast.error(err instanceof Error ? err.message : "حذف پیشفاکتور ناموفق بود"),
|
||||
)
|
||||
}
|
||||
urlOf={(a) => (a.id ? api.files.downloadUrl(q.id, "proforma", a.id) : null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* تاریخها و وضعیت */}
|
||||
<div className="border-t border-line pt-3">
|
||||
<div className="mb-2 text-[12px] font-bold text-muted">تاریخها و وضعیت:</div>
|
||||
|
||||
Reference in New Issue
Block a user