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;
|
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 دانلود میآیند)
|
// فایل پیوست — پاسخ بکاند (metadata؛ بایتها از endpoint دانلود میآیند)
|
||||||
interface RawAttachment {
|
interface RawAttachment {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -703,10 +754,11 @@ export const api = {
|
|||||||
quantity: li.quantity ?? 1,
|
quantity: li.quantity ?? 1,
|
||||||
project_ids: [],
|
project_ids: [],
|
||||||
}));
|
}));
|
||||||
// واحدهایی که در بکاند نیستند را بساز (issue: واحد جدید) — واحد خالی = «عدد»
|
// واحدِ تایپشدهی کاربر در بکاند نیست → همان موقع ساخته میشود (بدون هیچ
|
||||||
|
// مقدار پیشفرض/واحد جادویی؛ واحد خالی یعنی خطای صریح «واحد مشخص نشده»)
|
||||||
// پروژههای هر قلم هم به id واقعی ترجمه میشوند (در صورت نبودن ساخته میشوند)
|
// پروژههای هر قلم هم به id واقعی ترجمه میشوند (در صورت نبودن ساخته میشوند)
|
||||||
for (let i = 0; i < lineItems.length; i++) {
|
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 || []) {
|
for (const p of q.line_items[i].projects || []) {
|
||||||
const pid = looksLikeUuid(p.id) ? p.id : await projectIdByNameOrCreate(p.name);
|
const pid = looksLikeUuid(p.id) ? p.id : await projectIdByNameOrCreate(p.name);
|
||||||
if (pid) lineItems[i].project_ids.push(pid);
|
if (pid) lineItems[i].project_ids.push(pid);
|
||||||
@@ -822,10 +874,16 @@ export const api = {
|
|||||||
item_decisions: (patch.line_items || []).map((li) => {
|
item_decisions: (patch.line_items || []).map((li) => {
|
||||||
const idx = li.accepted_ts_id ? acceptedTs.findIndex((t) => t.id === li.accepted_ts_id) : -1;
|
const idx = li.accepted_ts_id ? acceptedTs.findIndex((t) => t.id === li.accepted_ts_id) : -1;
|
||||||
const chosen = !li.rejected && idx >= 0;
|
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 {
|
return {
|
||||||
line_item_id: li.id,
|
line_item_id: li.id,
|
||||||
status: chosen ? "chosen" : "rejected",
|
status: chosen ? "chosen" : "rejected",
|
||||||
...(chosen ? { accepted_termsheet_index: idx } : {}),
|
...(chosen ? { accepted_termsheet_index: idx } : {}),
|
||||||
|
...(chosen && chosenProjId ? { project_id: chosenProjId } : {}),
|
||||||
...(li.unit_price != null ? { adjusted_unit_price: li.unit_price } : {}),
|
...(li.unit_price != null ? { adjusted_unit_price: li.unit_price } : {}),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -965,8 +1023,12 @@ export const api = {
|
|||||||
// دستهی rogue دیگر هرگز ساخته نمیشود — فقط تطبیق با ۸ دستهی موجود؛
|
// دستهی rogue دیگر هرگز ساخته نمیشود — فقط تطبیق با ۸ دستهی موجود؛
|
||||||
// در صورت نبود تطبیق، خطا نمیدهیم و category_id را null میفرستیم.
|
// در صورت نبود تطبیق، خطا نمیدهیم و category_id را null میفرستیم.
|
||||||
const categoryId = m.category ? lookup.categoryNameToId.get(norm(m.category)) : undefined;
|
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);
|
const unitId = await unitIdByNameOrCreate(lookup, unitName);
|
||||||
// برند — اگر تعریف نشده، در جدول brands ساخته و به متریال متصل میشود
|
// برند — اگر تعریف نشده، در جدول brands ساخته و به متریال متصل میشود
|
||||||
const brandId = await brandIdByNameOrCreate(lookup, m.brand);
|
const brandId = await brandIdByNameOrCreate(lookup, m.brand);
|
||||||
@@ -995,6 +1057,10 @@ export const api = {
|
|||||||
tempIdToRealId.set(m.id, raw.id);
|
tempIdToRealId.set(m.id, raw.id);
|
||||||
return adaptMaterial(raw, lookup);
|
return adaptMaterial(raw, lookup);
|
||||||
},
|
},
|
||||||
|
// تغییر واحد مرجع متریال (صفحهی ضرایب تبدیل)
|
||||||
|
updateCanonicalUnit: async (id: string, canonical_unit_id: number): Promise<void> => {
|
||||||
|
await patchData(`/materials/${id}`, { canonical_unit_id });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
projects: {
|
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 ----------
|
// ---------- پیوستها (invoice / proforma) — بایتها در MinIO ----------
|
||||||
files: {
|
files: {
|
||||||
upload: (quoteId: string, kind: FileKind, file: File): Promise<Attachment> =>
|
upload: (quoteId: string, kind: FileKind, file: File): Promise<Attachment> =>
|
||||||
@@ -1146,7 +1258,7 @@ async function buildChildrenPayload(
|
|||||||
project_ids: [],
|
project_ids: [],
|
||||||
}));
|
}));
|
||||||
for (let i = 0; i < lis.length; i++) {
|
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 || []) {
|
for (const p of lineItems[i].projects || []) {
|
||||||
const pid = looksLikeUuid(p.id) ? p.id : await projectIdByNameOrCreate(p.name);
|
const pid = looksLikeUuid(p.id) ? p.id : await projectIdByNameOrCreate(p.name);
|
||||||
if (pid) lis[i].project_ids.push(pid);
|
if (pid) lis[i].project_ids.push(pid);
|
||||||
@@ -1175,20 +1287,23 @@ async function buildChildrenPayload(
|
|||||||
return { line_items: lis, termsheets: tss, item_termsheets: itemTerms };
|
return { line_items: lis, termsheets: tss, item_termsheets: itemTerms };
|
||||||
}
|
}
|
||||||
|
|
||||||
// واحد را پیدا میکند؛ اگر وجود نداشت، در بکاند میسازد و id برمیگرداند
|
// واحد را پیدا میکند؛ اگر وجود نداشت (با نامِ دقیقِ تایپشدهی کاربر)، در
|
||||||
|
// بکاند میسازد و id برمیگرداند. هیچ واحد پیشفرضی تزریق نمیشود — نام
|
||||||
|
// خالی/بیمعنی یعنی خطای صریح.
|
||||||
async function unitIdByNameOrCreate(lookup: Lookup, name: string): Promise<number> {
|
async function unitIdByNameOrCreate(lookup: Lookup, name: string): Promise<number> {
|
||||||
if (!name) throw new Error("واحد قلم مشخص نشده است");
|
const key = norm(name);
|
||||||
const existing = lookup.unitNameToId.get(norm(name));
|
if (!key) throw new Error("واحد قلم مشخص نشده است — واحد را وارد کنید");
|
||||||
|
const existing = lookup.unitNameToId.get(key);
|
||||||
if (existing != null) return existing;
|
if (existing != null) return existing;
|
||||||
// واحد جدید — ساخت در دیتابیس (code = نام نرمالشده)
|
// واحد جدید — ساخت در دیتابیس (code = نام نرمالشدهی همان واحد تایپشده)
|
||||||
const raw = await postData<RawUnit>("/units", {
|
const raw = await postData<RawUnit>("/units", {
|
||||||
code: norm(name) || "unit",
|
code: key,
|
||||||
name_fa: name,
|
name_fa: name.trim(),
|
||||||
dimension: null,
|
dimension: null,
|
||||||
si_factor: 1,
|
si_factor: 1,
|
||||||
is_base: false,
|
is_base: false,
|
||||||
});
|
});
|
||||||
lookup.unitNameToId.set(norm(name), raw.id);
|
lookup.unitNameToId.set(key, raw.id);
|
||||||
return raw.id;
|
return raw.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1489,14 +1604,17 @@ export const apiV26 = {
|
|||||||
// ساخت موجودیتهای کاتالوگ سراسری از داخل پنل انبار (موجودی اولیه):
|
// ساخت موجودیتهای کاتالوگ سراسری از داخل پنل انبار (موجودی اولیه):
|
||||||
// متریال/برند/واحد جدید بدون وابستگی به دفتر تدارکات ساخته میشوند.
|
// متریال/برند/واحد جدید بدون وابستگی به دفتر تدارکات ساخته میشوند.
|
||||||
catalog: {
|
catalog: {
|
||||||
createUnit: async (name: string): Promise<{ id: number; code: string; name_fa: string | null }> =>
|
createUnit: async (name: string): Promise<{ id: number; code: string; name_fa: string | null }> => {
|
||||||
postData("/units", {
|
const key = norm(name);
|
||||||
code: norm(name) || "unit",
|
if (!key) throw new Error("نام واحد معتبر نیست");
|
||||||
name_fa: name,
|
return postData("/units", {
|
||||||
|
code: key,
|
||||||
|
name_fa: name.trim(),
|
||||||
dimension: null,
|
dimension: null,
|
||||||
si_factor: 1,
|
si_factor: 1,
|
||||||
is_base: false,
|
is_base: false,
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
createBrand: async (name: string): Promise<{ id: string; name: string }> =>
|
createBrand: async (name: string): Promise<{ id: string; name: string }> =>
|
||||||
postData("/brands", { name, country: null }),
|
postData("/brands", { name, country: null }),
|
||||||
createMaterial: async (opts: {
|
createMaterial: async (opts: {
|
||||||
|
|||||||
Reference in New Issue
Block a user