refactor(catalog): reduce the category taxonomy to a supplier label
Categories are kept only as the label each supplier carries (suppliers.category_id + the material_categories list it points at). Everything that treated category as a shared key across materials, inventory, quotes and unit conversions is removed — including two features that were never populated (0 rows on every volume). Removed (backend): - routers category_attributes, category_ref_units, unit_conversions + registration - ?category_id= filters on /materials, /inventory and /quotes (the /suppliers one stays — it filters suppliers by their own label) - GET /inventory/materials/options: required a category and had no caller - the material-payload category derivation (expand_service.category_by_material, the inventory serializer lookup) and the material category on /pm/on-the-way; the wire keys stay, now always null, so the payload shape is unchanged - startup DDL: DROP the empty category_attributes / category_reference_units / unit_conversions tables (idempotent) and their CREATE statements; seed.py truncate list updated Removed (frontend): - the category-keyed conversion path: refUnitForCategory / findConversion / refConverted, the categoryUnits + unitConversions store slices, api clients, hooks and the "≈ converted quantity" line on quote detail and project pages (it needed rows in those tables to render, and there were none) - material.supplier_category and its read sites (the edit modal grew one yesterday to restore labels; the label lives on the supplier now) - the inventory page's category chips, the row filter and the «دسته» column, plus the lookup's categories fetch and catName helper Kept: supplier label + its list and picker, /suppliers?category_id=, the global standard_conversions feature and the units page (material_units conversions). Verified against a restore of the production dump: startup DDL drops exactly the three tables, suppliers keep their label (3/3), material_categories still serves its 8 rows, the removed endpoints 404, and materials/inventory/quotes/units/ standard-conversions all return 200 with quote line items intact.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
// منطق بیزنس مشترک پیشنهادها — پورت از app.js
|
||||
// ============================================================
|
||||
|
||||
import type { LineItem, Material, Quote, StandardConversion, Store, Supplier, UnitConversion } from "./types";
|
||||
import type { LineItem, Material, Quote, Store, Supplier } from "./types";
|
||||
import { normalize } from "./normalize";
|
||||
import { groupCommas } from "./numutil";
|
||||
import { fmtDate } from "./formatters";
|
||||
@@ -17,7 +17,6 @@ export function materialById(materials: Material[], id: string): Material {
|
||||
id: "",
|
||||
name: "",
|
||||
topic: "",
|
||||
supplier_category: "",
|
||||
brand: "",
|
||||
specs: [],
|
||||
base_unit: "",
|
||||
@@ -102,15 +101,14 @@ export function materialLabel(m: Pick<Material, "name" | "brand" | "specs"> | nu
|
||||
// نام پایهی متریال از نامِ ذخیرهشده.
|
||||
// متریالهای جدید فقط نامِ خودِ متریال را در name دارند (برند در brand و
|
||||
// مشخصهها در attributes ذخیره میشوند) و دستنخورده برمیگردند. رکوردهای قدیمی
|
||||
// name را به شکل «برند + دسته + مشخصه» ذخیره کردهاند؛ آن قطعات جدا میشوند تا
|
||||
// نامِ پایه باقی بماند. دستهی همان رکوردهای قدیمی همان دستهی نخستین
|
||||
// تأمینکننده است (supplier_category).
|
||||
export function materialBaseName(name: string, brand: string, specs: string[], category = ""): string {
|
||||
// name را به شکل «برند + مشخصه» ذخیره کردهاند؛ آن قطعات جدا میشوند تا نامِ
|
||||
// پایه باقی بماند.
|
||||
export function materialBaseName(name: string, brand: string, specs: string[]): string {
|
||||
const fallback = (name || "").trim();
|
||||
if (!fallback) return "";
|
||||
let base = fallback;
|
||||
// بلندترین قطعه اول حذف میشود تا حذفهای جزئی، رشته را ناقص نکنند
|
||||
const parts = [brand, category, ...specs]
|
||||
const parts = [brand, ...specs]
|
||||
.map((p) => (p || "").trim())
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => b.length - a.length);
|
||||
@@ -151,7 +149,7 @@ export function matchMaterial(
|
||||
materials.find(
|
||||
(m) =>
|
||||
base &&
|
||||
normalize(materialBaseName(m.name, m.brand, m.specs, m.supplier_category)) === base &&
|
||||
normalize(materialBaseName(m.name, m.brand, m.specs)) === base &&
|
||||
sameIdentity(m),
|
||||
)
|
||||
);
|
||||
@@ -216,57 +214,6 @@ export function itemProjects(q: Quote, li: LineItem): string[] {
|
||||
return (li.projects || []).map((p) => (typeof p === "string" ? p : p.name)).filter(Boolean);
|
||||
}
|
||||
|
||||
// ---------- واحد مرجع و ضریب تبدیل ----------
|
||||
|
||||
// واحد مرجع یک دسته
|
||||
export function refUnitForCategory(store: Pick<Store, "categoryUnits">, category: string): string {
|
||||
const cu = (store.categoryUnits || []).find((c) => normalize(c.category) === normalize(category));
|
||||
return cu ? cu.ref_unit : "";
|
||||
}
|
||||
|
||||
// ضریب تبدیل با اولویت: دستهای (برند مشخص > همه برندها) > تبدیل ثابت جهانی
|
||||
export function findConversion(
|
||||
store: Pick<Store, "unitConversions" | "standardConversions">,
|
||||
category: string,
|
||||
brand: string,
|
||||
fromUnit: string,
|
||||
toUnit: string,
|
||||
): UnitConversion | StandardConversion | null {
|
||||
const list = (store.unitConversions || []).filter(
|
||||
(c) =>
|
||||
normalize(c.category) === normalize(category) &&
|
||||
normalize(c.from_unit) === normalize(fromUnit) &&
|
||||
normalize(c.to_unit) === normalize(toUnit) &&
|
||||
(!c.brand || normalize(c.brand) === normalize(brand)),
|
||||
);
|
||||
list.sort((a, b) => (b.brand ? 1 : 0) - (a.brand ? 1 : 0));
|
||||
if (list[0]) return list[0];
|
||||
return (
|
||||
(store.standardConversions || []).find(
|
||||
(c) => normalize(c.from_unit) === normalize(fromUnit) && normalize(c.to_unit) === normalize(toUnit),
|
||||
) || null
|
||||
);
|
||||
}
|
||||
|
||||
// تبدیل یک قلم به واحد مرجع دسته؛ null وقتی ضریبی نیست یا واحد از قبل مرجع است
|
||||
export function refConverted(
|
||||
store: Pick<Store, "categoryUnits" | "unitConversions" | "standardConversions" | "materials">,
|
||||
li: LineItem,
|
||||
): { qty: number; unit: string; perRef: number | null } | null {
|
||||
const m = materialById(store.materials, li.material_id);
|
||||
// categoryUnits/unitConversions با «نام دسته» کلید خوردهاند؛ دستهی متریال
|
||||
// همان دستهی نخستین تأمینکننده است (v2.7). متریال بدون تأمینکننده دسته
|
||||
// ندارد و تبدیلش انجام نمیشود.
|
||||
const ref = refUnitForCategory(store, m.supplier_category);
|
||||
if (!ref || !li.unit || normalize(ref) === normalize(li.unit)) return null;
|
||||
const conv = findConversion(store, m.supplier_category, m.brand, li.unit, ref);
|
||||
if (!conv) return null;
|
||||
const qty = (li.quantity || 0) * conv.factor;
|
||||
const total = li.total_price != null ? li.total_price : (li.unit_price || 0) * (li.quantity || 0);
|
||||
const perRef = qty > 0 ? Math.round(total / qty) : null;
|
||||
return { qty, unit: ref, perRef };
|
||||
}
|
||||
|
||||
// واحدهای ناشناختهی استفادهشده در یک quote → لینک به داشبورد واحدها
|
||||
// واحدها دیگر hardcode نیستند — همه از بکاند میآیند؛ هشداری نمایش داده نمیشود
|
||||
export function unitWarning(_store: Store, _q: Quote): { unit: string; cat: string }[] {
|
||||
@@ -364,7 +311,7 @@ export function filterQuotes(
|
||||
if (q) {
|
||||
const hay =
|
||||
`${supObj.name} ${quote.project || ""} ` +
|
||||
matInfo.map((m) => `${m.supplier_category} ${m.brand} ${materialSpecs(m).join(" ")}`).join(" ");
|
||||
matInfo.map((m) => `${m.brand} ${materialSpecs(m).join(" ")}`).join(" ");
|
||||
if (!hay.toLowerCase().includes(q)) return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user