Merge prod_v5 (catalog v2.7, warehouse locking, per-project accept) into integration

Conflict in init_db.py resolved keeping the audit_log table ensure
function (new-one) — prod_v5 predates it and its side was empty.
Both sides' SQL lands: audit_log table plus the category-free materials
model.
This commit is contained in:
MMD
2026-09-19 18:36:29 +03:30
26 changed files with 350 additions and 273 deletions
+26 -3
View File
@@ -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>