feat(frontend): fulfillment split, API-driven topics, hub-and-spoke nav, stock-count modal, FOUND ceiling, semantic colors
- bug1: quote header fulfillment counter («رسید: X از Y قلم» amber / «تکمیل شد» green / «در راه»); expanded line items visually split into the received part (رسید شده ✓ + date + target warehouse from the receipt allocations) and the in-transit remainder — quantities always sum to the original line (invariant D holds, display-only split) - bug2: 8-topic grids (suppliers/wizard/units) now come from /material-categories via the store (new lib/topics.ts; fixed the mismatched «ابزار و تجهیزات ساخت» label); suppliers topic->category resolution no longer falls back to an arbitrary category; duplicate month/status lists reuse the shared dictionaries; TopicGrid gained a loading EmptyState - bug3: hub-and-spoke — the tab strip renders only on the warehouse dashboard; sub-views hide it and navigate back via «بازگشت به داشبورد»; «افزایش موجود» became the «تغییر موجودی» entry button - bug4: stock count goes through a confirmation modal (current vs counted vs variance with کسری/مازاد badges) before posting STOCK_COUNT; numbers parsed defensively - bug5: found qty is capped client-side (red input + inline warning, submit disabled) and backend returns 422 when exceeding the missing balance; fully-recovered losses drop off the pending list - bug6+7: quantities render as absolute values with semantic color coding (emerald inflow / rose outflow / blue neutral) across dashboard timeline, daily control and the change-inventory form
This commit is contained in:
+21
-29
@@ -13,6 +13,7 @@
|
||||
// ============================================================
|
||||
|
||||
import { normalize } from "./normalize";
|
||||
import { buildTopics } from "./topics";
|
||||
import { useAuthStore } from "../store/auth";
|
||||
import type {
|
||||
Attachment,
|
||||
@@ -279,6 +280,7 @@ interface RawReceipt {
|
||||
received_qty: number;
|
||||
received_at?: string | null;
|
||||
on_time?: boolean | null;
|
||||
allocations?: { warehouse_id: string; warehouse_name: string; quantity: number }[] | null;
|
||||
}
|
||||
interface RawLineItem {
|
||||
id: string;
|
||||
@@ -378,6 +380,7 @@ interface Lookup {
|
||||
categoryIdToSlug: Map<string, string>;
|
||||
categoryIdToName: Map<string, string>;
|
||||
categoryNameToId: Map<string, string>; // کلید: نام نرمالشده
|
||||
categorySlugToName: Map<string, string>; // slug → نام دسته (تاپیک)
|
||||
brandNameToId: Map<string, string>; // کلید: نام نرمالشده
|
||||
}
|
||||
|
||||
@@ -396,54 +399,35 @@ function buildLookup(units: RawUnit[], categories: RawMaterialCategory[]): Looku
|
||||
const categoryIdToSlug = new Map<string, string>();
|
||||
const categoryIdToName = new Map<string, string>();
|
||||
const categoryNameToId = new Map<string, string>();
|
||||
const categorySlugToName = new Map<string, string>();
|
||||
for (const c of categories) {
|
||||
categoryIdToSlug.set(c.id, c.slug);
|
||||
categoryIdToName.set(c.id, c.name);
|
||||
categoryNameToId.set(norm(c.name), c.id);
|
||||
categorySlugToName.set(c.slug, c.name);
|
||||
}
|
||||
return { units, categories, unitIdToName, unitNameToId, categoryIdToSlug, categoryIdToName, categoryNameToId, brandNameToId: new Map() };
|
||||
return { units, categories, unitIdToName, unitNameToId, categoryIdToSlug, categoryIdToName, categoryNameToId, categorySlugToName, brandNameToId: new Map() };
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Adapters — بکاند → تایپهای جلوی برنامه
|
||||
// ============================================================
|
||||
|
||||
// ۸ تاپیک ثابت جلوی برنامه ← از slug دستهبندی بکاند
|
||||
const SLUG_TO_TOPIC: Record<string, string> = {
|
||||
cement: "مصالح ساختمانی",
|
||||
rebar: "فلزات و مقاطع فولادی",
|
||||
"tile-ceramic": "نازککاری و دکوراسیون داخلی",
|
||||
paint: "نازککاری و دکوراسیون داخلی",
|
||||
"pipe-fitting": "تاسیسات مکانیکی و آبی",
|
||||
"electrical-cable": "تجهیزات و لوازم برقی",
|
||||
"electrical-panel": "تجهیزات و لوازم برقی",
|
||||
insulation: "مواد شیمیایی ساختمانی",
|
||||
drywall: "نازککاری و دکوراسیون داخلی",
|
||||
"concrete-additive": "مواد شیمیایی ساختمانی",
|
||||
// v2.6 backend: دستههای ۸گانهی ثابت (تاپیک = دسته)
|
||||
"mechanical-installations-pipes": "تاسیسات مکانیکی و آبی",
|
||||
"electrical-equipment-supplies": "تجهیزات و لوازم برقی",
|
||||
"doors-windows-glass": "درب، پنجره و شیشه",
|
||||
"metals-steel-sections": "فلزات و مقاطع فولادی",
|
||||
"construction-materials": "مصالح ساختمانی",
|
||||
"construction-chemicals": "مواد شیمیایی ساختمانی",
|
||||
"finishing-interior-decoration": "نازککاری و دکوراسیون داخلی",
|
||||
"construction-tools-equipment": "ابزار و تجهیزات کارگاهی",
|
||||
};
|
||||
|
||||
// v2.6 backend: بکاند برای ساخت تأمینکنندهی جدید category_id الزامی دارد.
|
||||
// از روی تاپیکِ انتخابشده، دستهی متناظر پیدا میشود (تاپیک = دسته).
|
||||
let _categoryIdCache: { slug: string; id: string }[] | null = null;
|
||||
// دسته از روی نام تاپیکِ انتخابشده پیدا میشود (تاپیک = دسته؛ نامها از همان
|
||||
// منبع /material-categories میآیند، پس مقایسه روی نام نرمالشده دقیق است).
|
||||
let _categoryIdCache: { name: string; id: string }[] | null = null;
|
||||
async function categoryIdForTopic(topic: string): Promise<string> {
|
||||
if (!_categoryIdCache) {
|
||||
try {
|
||||
const cats = await getData<RawMaterialCategory[]>(`/material-categories?page_size=100`);
|
||||
_categoryIdCache = cats.map((c) => ({ slug: c.slug, id: c.id }));
|
||||
_categoryIdCache = cats.map((c) => ({ name: c.name, id: c.id }));
|
||||
} catch {
|
||||
_categoryIdCache = [];
|
||||
}
|
||||
}
|
||||
const match = _categoryIdCache.find((c) => SLUG_TO_TOPIC[c.slug] === topic) || _categoryIdCache[0];
|
||||
const wanted = norm(topic);
|
||||
const match = _categoryIdCache.find((c) => norm(c.name) === wanted);
|
||||
return match ? match.id : "";
|
||||
}
|
||||
|
||||
@@ -452,7 +436,9 @@ function adaptMaterial(raw: RawMaterial, lookup: Lookup): Material {
|
||||
id: raw.id,
|
||||
name: raw.name,
|
||||
// تاپیک ذخیرهشده در بکاند اولویت دارد؛ وگرنه از slug دسته استنتاج میشود
|
||||
topic: raw.topic || (raw.category && SLUG_TO_TOPIC[raw.category.slug]) || "",
|
||||
topic:
|
||||
raw.topic ||
|
||||
(raw.category?.slug ? lookup.categorySlugToName.get(raw.category.slug) || "" : ""),
|
||||
category: raw.category?.name || "",
|
||||
brand: raw.brands?.[0]?.name || "",
|
||||
// مشخصهها از attribute های متریال خوانده میشوند (EAV)
|
||||
@@ -481,6 +467,11 @@ function adaptLineItem(raw: RawLineItem): Quote["line_items"][number] {
|
||||
rejected: raw.decision?.status === "rejected",
|
||||
arrived_date: raw.receipt?.received_at ?? null,
|
||||
received_qty: raw.receipt?.received_qty ?? null,
|
||||
received_warehouses: (raw.receipt?.allocations || []).map((a) => ({
|
||||
id: a.warehouse_id,
|
||||
name: a.warehouse_name,
|
||||
quantity: a.quantity,
|
||||
})),
|
||||
projects: (raw.projects || []).map((p) => ({ id: p.id, name: p.name })),
|
||||
};
|
||||
}
|
||||
@@ -687,6 +678,7 @@ export const api = {
|
||||
categoryUnits: cruRes.map(adaptCategoryUnit),
|
||||
unitConversions: ucRes.map((uc) => adaptUnitConversion(uc, lookup)),
|
||||
standardConversions: scuRes.map(adaptStandardConversion),
|
||||
topics: buildTopics(categoriesRes),
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user