Files
ERP/frontend/src/pages/Inventory/useInventoryData.ts
T
ZCode b469238153 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.
2026-09-20 19:44:23 +00:00

118 lines
4.1 KiB
TypeScript

// ============================================================
// Inventory data hook — loads all dash data (scoped by role/project)
// ============================================================
import { useCallback, useEffect, useMemo, useState } from "react";
import { apiV26 } from "../../lib/api";
import { useAuthStore } from "../../store/auth";
import { useDataChanged } from "../../lib/crossTab";
import type { InventoryRow, InventoryTransaction, Warehouse } from "../../lib/types";
export interface LookupData {
units: { id: number; code: string; name_fa: string | null }[];
brands: { id: string; name: string }[];
materials: { id: string; name: string; canonical_unit: { id: number; code: string; name_fa: string | null } | null }[];
projects: { id: string; name: string; pm_user_id: string | null; pm_name?: string | null }[];
}
export interface InventoryData {
warehouses: Warehouse[];
inventory: InventoryRow[];
transactions: InventoryTransaction[];
lookup: LookupData;
loading: boolean;
refresh: () => Promise<void>;
}
export function useInventoryData(): InventoryData {
const user = useAuthStore((s) => s.user);
const isPM = user?.role === "pm";
const projectId = isPM ? user.project_id || undefined : undefined;
const [warehouses, setWarehouses] = useState<Warehouse[]>([]);
const [inventory, setInventory] = useState<InventoryRow[]>([]);
const [transactions, setTransactions] = useState<InventoryTransaction[]>([]);
const [lookup, setLookup] = useState<LookupData>({
units: [], brands: [], materials: [], projects: [],
});
const [loading, setLoading] = useState(true);
const load = useCallback(async () => {
try {
const [units, brands, mats, projs, whs, inv, txs] = await Promise.all([
apiV26.lookup.units(),
apiV26.lookup.brands(),
apiV26.lookup.materials(),
apiV26.lookup.projects(),
apiV26.warehouses.list(projectId),
apiV26.inventory.list({ project_id: projectId }),
apiV26.inventory.transactions({ project_id: projectId }),
]);
setLookup({ units, brands, materials: mats, projects: projs });
setWarehouses(whs);
setInventory(inv);
setTransactions(txs);
} catch {
// پروژه‌ی فعال ممکن است همین حالا باطل شده باشد — گیتِ پروژه در سطح مسیر
// (RequirePmProjects) پروژه‌ی جایگزین/صفحه‌ی خالی را نشان می‌دهد.
} finally {
setLoading(false);
}
}, [projectId]);
useEffect(() => {
setLoading(true);
void load();
}, [load]);
// تب‌های دیگر (ثبت تراکنش/رسید در تب دوم) → بازخوانی خودکار همین صفحه
useDataChanged(
useCallback(() => {
void load();
}, [load]),
["quotes", "inventory"],
);
const scopedWarehouses = useMemo(
() =>
user?.role === "admin" || user?.role === "procurement_manager"
? warehouses
: warehouses.filter((w) => String(w.project_id) === String(user?.project_id || "")),
[warehouses, user?.role, user?.project_id],
);
return {
warehouses: scopedWarehouses,
inventory,
transactions,
lookup,
loading,
refresh: load,
};
}
export function unitName(units: LookupData["units"], id: number | null | undefined): string {
const u = units.find((x) => x.id === id);
return u ? u.name_fa || u.code : "—";
}
export function brandName(brands: LookupData["brands"], id: string | null | undefined): string {
const b = brands.find((x) => x.id === id);
return b ? b.name : "—";
}
export function materialName(materials: LookupData["materials"], id: string): string {
const m = materials.find((x) => x.id === id);
return m ? m.name : "—";
}
export function projName(projects: LookupData["projects"], id: string | null | undefined): string {
const p = projects.find((x) => x.id === id);
return p ? p.name : "—";
}
export function whName(warehouses: Warehouse[], id: string | null | undefined): string {
const w = warehouses.find((x) => x.id === id);
return w ? w.name : "—";
}