fix(api): no hardcoded unit defaults; material_units/brand/catalog APIs; accept sends decision project
- Remove all || «عدد» / || "unit" fallbacks: materials/line items without an explicit unit now fail with a clear Persian error instead of silently creating an «عدد» unit row (POST /units happens only for typed names). - unitIdByNameOrCreate/createUnit guard empty names; code = normalized typed name only. - Add api.materialUnits (list/create/update/remove with brand_material_id), api.brandMaterials (list by material + link), materials.updateCanonicalUnit, apiV26.catalog.* unit/brand/material creation helpers (shared usage). - Accept payload: each chosen item_decision now carries the selected single project_id so backend can bind decision.project_id (PM on-the-way segregation).
This commit is contained in:
+135
-17
@@ -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<void> => {
|
||||
await patchData(`/materials/${id}`, { canonical_unit_id });
|
||||
},
|
||||
},
|
||||
|
||||
projects: {
|
||||
@@ -1066,6 +1132,52 @@ export const api = {
|
||||
},
|
||||
},
|
||||
|
||||
// ---------- واحدهای هر متریال (material_units — ضریب تبدیل نسبت به واحد مرجع) ----------
|
||||
materialUnits: {
|
||||
list: async (materialId: string): Promise<MaterialUnitRow[]> => {
|
||||
const data = await getData<RawMaterialUnit[]>(
|
||||
`/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<MaterialUnitRow> => {
|
||||
const raw = await postData<RawMaterialUnit>(`/materials/${materialId}/units`, body);
|
||||
return adaptMaterialUnit(raw);
|
||||
},
|
||||
update: async (
|
||||
materialId: string,
|
||||
muId: string,
|
||||
body: { conversion_factor?: number; is_default?: boolean },
|
||||
): Promise<MaterialUnitRow> => {
|
||||
const raw = await patchData<RawMaterialUnit>(`/materials/${materialId}/units/${muId}`, body);
|
||||
return adaptMaterialUnit(raw);
|
||||
},
|
||||
remove: async (materialId: string, muId: string): Promise<void> => {
|
||||
await deleteData(`/materials/${materialId}/units/${muId}`);
|
||||
},
|
||||
},
|
||||
|
||||
// ---------- اتصال برند↔متریال (برای قید برند در ضریبهای تبدیل) ----------
|
||||
brandMaterials: {
|
||||
listByMaterial: async (materialId: string): Promise<BrandLinkRow[]> => {
|
||||
const data = await getData<RawBrandLink[]>(
|
||||
`/brand-materials?material_id=${materialId}&page_size=100&expand=brand`,
|
||||
);
|
||||
return data.map(adaptBrandLink);
|
||||
},
|
||||
link: async (materialId: string, brandId: string): Promise<void> => {
|
||||
await postData("/brand-materials", { brand_id: brandId, material_id: materialId });
|
||||
},
|
||||
},
|
||||
|
||||
// ---------- پیوستها (invoice / proforma) — بایتها در MinIO ----------
|
||||
files: {
|
||||
upload: (quoteId: string, kind: FileKind, file: File): Promise<Attachment> =>
|
||||
@@ -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<number> {
|
||||
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<RawUnit>("/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: {
|
||||
|
||||
Reference in New Issue
Block a user