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:
@@ -3,11 +3,4 @@ export { useQuotes, useCreateQuote, useUpdateQuote, useDeleteQuote } from "./use
|
||||
export { useSuppliers, useAddSupplier, useUpdateSupplier, useRemoveSupplier } from "./useSuppliers";
|
||||
export { useMaterials, useAddMaterial } from "./useMaterials";
|
||||
export { useProjects, useAddProject } from "./useProjects";
|
||||
export {
|
||||
useUnits,
|
||||
useSaveRefUnit,
|
||||
useSaveConversion,
|
||||
useDeleteConversion,
|
||||
useSaveStandardConversion,
|
||||
useDeleteStandardConversion,
|
||||
} from "./useUnits";
|
||||
export { useUnits, useSaveStandardConversion, useDeleteStandardConversion } from "./useUnits";
|
||||
|
||||
@@ -1,52 +1,16 @@
|
||||
// ============================================================
|
||||
// hooks واحدها — واحد مرجع، ضرایب دستهای و ثابت
|
||||
// hooks واحدها — تبدیلهای ثابت جهانی (دستهایها حذف شدند)
|
||||
// ============================================================
|
||||
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { CategoryUnit, StandardConversion, UnitConversion } from "../lib/types";
|
||||
import type { StandardConversion } from "../lib/types";
|
||||
import { api } from "../lib/api";
|
||||
import { useAppStore } from "../store";
|
||||
import { useInvalidateStore } from "./useStoreData";
|
||||
|
||||
export function useUnits(): {
|
||||
categoryUnits: CategoryUnit[];
|
||||
unitConversions: UnitConversion[];
|
||||
standardConversions: StandardConversion[];
|
||||
} {
|
||||
const categoryUnits = useAppStore((s) => s.categoryUnits);
|
||||
const unitConversions = useAppStore((s) => s.unitConversions);
|
||||
export function useUnits(): { standardConversions: StandardConversion[] } {
|
||||
const standardConversions = useAppStore((s) => s.standardConversions);
|
||||
return { categoryUnits, unitConversions, standardConversions };
|
||||
}
|
||||
|
||||
export function useSaveRefUnit() {
|
||||
const setCategoryUnit = useAppStore((s) => s.setCategoryUnit);
|
||||
const invalidate = useInvalidateStore();
|
||||
return useMutation({
|
||||
mutationFn: (cu: CategoryUnit) => api.units.saveCategoryUnit(cu),
|
||||
onMutate: (cu) => setCategoryUnit(cu),
|
||||
onSettled: () => invalidate(),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSaveConversion() {
|
||||
const setConversion = useAppStore((s) => s.setConversion);
|
||||
const invalidate = useInvalidateStore();
|
||||
return useMutation({
|
||||
mutationFn: (c: UnitConversion) => api.units.saveConversion(c),
|
||||
onMutate: (c) => setConversion(c),
|
||||
onSettled: () => invalidate(),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteConversion() {
|
||||
const deleteConversion = useAppStore((s) => s.deleteConversion);
|
||||
const invalidate = useInvalidateStore();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => api.units.deleteConversion(id),
|
||||
onMutate: (id) => deleteConversion(id),
|
||||
onSettled: () => invalidate(),
|
||||
});
|
||||
return { standardConversions };
|
||||
}
|
||||
|
||||
export function useSaveStandardConversion() {
|
||||
|
||||
+11
-116
@@ -18,7 +18,6 @@ import { jalaliToGregorian, toAsciiDigits } from "./jalali";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import type {
|
||||
Attachment,
|
||||
CategoryUnit,
|
||||
ConfirmedReceipt,
|
||||
InventoryRow,
|
||||
InventoryTransaction,
|
||||
@@ -30,7 +29,6 @@ import type {
|
||||
Store,
|
||||
Supplier,
|
||||
TransactionType,
|
||||
UnitConversion,
|
||||
Warehouse,
|
||||
} from "./types";
|
||||
|
||||
@@ -252,10 +250,6 @@ async function patchData<T>(path: string, body: unknown): Promise<T> {
|
||||
const res = await request<{ data: T }>(path, { method: "PATCH", body: JSON.stringify(body) });
|
||||
return res.data;
|
||||
}
|
||||
async function putData<T>(path: string, body: unknown): Promise<T> {
|
||||
const res = await request<{ data: T }>(path, { method: "PUT", body: JSON.stringify(body) });
|
||||
return res.data;
|
||||
}
|
||||
async function deleteData<T>(path: string): Promise<T> {
|
||||
const res = await request<{ data: T }>(path, { method: "DELETE" });
|
||||
return res.data;
|
||||
@@ -322,7 +316,6 @@ interface RawSupplier {
|
||||
phone?: string | null;
|
||||
contact_person?: string | null;
|
||||
tax_id?: string | null;
|
||||
categories?: string[] | null; // متریالهای تأمینشده
|
||||
// v2.6: هر تأمینکننده دقیقاً یک دسته دارد (FK) — بکاند این را میفرستد
|
||||
category_id?: string | null;
|
||||
category?: { id: string; slug: string; name: string } | null;
|
||||
@@ -402,21 +395,6 @@ interface RawQuote {
|
||||
line_items?: RawLineItem[] | null;
|
||||
termsheets?: RawTermsheet[] | null;
|
||||
}
|
||||
interface RawCategoryUnit {
|
||||
id: string;
|
||||
category_id: string;
|
||||
ref_unit: string;
|
||||
category?: RawCategory | null;
|
||||
}
|
||||
interface RawUnitConversion {
|
||||
id: string;
|
||||
category_id: string;
|
||||
brand?: string | null;
|
||||
from_unit: string;
|
||||
to_unit: string;
|
||||
factor: number;
|
||||
category?: RawCategory | null;
|
||||
}
|
||||
interface RawStandardConversion {
|
||||
id: string;
|
||||
from_unit: string;
|
||||
@@ -562,9 +540,6 @@ function adaptMaterial(raw: RawMaterial, lookup: Lookup): Material {
|
||||
topic:
|
||||
raw.topic ||
|
||||
(raw.category?.slug ? lookup.categorySlugToName.get(raw.category.slug) || "" : ""),
|
||||
// v2.7: دستهی متریال در بکاند از نخستین تأمینکنندهی متصل میآید
|
||||
// (متریالِ بدون تأمینکننده → خالی)
|
||||
supplier_category: raw.category?.name || "",
|
||||
brand: raw.brands?.[0]?.name || "",
|
||||
// مشخصهها از attribute های متریال خوانده میشوند (EAV)
|
||||
specs: (raw.attributes || [])
|
||||
@@ -645,15 +620,13 @@ function adaptQuote(raw: RawQuote): Quote {
|
||||
}
|
||||
|
||||
function adaptSupplier(raw: RawSupplier, topic: string, categories: string[]): Supplier {
|
||||
// v2.6/v2.7: هر تأمینکننده دقیقاً یک دسته دارد (FK) و بکاند آن را به شکل
|
||||
// category برمیگرداند — همین اولویت اول است. categories قدیمی (ستون حذفشده)
|
||||
// و دستههای استنتاجشده از پیشنهادها فقط پشتیباناند.
|
||||
// هر تأمینکننده دقیقاً یک دسته دارد (FK) و بکاند آن را به شکل category
|
||||
// برمیگرداند — منبع اصلی همین است؛ پارامتر categories فقط پشتیبان است.
|
||||
const own = raw.category?.name ? [raw.category.name] : [];
|
||||
const legacy = Array.isArray(raw.categories) ? raw.categories.filter(Boolean) : [];
|
||||
return {
|
||||
id: raw.id,
|
||||
topic,
|
||||
categories: own.length ? own : legacy.length ? legacy : categories,
|
||||
categories: own.length ? own : categories,
|
||||
name: raw.name,
|
||||
location: raw.location || "",
|
||||
phone: raw.phone || "",
|
||||
@@ -671,54 +644,29 @@ function deriveSupplierTopics(
|
||||
materialsById: Map<string, Material>,
|
||||
): Supplier[] {
|
||||
const topics = new Map<string, Set<string>>();
|
||||
const cats = new Map<string, Set<string>>();
|
||||
for (const q of quotes) {
|
||||
for (const li of q.line_items) {
|
||||
const m = materialsById.get(li.material_id);
|
||||
if (!m) continue;
|
||||
if (!topics.has(q.supplier_id)) topics.set(q.supplier_id, new Set());
|
||||
if (!cats.has(q.supplier_id)) cats.set(q.supplier_id, new Set());
|
||||
if (m.topic) topics.get(q.supplier_id)!.add(m.topic);
|
||||
if (m.supplier_category) cats.get(q.supplier_id)!.add(m.supplier_category);
|
||||
}
|
||||
}
|
||||
return rawSuppliers.map((s) => {
|
||||
const t = topics.get(s.id);
|
||||
const c = cats.get(s.id);
|
||||
const derivedTopic = t && t.size ? [...t][0] : "";
|
||||
const derivedCats = c ? [...c].sort((a, b) => a.localeCompare(b, "fa")) : [];
|
||||
// اگر از quotes چیزی پیدا نشد، registry را چک کن
|
||||
if (!derivedTopic && !derivedCats.length) {
|
||||
if (!derivedTopic) {
|
||||
const reg = supplierTopicRegistry.get(s.id);
|
||||
if (reg) {
|
||||
return adaptSupplier(s, reg.topic, reg.categories);
|
||||
}
|
||||
if (reg) return adaptSupplier(s, reg.topic, reg.categories);
|
||||
}
|
||||
// v2.7: متریالها از دسته جدا شدند، پس استنتاج از اقلام پیشنهاد معمولاً
|
||||
// دستهای نمیدهد و صفحهی تأمینکنندهها بینام میشد. دستهٔ ثبتشده روی
|
||||
// خودِ تأمینکننده (FK بکاند) منبع اصلی است؛ نامش با نام تاپیکها یکی است.
|
||||
// دستهٔ ثبتشده روی خودِ تأمینکننده (FK بکاند) منبع اصلی است و نامش با نام
|
||||
// تاپیکها یکی است؛ تاپیک از اقلام پیشنهاد استنتاج میشود.
|
||||
const backendCat = s.category?.name || "";
|
||||
return adaptSupplier(
|
||||
s,
|
||||
derivedTopic || backendCat,
|
||||
derivedCats.length ? derivedCats : backendCat ? [backendCat] : [],
|
||||
);
|
||||
return adaptSupplier(s, derivedTopic || backendCat, backendCat ? [backendCat] : []);
|
||||
});
|
||||
}
|
||||
|
||||
function adaptCategoryUnit(raw: RawCategoryUnit): CategoryUnit {
|
||||
return { id: raw.id, category: raw.category?.name || "", ref_unit: raw.ref_unit };
|
||||
}
|
||||
function adaptUnitConversion(raw: RawUnitConversion, lookup: Lookup): UnitConversion {
|
||||
return {
|
||||
id: raw.id,
|
||||
category: raw.category?.name || lookup.categoryIdToName.get(raw.category_id) || "",
|
||||
brand: raw.brand || "",
|
||||
from_unit: raw.from_unit,
|
||||
to_unit: raw.to_unit,
|
||||
factor: raw.factor,
|
||||
};
|
||||
}
|
||||
function adaptStandardConversion(raw: RawStandardConversion): StandardConversion {
|
||||
return { id: raw.id, from_unit: raw.from_unit, to_unit: raw.to_unit, factor: raw.factor };
|
||||
}
|
||||
@@ -836,16 +784,14 @@ function registerSupplierTopic(id: string, topic: string, categories: string[]):
|
||||
export const api = {
|
||||
// ---------- کل store (یک بار در ریشه) ----------
|
||||
async listAll(): Promise<Store> {
|
||||
const [quotesRes, suppliersRes, materialsRes, projectsRes, unitsRes, categoriesRes, cruRes, ucRes, scuRes] =
|
||||
const [quotesRes, suppliersRes, materialsRes, projectsRes, unitsRes, categoriesRes, scuRes] =
|
||||
await Promise.all([
|
||||
getData<RawQuote[]>(`/quotes?expand=supplier,project,events,line_items,termsheets,decision,receipt&page_size=${PAGE_SIZE}`),
|
||||
getData<RawSupplier[]>(`/suppliers?page_size=${PAGE_SIZE}`),
|
||||
getData<RawMaterial[]>(`/materials?expand=category,brands,canonical_unit,attributes&page_size=${PAGE_SIZE}`),
|
||||
getData<RawMaterial[]>(`/materials?expand=brands,canonical_unit,attributes&page_size=${PAGE_SIZE}`),
|
||||
getData<RawProject[]>(`/projects?page_size=${PAGE_SIZE}`),
|
||||
getData<RawUnit[]>(`/units?page_size=${PAGE_SIZE}`),
|
||||
getData<RawMaterialCategory[]>(`/material-categories?page_size=${PAGE_SIZE}`),
|
||||
getData<RawCategoryUnit[]>(`/category-reference-units?expand=category&page_size=${PAGE_SIZE}`),
|
||||
getData<RawUnitConversion[]>(`/unit-conversions?expand=category&page_size=${PAGE_SIZE}`),
|
||||
getData<RawStandardConversion[]>(`/standard-conversions?page_size=${PAGE_SIZE}`),
|
||||
]);
|
||||
|
||||
@@ -860,8 +806,6 @@ export const api = {
|
||||
materials,
|
||||
projects: projectsRes.map((p) => ({ id: p.id, name: p.name })),
|
||||
quotes,
|
||||
categoryUnits: cruRes.map(adaptCategoryUnit),
|
||||
unitConversions: ucRes.map((uc) => adaptUnitConversion(uc, lookup)),
|
||||
standardConversions: scuRes.map(adaptStandardConversion),
|
||||
topics: buildTopics(categoriesRes),
|
||||
};
|
||||
@@ -1136,7 +1080,7 @@ export const api = {
|
||||
|
||||
materials: {
|
||||
list: async (): Promise<Material[]> => {
|
||||
const raw = await getData<RawMaterial[]>(`/materials?expand=category,brands,canonical_unit,attributes&page_size=${PAGE_SIZE}`);
|
||||
const raw = await getData<RawMaterial[]>(`/materials?expand=brands,canonical_unit,attributes&page_size=${PAGE_SIZE}`);
|
||||
const units = await getData<RawUnit[]>(`/units?page_size=${PAGE_SIZE}`);
|
||||
const categories = await getData<RawMaterialCategory[]>(`/material-categories?page_size=${PAGE_SIZE}`);
|
||||
return raw.map((m) => adaptMaterial(m, buildLookup(units, categories)));
|
||||
@@ -1196,46 +1140,6 @@ export const api = {
|
||||
},
|
||||
|
||||
units: {
|
||||
getCategoryUnits: async (): Promise<CategoryUnit[]> => {
|
||||
const raw = await getData<RawCategoryUnit[]>(`/category-reference-units?expand=category&page_size=${PAGE_SIZE}`);
|
||||
return raw.map(adaptCategoryUnit);
|
||||
},
|
||||
saveCategoryUnit: async (cu: CategoryUnit): Promise<void> => {
|
||||
const lookup = await fetchLookup();
|
||||
const categoryId = lookup.categoryNameToId.get(norm(cu.category));
|
||||
if (!categoryId) throw new Error(`دستهبندی «${cu.category}» یافت نشد`);
|
||||
await putData(`/category-reference-units/${categoryId}`, { ref_unit: cu.ref_unit });
|
||||
},
|
||||
getConversions: async (): Promise<UnitConversion[]> => {
|
||||
const lookup = await fetchLookup();
|
||||
const raw = await getData<RawUnitConversion[]>(`/unit-conversions?expand=category&page_size=${PAGE_SIZE}`);
|
||||
return raw.map((uc) => adaptUnitConversion(uc, lookup));
|
||||
},
|
||||
saveConversion: async (c: UnitConversion): Promise<void> => {
|
||||
const lookup = await fetchLookup();
|
||||
const categoryId = lookup.categoryNameToId.get(norm(c.category));
|
||||
if (!categoryId) throw new Error(`دستهبندی «${c.category}» یافت نشد`);
|
||||
const body = {
|
||||
category_id: categoryId,
|
||||
brand: c.brand || null,
|
||||
from_unit: c.from_unit,
|
||||
to_unit: c.to_unit,
|
||||
factor: c.factor,
|
||||
};
|
||||
if (looksLikeUuid(c.id)) {
|
||||
await patchData(`/unit-conversions/${c.id}`, {
|
||||
brand: c.brand || null,
|
||||
from_unit: c.from_unit,
|
||||
to_unit: c.to_unit,
|
||||
factor: c.factor,
|
||||
});
|
||||
} else {
|
||||
await postData("/unit-conversions", body);
|
||||
}
|
||||
},
|
||||
deleteConversion: async (id: string): Promise<void> => {
|
||||
await deleteData(`/unit-conversions/${id}`);
|
||||
},
|
||||
getStandardConversions: async (): Promise<StandardConversion[]> => {
|
||||
const raw = await getData<RawStandardConversion[]>(`/standard-conversions?page_size=${PAGE_SIZE}`);
|
||||
return raw.map(adaptStandardConversion);
|
||||
@@ -1781,15 +1685,6 @@ export const apiV26 = {
|
||||
types: async (): Promise<{ type: TransactionType; label: string }[]> => {
|
||||
return getData<{ type: TransactionType; label: string }[]>("/inventory/meta/types");
|
||||
},
|
||||
materialOptions: async (category_id: string, warehouse_id?: string): Promise<{ id: string; name: string; brands: { id: string; name: string }[] }[]> => {
|
||||
const params: Record<string, string> = { category_id };
|
||||
if (warehouse_id) params.warehouse_id = warehouse_id;
|
||||
const pid = activeProjectId();
|
||||
if (pid) params.project_id = pid;
|
||||
return getData<{ id: string; name: string; brands: { id: string; name: string }[] }[]>(
|
||||
`/inventory/materials/options?${new URLSearchParams(params).toString()}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
pm: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -22,10 +22,6 @@ export interface Material {
|
||||
id: string; // "m1", "m2", ...
|
||||
name: string; // نامِ خودِ متریال — برند در brand و مشخصهها در specs
|
||||
topic: string;
|
||||
// دستهی نمایشی — از نخستین تأمینکنندهی متصل به متریال میآید (v2.7:
|
||||
// خودِ متریال دیگر دسته ندارد و دسته روی تأمینکننده است). متریال بدون
|
||||
// تأمینکننده = رشتهی خالی. این مقدار کلیدِ تبدیلهای واحد هم هست.
|
||||
supplier_category: string;
|
||||
brand: string; // مثلاً "ذوب آهن"
|
||||
specs: string[]; // آرایهای از مشخصهها (مثلاً ["A2"])
|
||||
base_unit: string; // واحد پایه
|
||||
@@ -100,23 +96,6 @@ export interface Quote {
|
||||
termsheets: Termsheet[];
|
||||
}
|
||||
|
||||
// واحد مرجع هر دسته
|
||||
export interface CategoryUnit {
|
||||
id: string;
|
||||
category: string;
|
||||
ref_unit: string;
|
||||
}
|
||||
|
||||
// ضریب تبدیل (دستهای)
|
||||
export interface UnitConversion {
|
||||
id: string;
|
||||
category: string;
|
||||
brand: string; // خالی = همه برندها
|
||||
from_unit: string;
|
||||
to_unit: string;
|
||||
factor: number; // 1 from_unit = factor * to_unit
|
||||
}
|
||||
|
||||
// ضریب تبدیل استاندارد (جهانی)
|
||||
export interface StandardConversion {
|
||||
id: string;
|
||||
@@ -131,8 +110,6 @@ export interface Store {
|
||||
materials: Material[];
|
||||
projects: Project[];
|
||||
quotes: Quote[];
|
||||
categoryUnits: CategoryUnit[];
|
||||
unitConversions: UnitConversion[];
|
||||
standardConversions: StandardConversion[];
|
||||
topics: Topic[]; // دستهبندیهای ۸گانه از /material-categories
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export function DashboardPage() {
|
||||
const suppliers = useSuppliers();
|
||||
const materials = useMaterials();
|
||||
const projects = useProjects();
|
||||
const { categoryUnits, unitConversions, standardConversions } = useUnits();
|
||||
const { standardConversions } = useUnits();
|
||||
|
||||
const expandedQuoteIds = useAppStore((s) => s.expandedQuoteIds);
|
||||
const toggleExpand = useAppStore((s) => s.toggleExpand);
|
||||
@@ -40,8 +40,8 @@ export function DashboardPage() {
|
||||
const [editId, setEditId] = useState<string | null>(null);
|
||||
|
||||
const store = useMemo(
|
||||
() => ({ quotes, suppliers, materials, projects, categoryUnits, unitConversions, standardConversions, topics }),
|
||||
[quotes, suppliers, materials, projects, categoryUnits, unitConversions, standardConversions, topics],
|
||||
() => ({ quotes, suppliers, materials, projects, standardConversions, topics }),
|
||||
[quotes, suppliers, materials, projects, standardConversions, topics],
|
||||
);
|
||||
|
||||
// فیلتر + مرتبسازی بر اساس زمان ساخت (created_at)
|
||||
|
||||
@@ -308,7 +308,6 @@ export function EditModal({ quoteId, onClose }: EditModalProps) {
|
||||
brand,
|
||||
specs,
|
||||
base_unit: unit,
|
||||
supplier_category: "",
|
||||
};
|
||||
materialsCopy.push(nm);
|
||||
const created = await addMaterial.mutateAsync(nm);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Package, Scale, CreditCard, CalendarDays, Paperclip, Building2, Ban } from "lucide-react";
|
||||
import { Package, CreditCard, CalendarDays, Paperclip, Building2, Ban } from "lucide-react";
|
||||
import type { Attachment, Quote, Store } from "../../lib/types";
|
||||
import { api } from "../../lib/api";
|
||||
import {
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
itemTermsheetIds,
|
||||
lineItemTotal,
|
||||
materialById,
|
||||
refConverted,
|
||||
} from "../../lib/quoteUtils";
|
||||
import { fmtDate, fmtDateTime, fmtNum } from "../../lib/formatters";
|
||||
import { groupCommas } from "../../lib/numutil";
|
||||
@@ -76,7 +75,6 @@ export function QuoteDetail({ store, q }: QuoteDetailProps) {
|
||||
const tsIds = itemTermsheetIds(li).filter((tid) => tsList.some((t) => t.id === tid));
|
||||
const total = lineItemTotal(li);
|
||||
const projs = itemProjects(q, li);
|
||||
const conv = isRejected ? null : refConverted(store, li);
|
||||
|
||||
// رسید جزیی → قلم به دو بخش «رسید شده» و «در راه» شکسته میشود (نمایشی):
|
||||
// مجموع دو بخش همیشه با تعداد/مبلغ اصلی برابر است (عدم تغییر جمع کل پیشنهاد).
|
||||
@@ -140,12 +138,6 @@ export function QuoteDetail({ store, q }: QuoteDetailProps) {
|
||||
{whNames.length > 0 && <span>به انبار: {whNames.join("، ")}</span>}
|
||||
</div>
|
||||
)}
|
||||
{conv && (
|
||||
<div className="fb-conv">
|
||||
<Scale className="me-1 inline h-3 w-3" /> ≈ {groupCommas(conv.qty, true)} {conv.unit}
|
||||
{conv.perRef != null ? ` · ${groupCommas(conv.perRef, true)} تومان/${conv.unit}` : ""}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -220,7 +220,7 @@ export function ChangeInventoryPage() {
|
||||
let m = matByName(matName);
|
||||
if (!m) {
|
||||
const created = await apiV26.catalog.createMaterial({ name: matName, canonical_unit_id: uid, brand_ids: bid ? [bid] : [] });
|
||||
m = { id: created.id, name: created.name, category: null, canonical_unit: null };
|
||||
m = { id: created.id, name: created.name, canonical_unit: null };
|
||||
}
|
||||
const op: Record<string, unknown> = {
|
||||
transaction_type: "PURCHASE", warehouse_id: whId, material_id: m.id, brand_id: bid,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useAuthStore } from "../../store/auth";
|
||||
import { AppSwitcherMenu } from "../../components/AppSwitcherMenu";
|
||||
import { inventorySubheader } from "./panelMenu";
|
||||
import { InventoryTopNav } from "./InventoryTopNav";
|
||||
import { useInventoryData, brandName, catName, materialName, unitName, whName } from "./useInventoryData";
|
||||
import { useInventoryData, brandName, materialName, unitName, whName } from "./useInventoryData";
|
||||
import type { InventoryRow, InventoryTransaction, TransactionType } from "../../lib/types";
|
||||
import { faNum } from "../../lib/formatters";
|
||||
import { isoToJalaliStr } from "../../lib/jalali";
|
||||
@@ -55,8 +55,7 @@ export function InventoryPage() {
|
||||
const isPM = user?.role === "pm";
|
||||
const [params] = useSearchParams();
|
||||
|
||||
const [filters, setFilters] = useState<{ category: string; warehouse: string; material: string; project: string }>({
|
||||
category: "",
|
||||
const [filters, setFilters] = useState<{ warehouse: string; material: string; project: string }>({
|
||||
warehouse: params.get("warehouse") || "",
|
||||
material: "",
|
||||
project: "",
|
||||
@@ -81,7 +80,6 @@ export function InventoryPage() {
|
||||
inventory.forEach((r) => {
|
||||
const whId = r.warehouse?.id;
|
||||
if (!whId || !visibleIds.has(whId)) return;
|
||||
if (filters.category && r.material.category?.id !== filters.category) return;
|
||||
if (filters.warehouse && whId !== filters.warehouse) return;
|
||||
if (filters.material && !r.material.name.includes(filters.material.trim())) return;
|
||||
const gk = `${whId}|${r.material.id}|${specsHash(r.specs)}`;
|
||||
@@ -132,7 +130,7 @@ export function InventoryPage() {
|
||||
.slice()
|
||||
.sort((a, b) => new Date(a.transaction_date).getTime() - new Date(b.transaction_date).getTime());
|
||||
|
||||
const hasFilter = !!(filters.category || filters.warehouse || (isAdmin && filters.project));
|
||||
const hasFilter = !!(filters.warehouse || (isAdmin && filters.project));
|
||||
|
||||
if (loading) return <div className="min-h-screen" />;
|
||||
if (!user || (!isAdmin && !isPM)) {
|
||||
@@ -248,40 +246,6 @@ export function InventoryPage() {
|
||||
{/* فیلترهای پیشرفته — برچسبهای لمسی بدون سلکت و بدون بیرونزدگی */}
|
||||
{showMobileFilters && (
|
||||
<div className="mt-2.5 border-t border-line/60 pt-2.5 flex flex-col gap-2.5">
|
||||
{/* دستهبندیها */}
|
||||
<div>
|
||||
<div className="mb-1.5 text-[11px] font-bold text-muted">دستهبندی متریال:</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFilters((f) => ({ ...f, category: "" }))}
|
||||
className={cn(
|
||||
"rounded-lg px-2.5 py-1 text-[11.5px] transition-colors",
|
||||
!filters.category
|
||||
? "bg-accent text-white font-bold"
|
||||
: "border border-line bg-bg/40 text-ink hover:bg-panel"
|
||||
)}
|
||||
>
|
||||
همه دستهبندیها
|
||||
</button>
|
||||
{lookup.categories.map((c) => (
|
||||
<button
|
||||
key={c.id}
|
||||
type="button"
|
||||
onClick={() => setFilters((f) => ({ ...f, category: f.category === c.id ? "" : c.id }))}
|
||||
className={cn(
|
||||
"rounded-lg px-2.5 py-1 text-[11.5px] transition-colors",
|
||||
filters.category === c.id
|
||||
? "bg-accent text-white font-bold"
|
||||
: "border border-line bg-bg/40 text-ink hover:bg-panel"
|
||||
)}
|
||||
>
|
||||
{c.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* پروژهها (مخصوص ادمین) */}
|
||||
{isAdmin && lookup.projects.length > 0 && (
|
||||
<div>
|
||||
@@ -324,7 +288,7 @@ export function InventoryPage() {
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-red font-medium hover:underline inline-flex items-center gap-1"
|
||||
onClick={() => setFilters({ category: "", warehouse: "", material: "", project: "" })}
|
||||
onClick={() => setFilters({ warehouse: "", material: "", project: "" })}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
پاک کردن فیلترها
|
||||
@@ -347,7 +311,6 @@ export function InventoryPage() {
|
||||
) : (
|
||||
groups.map((g) => {
|
||||
const isExp = expandedKey === g.key;
|
||||
const matCat = lookup.materials.find((m) => m.id === g.matId)?.category;
|
||||
return (
|
||||
<InventoryMobileCard
|
||||
key={g.key}
|
||||
@@ -355,7 +318,6 @@ export function InventoryPage() {
|
||||
isExp={isExp}
|
||||
isAdmin={isAdmin}
|
||||
matName={materialName(lookup.materials, g.matId)}
|
||||
matCatName={matCat ? catName(lookup.categories, matCat.id) : "—"}
|
||||
wh={whName(warehouses, g.whId)}
|
||||
projName={lookup.projects.find((p) => p.id === warehouses.find((w) => w.id === g.whId)?.project_id)?.name || ""}
|
||||
unit={unitName(lookup.units, g.unitId)}
|
||||
@@ -410,7 +372,6 @@ export function InventoryPage() {
|
||||
<tbody>
|
||||
{groups.map((g) => {
|
||||
const isExp = expandedKey === g.key;
|
||||
const matCat = lookup.materials.find((m) => m.id === g.matId)?.category;
|
||||
return (
|
||||
<GroupRow
|
||||
key={g.key}
|
||||
@@ -418,7 +379,6 @@ export function InventoryPage() {
|
||||
isExp={isExp}
|
||||
isAdmin={isAdmin}
|
||||
matName={materialName(lookup.materials, g.matId)}
|
||||
matCatName={matCat ? catName(lookup.categories, matCat.id) : "—"}
|
||||
wh={whName(warehouses, g.whId)}
|
||||
projName={lookup.projects.find((p) => p.id === warehouses.find((w) => w.id === g.whId)?.project_id)?.name || ""}
|
||||
unit={unitName(lookup.units, g.unitId)}
|
||||
@@ -475,7 +435,6 @@ function InventoryMobileCard({
|
||||
isExp,
|
||||
isAdmin,
|
||||
matName,
|
||||
matCatName,
|
||||
wh,
|
||||
projName,
|
||||
unit,
|
||||
@@ -486,7 +445,6 @@ function InventoryMobileCard({
|
||||
isExp: boolean;
|
||||
isAdmin: boolean;
|
||||
matName: string;
|
||||
matCatName: string;
|
||||
wh: string;
|
||||
projName: string;
|
||||
unit: string;
|
||||
@@ -508,11 +466,6 @@ function InventoryMobileCard({
|
||||
<h3 className="text-[13.5px] font-bold leading-snug text-ink break-words">
|
||||
{matName}
|
||||
</h3>
|
||||
{matCatName !== "—" && (
|
||||
<span className="mt-0.5 inline-block text-[11px] text-muted">
|
||||
{matCatName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
@@ -574,12 +527,11 @@ function InventoryMobileCard({
|
||||
);
|
||||
}
|
||||
|
||||
function GroupRow({ g, isExp, isAdmin, matName, matCatName, wh, projName, unit, onToggle, timeline }: {
|
||||
function GroupRow({ g, isExp, isAdmin, matName, wh, projName, unit, onToggle, timeline }: {
|
||||
g: Group;
|
||||
isExp: boolean;
|
||||
isAdmin: boolean;
|
||||
matName: string;
|
||||
matCatName: string;
|
||||
wh: string;
|
||||
projName: string;
|
||||
unit: string;
|
||||
@@ -591,7 +543,6 @@ function GroupRow({ g, isExp, isAdmin, matName, matCatName, wh, projName, unit,
|
||||
<tr className={`border-b border-line/60 cursor-pointer transition-colors ${isExp ? "bg-accent/5" : "hover:bg-accent/5"}`} onClick={onToggle}>
|
||||
<td className="py-2.5 text-center text-muted">{isExp ? "▾" : "▸"}</td>
|
||||
<td className="py-2.5 pe-3 font-bold text-ink">{matName}</td>
|
||||
<td className="py-2.5 pe-3 text-muted">{matCatName}</td>
|
||||
<td className="py-2.5 pe-3">
|
||||
<div>{wh}</div>
|
||||
{isAdmin && <span className="block text-[10.5px] text-muted">{projName}</span>}
|
||||
|
||||
@@ -9,10 +9,9 @@ import { useDataChanged } from "../../lib/crossTab";
|
||||
import type { InventoryRow, InventoryTransaction, Warehouse } from "../../lib/types";
|
||||
|
||||
export interface LookupData {
|
||||
categories: { id: string; slug: string; name: string }[];
|
||||
units: { id: number; code: string; name_fa: string | null }[];
|
||||
brands: { id: string; name: string }[];
|
||||
materials: { id: string; name: string; category: { id: string; name: string } | null; canonical_unit: { id: number; code: string; name_fa: string | null } | null }[];
|
||||
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 }[];
|
||||
}
|
||||
|
||||
@@ -34,14 +33,13 @@ export function useInventoryData(): InventoryData {
|
||||
const [inventory, setInventory] = useState<InventoryRow[]>([]);
|
||||
const [transactions, setTransactions] = useState<InventoryTransaction[]>([]);
|
||||
const [lookup, setLookup] = useState<LookupData>({
|
||||
categories: [], units: [], brands: [], materials: [], projects: [],
|
||||
units: [], brands: [], materials: [], projects: [],
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const [cats, units, brands, mats, projs, whs, inv, txs] = await Promise.all([
|
||||
apiV26.lookup.categories(),
|
||||
const [units, brands, mats, projs, whs, inv, txs] = await Promise.all([
|
||||
apiV26.lookup.units(),
|
||||
apiV26.lookup.brands(),
|
||||
apiV26.lookup.materials(),
|
||||
@@ -50,7 +48,7 @@ export function useInventoryData(): InventoryData {
|
||||
apiV26.inventory.list({ project_id: projectId }),
|
||||
apiV26.inventory.transactions({ project_id: projectId }),
|
||||
]);
|
||||
setLookup({ categories: cats, units, brands, materials: mats, projects: projs });
|
||||
setLookup({ units, brands, materials: mats, projects: projs });
|
||||
setWarehouses(whs);
|
||||
setInventory(inv);
|
||||
setTransactions(txs);
|
||||
@@ -108,11 +106,6 @@ export function materialName(materials: LookupData["materials"], id: string): st
|
||||
return m ? m.name : "—";
|
||||
}
|
||||
|
||||
export function catName(categories: LookupData["categories"], id: string | null | undefined): string {
|
||||
const c = categories.find((x) => x.id === id);
|
||||
return c ? c.name : "—";
|
||||
}
|
||||
|
||||
export function projName(projects: LookupData["projects"], id: string | null | undefined): string {
|
||||
const p = projects.find((x) => x.id === id);
|
||||
return p ? p.name : "—";
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
lineItemTotal,
|
||||
materialById,
|
||||
materialLabel,
|
||||
refConverted,
|
||||
supplierById,
|
||||
} from "../../lib/quoteUtils";
|
||||
import { faNum, fmtDate } from "../../lib/formatters";
|
||||
@@ -81,11 +80,11 @@ export function ProjectsPage() {
|
||||
const quotes = useQuotes();
|
||||
const suppliers = useSuppliers();
|
||||
const materials = useMaterials();
|
||||
const { categoryUnits, unitConversions, standardConversions } = useUnits();
|
||||
const { standardConversions } = useUnits();
|
||||
const addProject = useAddProject();
|
||||
const topics = useAppStore((s) => s.topics);
|
||||
|
||||
const store: Store = { quotes, suppliers, materials, projects, categoryUnits, unitConversions, standardConversions, topics };
|
||||
const store: Store = { quotes, suppliers, materials, projects, standardConversions, topics };
|
||||
|
||||
const [openProject, setOpenProject] = useState<string | null>(null);
|
||||
const [newName, setNewName] = useState("");
|
||||
@@ -189,17 +188,10 @@ function renderProjectDetail(store: Store, name: string): React.ReactElement {
|
||||
|
||||
const itemLine = (li: Quote["line_items"][number]) => {
|
||||
const total = lineItemTotal(li);
|
||||
const conv = refConverted(store, li);
|
||||
return (
|
||||
<div key={li.id}>
|
||||
{matName(li.material_id)} — {faNum(li.quantity)} {li.unit} × {faNum(li.unit_price || 0)} ={" "}
|
||||
<b>{faNum(total || 0)} تومان</b>
|
||||
{conv && (
|
||||
<span className="pj-conv">
|
||||
{" "}
|
||||
— <span className="pj-conv">≈ {faNum(conv.qty)} {conv.unit}({faNum(conv.perRef ?? 0)} تومان/{conv.unit})</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -48,21 +48,8 @@ export function SupplierDetailPage() {
|
||||
return [...set].sort((a, b) => a.localeCompare(b, "fa"));
|
||||
}, [materials]);
|
||||
|
||||
// متریالهای خریداریشده از این تأمینکننده (از پیشنهادها)
|
||||
const purchased = useMemo(() => {
|
||||
const out = new Set<string>();
|
||||
quotes.forEach((q) => {
|
||||
if (q.supplier_id !== id) return;
|
||||
(q.line_items || []).forEach((li) => {
|
||||
if (li.rejected) return;
|
||||
const m = materials.find((x) => x.id === li.material_id);
|
||||
// دستهی نمایشی متریال = دستهی نخستین تأمینکنندهی متصل (v2.7)
|
||||
if (!m || !m.supplier_category) return;
|
||||
out.add(m.supplier_category);
|
||||
});
|
||||
});
|
||||
return [...out].sort((a, b) => a.localeCompare(b, "fa"));
|
||||
}, [quotes, materials, id]);
|
||||
// دستهی این تأمینکننده — دسته فقط برچسبِ خودِ تأمینکننده است (v2.7)
|
||||
const purchased = useMemo(() => [...(s?.categories || [])].sort((a, b) => a.localeCompare(b, "fa")), [s]);
|
||||
|
||||
if (!s) {
|
||||
return (
|
||||
|
||||
@@ -77,7 +77,7 @@ export function materialOptions(
|
||||
// همنام پیشنهاد میشوند.
|
||||
if (cat)
|
||||
list = list.filter(
|
||||
(m) => normalize(materialBaseName(m.name, m.brand, m.specs, m.supplier_category)) === normalize(cat),
|
||||
(m) => normalize(materialBaseName(m.name, m.brand, m.specs)) === normalize(cat),
|
||||
);
|
||||
// مشخصهها سراسری هستند: رجیستری attribute-definitions + مشخصههای همهی
|
||||
// متریالها + مشخصههای تایپشده در سایر ردیفها. به تاپیک/دسته/برندِ قلمِ
|
||||
@@ -109,7 +109,7 @@ export function Step2LineItems({ items, onItemsChange, selectedTopic, onRemember
|
||||
const allMaterialNames = useMemo(() => {
|
||||
const set = new Set<string>();
|
||||
materials.forEach((m) => {
|
||||
const base = materialBaseName(m.name, m.brand, m.specs, m.supplier_category);
|
||||
const base = materialBaseName(m.name, m.brand, m.specs);
|
||||
if (base) set.add(base);
|
||||
});
|
||||
return [...set].sort((a, b) => a.localeCompare(b, "fa"));
|
||||
|
||||
@@ -317,7 +317,6 @@ export function WizardPage() {
|
||||
brand,
|
||||
specs,
|
||||
base_unit: unit || "عدد",
|
||||
supplier_category: "",
|
||||
});
|
||||
materialId = created.id;
|
||||
} catch (err) {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
import { create } from "zustand";
|
||||
import type {
|
||||
CategoryUnit,
|
||||
Material,
|
||||
Project,
|
||||
Quote,
|
||||
@@ -15,7 +14,6 @@ import type {
|
||||
Store,
|
||||
Supplier,
|
||||
Topic,
|
||||
UnitConversion,
|
||||
} from "../lib/types";
|
||||
|
||||
interface AppStore {
|
||||
@@ -24,8 +22,6 @@ interface AppStore {
|
||||
materials: Material[];
|
||||
projects: Project[];
|
||||
quotes: Quote[];
|
||||
categoryUnits: CategoryUnit[];
|
||||
unitConversions: UnitConversion[];
|
||||
standardConversions: StandardConversion[];
|
||||
topics: Topic[]; // دستهبندیهای ۸گانه — از /material-categories
|
||||
|
||||
@@ -45,9 +41,6 @@ interface AppStore {
|
||||
deleteSupplier: (id: string) => void;
|
||||
addMaterial: (m: Material) => void;
|
||||
addProject: (p: Project) => void;
|
||||
setCategoryUnit: (cu: CategoryUnit) => void;
|
||||
setConversion: (c: UnitConversion) => void;
|
||||
deleteConversion: (id: string) => void;
|
||||
setStandardConversion: (sc: StandardConversion) => void;
|
||||
deleteStandardConversion: (id: string) => void;
|
||||
|
||||
@@ -62,8 +55,6 @@ export const useAppStore = create<AppStore>()((set) => ({
|
||||
materials: [],
|
||||
projects: [],
|
||||
quotes: [],
|
||||
categoryUnits: [],
|
||||
unitConversions: [],
|
||||
standardConversions: [],
|
||||
topics: [],
|
||||
|
||||
@@ -77,8 +68,6 @@ export const useAppStore = create<AppStore>()((set) => ({
|
||||
materials: store.materials,
|
||||
projects: store.projects,
|
||||
quotes: store.quotes,
|
||||
categoryUnits: store.categoryUnits,
|
||||
unitConversions: store.unitConversions,
|
||||
standardConversions: store.standardConversions,
|
||||
topics: store.topics,
|
||||
}),
|
||||
@@ -96,24 +85,6 @@ export const useAppStore = create<AppStore>()((set) => ({
|
||||
deleteSupplier: (id) => set((s) => ({ suppliers: s.suppliers.filter((x) => x.id !== id) })),
|
||||
addMaterial: (m) => set((s) => ({ materials: [...s.materials, m] })),
|
||||
addProject: (p) => set((s) => ({ projects: [...s.projects, p] })),
|
||||
setCategoryUnit: (cu) =>
|
||||
set((s) => {
|
||||
const idx = s.categoryUnits.findIndex((x) => x.id === cu.id);
|
||||
const categoryUnits = [...s.categoryUnits];
|
||||
if (idx >= 0) categoryUnits[idx] = cu;
|
||||
else categoryUnits.push(cu);
|
||||
return { categoryUnits };
|
||||
}),
|
||||
setConversion: (c) =>
|
||||
set((s) => {
|
||||
const idx = s.unitConversions.findIndex((x) => x.id === c.id);
|
||||
const unitConversions = [...s.unitConversions];
|
||||
if (idx >= 0) unitConversions[idx] = c;
|
||||
else unitConversions.push(c);
|
||||
return { unitConversions };
|
||||
}),
|
||||
deleteConversion: (id) =>
|
||||
set((s) => ({ unitConversions: s.unitConversions.filter((x) => x.id !== id) })),
|
||||
setStandardConversion: (sc) =>
|
||||
set((s) => {
|
||||
const idx = s.standardConversions.findIndex((x) => x.id === sc.id);
|
||||
|
||||
Reference in New Issue
Block a user