diff --git a/backend/app/db/init_db.py b/backend/app/db/init_db.py index d8e175c..2a6b25d 100644 --- a/backend/app/db/init_db.py +++ b/backend/app/db/init_db.py @@ -345,6 +345,18 @@ BASE_UNITS: list[dict] = [ {"code": "m", "name_fa": "متر", "dimension": "length", "si_factor": 1, "is_base": True}, {"code": "bag", "name_fa": "کیسه", "dimension": "count", "si_factor": 1, "is_base": False}, {"code": "ton", "name_fa": "تن", "dimension": "mass", "si_factor": 1000, "is_base": False}, + {"code": "g", "name_fa": "گرم", "dimension": "mass", "si_factor": 0.001, "is_base": False}, + {"code": "kpiece", "name_fa": "هزار عدد", "dimension": "count", "si_factor": 1000, "is_base": False}, +] + +# تبدیل‌های ثابت جهانی («تبدیل‌های ثابت — همه متریال‌ها») — ضرایب استاندارد و +# فراگیر که در هر استارت‌آپ به‌صورت idempotent ذخیره می‌شوند. کلید یکتا: +# (from_unit, to_unit)؛ مقدار فیلدها، نام نمایشی فارسی واحد است — همان +# قالبی که فرانت‌اند در standard_conversions می‌نویسد. +STANDARD_CONVERSIONS: list[dict] = [ + {"from_unit": "تن", "to_unit": "کیلوگرم", "factor": 1000}, + {"from_unit": "کیلوگرم", "to_unit": "گرم", "factor": 1000}, + {"from_unit": "هزار عدد", "to_unit": "عدد", "factor": 1000}, ] @@ -379,6 +391,14 @@ async def ensure_v26(engine: AsyncEngine) -> None: "VALUES (:code, :name_fa, :dimension, :si_factor, :is_base) ON CONFLICT (code) DO NOTHING", u, ) + # Seed the global fixed conversions (idempotent — unique key from/to unit). + for c in STANDARD_CONVERSIONS: + await run_statement( + "INSERT INTO standard_conversions (from_unit, to_unit, factor) " + "VALUES (:from_unit, :to_unit, :factor) " + "ON CONFLICT (from_unit, to_unit) DO NOTHING", + c, + ) # v2.5/v2.6 category decoupling from materials: drop trigger, function, and column. await run_statement("DROP TRIGGER IF EXISTS trg_supplier_materials_derive_category ON supplier_materials") await run_statement("DROP FUNCTION IF EXISTS fn_derive_material_category()") diff --git a/frontend/src/components/Combobox.tsx b/frontend/src/components/Combobox.tsx index a1a320c..3b02ec8 100644 --- a/frontend/src/components/Combobox.tsx +++ b/frontend/src/components/Combobox.tsx @@ -25,6 +25,9 @@ export interface ComboboxProps { onAdd?: (val: string) => void; onInputChange?: (val: string) => void; onBlur?: () => void; + // با false، فوکوسِ برنامه‌ای (مثل فوکوسِ خودکار Dialog) منو را باز نمی‌کند — + // فقط کلیک یا تایپِ صریح کاربر بازش می‌کند. + openOnFocus?: boolean; } function toItem(x: ComboboxSourceItem): ComboboxItem { @@ -49,6 +52,7 @@ export function Combobox({ onAdd, onInputChange, onBlur, + openOnFocus = true, }: ComboboxProps) { const [text, setText] = useState(value ?? ""); const [open, setOpen] = useState(false); @@ -132,9 +136,13 @@ export function Combobox({ onChange={(e) => { setText(e.target.value); setHighlight(-1); + if (!open) setOpen(true); onInputChange?.(e.target.value); }} - onFocus={() => setOpen(true)} + onFocus={() => { + if (openOnFocus) setOpen(true); + }} + onClick={() => setOpen(true)} onBlur={onBlur} onKeyDown={onKeyDown} /> diff --git a/frontend/src/pages/Dashboard/EditModal.tsx b/frontend/src/pages/Dashboard/EditModal.tsx index 5b667a7..1b8aba8 100644 --- a/frontend/src/pages/Dashboard/EditModal.tsx +++ b/frontend/src/pages/Dashboard/EditModal.tsx @@ -88,6 +88,7 @@ export function EditModal({ quoteId, onClose }: EditModalProps) { const [selectedProjects, setSelectedProjects] = useState([]); const [notes, setNotes] = useState(""); const [invoices, setInvoices] = useState([]); + const [proformas, setProformas] = useState([]); const [itemRows, setItemRows] = useState([]); const [tsCards, setTsCards] = useState([]); const [quoteDate, setQuoteDate] = useState(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} /> @@ -724,6 +730,23 @@ export function EditModal({ quoteId, onClose }: EditModalProps) { /> + {/* پیش‌فاکتورها — در تمامی مراحل چرخه عمر پیشنهاد قابل افزودن/ویرایش است */} +
+
پیش‌فاکتورها (چند فایل):
+ 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)} + /> +
+ {/* تاریخ‌ها و وضعیت */}
تاریخ‌ها و وضعیت:
diff --git a/frontend/src/pages/Dashboard/QuoteCard.tsx b/frontend/src/pages/Dashboard/QuoteCard.tsx index 5f210e3..738d8df 100644 --- a/frontend/src/pages/Dashboard/QuoteCard.tsx +++ b/frontend/src/pages/Dashboard/QuoteCard.tsx @@ -140,12 +140,26 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit {sup.name.slice(0, 1)}
- - {sup.name} - +
+ + {sup.name} + + {/* فاکتور رسمی / غیررسمی — از چک‌باکس «فاکتور رسمی» هنگام ثبت پیشنهاد */} + + {q.is_official ? "رسمی" : "غیررسمی"} + +
{sup.location &&
{sup.location}
}
diff --git a/frontend/src/pages/Units/UnitsPage.tsx b/frontend/src/pages/Units/UnitsPage.tsx index 4dc1f74..e1c5717 100644 --- a/frontend/src/pages/Units/UnitsPage.tsx +++ b/frontend/src/pages/Units/UnitsPage.tsx @@ -496,7 +496,7 @@ export function UnitsPage() { )} /> )} - {g.base} + {g.base} {hasChildren ? faNum(g.variants.length) : g.repRef || "—"} @@ -519,7 +519,7 @@ export function UnitsPage() { )} onClick={() => setMaterialId(v.id)} > - {v.label} + {v.label} {v.refUnit || "—"} ))}