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:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { Scale, Repeat, TriangleAlert, Link2, Pencil, Trash2, Plus } from "lucide-react";
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
useUnits,
|
||||
} from "../../hooks";
|
||||
import { normalize } from "../../lib/normalize";
|
||||
import { TOPICS } from "../../lib/constants";
|
||||
import { topicByName } from "../../lib/topics";
|
||||
import { useAppStore } from "../../store";
|
||||
import { faNum } from "../../lib/formatters";
|
||||
import { parseNum } from "../../lib/numutil";
|
||||
import { findConversion, materialById } from "../../lib/quoteUtils";
|
||||
@@ -42,6 +43,7 @@ export function UnitsPage() {
|
||||
const suppliers = useSuppliers();
|
||||
const materials = useMaterials();
|
||||
const quotes = useQuotes();
|
||||
const topics = useAppStore((s) => s.topics);
|
||||
const { categoryUnits, unitConversions, standardConversions } = useUnits();
|
||||
const saveRefUnit = useSaveRefUnit();
|
||||
const saveConversion = useSaveConversion();
|
||||
@@ -154,11 +156,11 @@ export function UnitsPage() {
|
||||
|
||||
const topicCounts = useMemo(() => {
|
||||
const out: Record<string, number> = {};
|
||||
TOPICS.forEach((t) => {
|
||||
topics.forEach((t) => {
|
||||
out[t.name] = Object.keys(catTopicMap).filter((c) => normalize(catTopicMap[c]) === normalize(t.name)).length;
|
||||
});
|
||||
return out;
|
||||
}, [catTopicMap]);
|
||||
}, [topics, catTopicMap]);
|
||||
|
||||
// ---------- actions ----------
|
||||
const saveRef = () => {
|
||||
@@ -282,7 +284,11 @@ export function UnitsPage() {
|
||||
};
|
||||
|
||||
// ---------- پیشفرض از URL ----------
|
||||
useMemo(() => {
|
||||
// یک بار بهازای هر پارامتر اعمال میشود (وقتی دادهی لازم لود شد)
|
||||
const appliedParams = useRef("");
|
||||
useEffect(() => {
|
||||
const key = `${params.get("cat") ?? ""}|${params.get("topic") ?? ""}`;
|
||||
if (appliedParams.current === key) return;
|
||||
const cat = params.get("cat");
|
||||
const topicParam = params.get("topic");
|
||||
const unitParam = params.get("unit");
|
||||
@@ -295,14 +301,17 @@ export function UnitsPage() {
|
||||
}
|
||||
}
|
||||
if (!catFound && topicParam) {
|
||||
const t = TOPICS.find((x) => normalize(x.name) === normalize(topicParam));
|
||||
if (t) setSelectedTopic(t.name);
|
||||
const t = topicByName(topicParam, topics);
|
||||
if (t) {
|
||||
setSelectedTopic(t.name);
|
||||
catFound = "";
|
||||
}
|
||||
}
|
||||
if (unitParam && catFound) {
|
||||
setFormFrom(unitParam);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
if (catFound !== null) appliedParams.current = key;
|
||||
}, [params, topics, catTopicMap]);
|
||||
|
||||
// ---------- render ----------
|
||||
const ref = selectedCategory ? refUnitFor(selectedCategory) : "";
|
||||
@@ -329,7 +338,7 @@ export function UnitsPage() {
|
||||
<div className="units-hint mb-3 text-[12px] text-muted">
|
||||
اول موضوع را انتخاب کنید تا فقط دستههای همان موضوع بیاید
|
||||
</div>
|
||||
<TopicGrid onSelect={(t) => setSelectedTopic(t)} showCounts counts={topicCounts} />
|
||||
<TopicGrid topics={topics} onSelect={(t) => setSelectedTopic(t)} showCounts counts={topicCounts} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="units-layout">
|
||||
|
||||
Reference in New Issue
Block a user