diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index d420281..796a15e 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -357,6 +357,57 @@ interface RawMaterialCategory { name: string; } +// یک ردیف material_units از بک‌اند (با expand=unit) +interface RawMaterialUnit { + id: string; + material_id: string; + brand_material_id: string | null; + unit_id: number; + conversion_factor: number; + is_default: boolean; + unit?: { id: number; code: string; name_fa: string | null } | null; +} + +export interface MaterialUnitRow { + id: string; + material_id: string; + brand_material_id: string | null; + unit_id: number; + conversion_factor: number; + is_default: boolean; + unit: { id: number; code: string; name_fa: string | null } | null; +} + +function adaptMaterialUnit(raw: RawMaterialUnit): MaterialUnitRow { + return { + id: raw.id, + material_id: raw.material_id, + brand_material_id: raw.brand_material_id, + unit_id: raw.unit_id, + conversion_factor: raw.conversion_factor, + is_default: raw.is_default, + unit: raw.unit ?? null, + }; +} + +// اتصال برند ↔ متریال (brand_materials) برای قید برند در ضریب‌های تبدیل +interface RawBrandLink { + id: string; + brand_id: string; + material_id: string; + brand?: { id: string; name: string } | null; +} + +export interface BrandLinkRow { + id: string; + brand_id: string; + brand_name: string; +} + +function adaptBrandLink(raw: RawBrandLink): BrandLinkRow { + return { id: raw.id, brand_id: raw.brand_id, brand_name: raw.brand?.name || "" }; +} + // فایل پیوست — پاسخ بک‌اند (metadata؛ بایت‌ها از endpoint دانلود می‌آیند) interface RawAttachment { id: string; @@ -703,10 +754,11 @@ export const api = { quantity: li.quantity ?? 1, project_ids: [], })); - // واحدهایی که در بک‌اند نیستند را بساز (issue: واحد جدید) — واحد خالی = «عدد» + // واحدِ تایپ‌شده‌ی کاربر در بک‌اند نیست → همان موقع ساخته می‌شود (بدون هیچ + // مقدار پیش‌فرض/واحد جادویی؛ واحد خالی یعنی خطای صریح «واحد مشخص نشده») // پروژه‌های هر قلم هم به id واقعی ترجمه می‌شوند (در صورت نبودن ساخته می‌شوند) for (let i = 0; i < lineItems.length; i++) { - lineItems[i].unit_id = await unitIdByNameOrCreate(lookup, q.line_items[i].unit || "عدد"); + lineItems[i].unit_id = await unitIdByNameOrCreate(lookup, q.line_items[i].unit); for (const p of q.line_items[i].projects || []) { const pid = looksLikeUuid(p.id) ? p.id : await projectIdByNameOrCreate(p.name); if (pid) lineItems[i].project_ids.push(pid); @@ -822,10 +874,16 @@ export const api = { item_decisions: (patch.line_items || []).map((li) => { const idx = li.accepted_ts_id ? acceptedTs.findIndex((t) => t.id === li.accepted_ts_id) : -1; const chosen = !li.rejected && idx >= 0; + // پروژه‌ی واحدِ انتخاب‌شده برای این قلم — مستقیم داخل decision می‌رود تا + // «کالاهای در راه» همان مدیر پروژه را (با item_decisions.project_id) + // نشان دهد؛ قبلاً فقط item_projects جداگانه می‌رفت و decision بدون + // پروژه می‌ماند (قلم‌ها در پنل هیچ مدیر پروژه‌ای دیده نمی‌شدند) + const chosenProjId = itemProjects.find((p) => p.line_item_id === li.id)?.project_ids[0]; return { line_item_id: li.id, status: chosen ? "chosen" : "rejected", ...(chosen ? { accepted_termsheet_index: idx } : {}), + ...(chosen && chosenProjId ? { project_id: chosenProjId } : {}), ...(li.unit_price != null ? { adjusted_unit_price: li.unit_price } : {}), }; }), @@ -965,8 +1023,12 @@ export const api = { // دسته‌ی rogue دیگر هرگز ساخته نمی‌شود — فقط تطبیق با ۸ دسته‌ی موجود؛ // در صورت نبود تطبیق، خطا نمی‌دهیم و category_id را null می‌فرستیم. const categoryId = m.category ? lookup.categoryNameToId.get(norm(m.category)) : undefined; - // واحد مرجع — اگر تعریف نشده، بساز (بک‌اند canonical_unit_id الزامی دارد) - const unitName = m.base_unit || "عدد"; + // واحد مرجع — بدون مقدار پیش‌فرض؛ متریال بدون واحدِ صریح ساخته نمی‌شود + // (بک‌اند canonical_unit_id را الزامی می‌داند) + const unitName = (m.base_unit || "").trim(); + if (!unitName) { + throw new Error("واحد مرجع متریال مشخص نشده است — واحد را وارد کنید"); + } const unitId = await unitIdByNameOrCreate(lookup, unitName); // برند — اگر تعریف نشده، در جدول brands ساخته و به متریال متصل می‌شود const brandId = await brandIdByNameOrCreate(lookup, m.brand); @@ -995,6 +1057,10 @@ export const api = { tempIdToRealId.set(m.id, raw.id); return adaptMaterial(raw, lookup); }, + // تغییر واحد مرجع متریال (صفحه‌ی ضرایب تبدیل) + updateCanonicalUnit: async (id: string, canonical_unit_id: number): Promise => { + await patchData(`/materials/${id}`, { canonical_unit_id }); + }, }, projects: { @@ -1066,6 +1132,52 @@ export const api = { }, }, + // ---------- واحدهای هر متریال (material_units — ضریب تبدیل نسبت به واحد مرجع) ---------- + materialUnits: { + list: async (materialId: string): Promise => { + const data = await getData( + `/materials/${materialId}/units?page_size=100&expand=unit,brand_material`, + ); + return data.map(adaptMaterialUnit); + }, + create: async ( + materialId: string, + body: { + brand_material_id?: string | null; + unit_id: number; + conversion_factor: number; + is_default?: boolean; + }, + ): Promise => { + const raw = await postData(`/materials/${materialId}/units`, body); + return adaptMaterialUnit(raw); + }, + update: async ( + materialId: string, + muId: string, + body: { conversion_factor?: number; is_default?: boolean }, + ): Promise => { + const raw = await patchData(`/materials/${materialId}/units/${muId}`, body); + return adaptMaterialUnit(raw); + }, + remove: async (materialId: string, muId: string): Promise => { + await deleteData(`/materials/${materialId}/units/${muId}`); + }, + }, + + // ---------- اتصال برند↔متریال (برای قید برند در ضریب‌های تبدیل) ---------- + brandMaterials: { + listByMaterial: async (materialId: string): Promise => { + const data = await getData( + `/brand-materials?material_id=${materialId}&page_size=100&expand=brand`, + ); + return data.map(adaptBrandLink); + }, + link: async (materialId: string, brandId: string): Promise => { + await postData("/brand-materials", { brand_id: brandId, material_id: materialId }); + }, + }, + // ---------- پیوست‌ها (invoice / proforma) — بایت‌ها در MinIO ---------- files: { upload: (quoteId: string, kind: FileKind, file: File): Promise => @@ -1146,7 +1258,7 @@ async function buildChildrenPayload( project_ids: [], })); for (let i = 0; i < lis.length; i++) { - lis[i].unit_id = await unitIdByNameOrCreate(lookup, lineItems[i].unit || "عدد"); + lis[i].unit_id = await unitIdByNameOrCreate(lookup, lineItems[i].unit); for (const p of lineItems[i].projects || []) { const pid = looksLikeUuid(p.id) ? p.id : await projectIdByNameOrCreate(p.name); if (pid) lis[i].project_ids.push(pid); @@ -1175,20 +1287,23 @@ async function buildChildrenPayload( return { line_items: lis, termsheets: tss, item_termsheets: itemTerms }; } -// واحد را پیدا می‌کند؛ اگر وجود نداشت، در بک‌اند می‌سازد و id برمی‌گرداند +// واحد را پیدا می‌کند؛ اگر وجود نداشت (با نامِ دقیقِ تایپ‌شده‌ی کاربر)، در +// بک‌اند می‌سازد و id برمی‌گرداند. هیچ واحد پیش‌فرضی تزریق نمی‌شود — نام +// خالی/بی‌معنی یعنی خطای صریح. async function unitIdByNameOrCreate(lookup: Lookup, name: string): Promise { - if (!name) throw new Error("واحد قلم مشخص نشده است"); - const existing = lookup.unitNameToId.get(norm(name)); + const key = norm(name); + if (!key) throw new Error("واحد قلم مشخص نشده است — واحد را وارد کنید"); + const existing = lookup.unitNameToId.get(key); if (existing != null) return existing; - // واحد جدید — ساخت در دیتابیس (code = نام نرمال‌شده) + // واحد جدید — ساخت در دیتابیس (code = نام نرمال‌شده‌ی همان واحد تایپ‌شده) const raw = await postData("/units", { - code: norm(name) || "unit", - name_fa: name, + code: key, + name_fa: name.trim(), dimension: null, si_factor: 1, is_base: false, }); - lookup.unitNameToId.set(norm(name), raw.id); + lookup.unitNameToId.set(key, raw.id); return raw.id; } @@ -1489,14 +1604,17 @@ export const apiV26 = { // ساخت موجودیت‌های کاتالوگ سراسری از داخل پنل انبار (موجودی اولیه): // متریال/برند/واحد جدید بدون وابستگی به دفتر تدارکات ساخته می‌شوند. catalog: { - createUnit: async (name: string): Promise<{ id: number; code: string; name_fa: string | null }> => - postData("/units", { - code: norm(name) || "unit", - name_fa: name, + createUnit: async (name: string): Promise<{ id: number; code: string; name_fa: string | null }> => { + const key = norm(name); + if (!key) throw new Error("نام واحد معتبر نیست"); + return postData("/units", { + code: key, + name_fa: name.trim(), dimension: null, si_factor: 1, is_base: false, - }), + }); + }, createBrand: async (name: string): Promise<{ id: string; name: string }> => postData("/brands", { name, country: null }), createMaterial: async (opts: {