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:
ali_mohaghegh
2026-09-02 18:15:32 +03:30
parent ca84d8664e
commit ac76af2ffe
27 changed files with 500 additions and 217 deletions
+10 -5
View File
@@ -1,20 +1,25 @@
import { TOPICS } from "../lib/constants";
import { EmptyState } from "./EmptyState";
import type { Topic } from "../lib/types";
import { normalize } from "../lib/normalize";
import { faNum } from "../lib/formatters";
import { cn } from "../lib/utils";
export interface TopicGridProps {
topics: Topic[]; // از store (API) می‌آید
selectedTopic?: string;
onSelect: (topicKey: string) => void;
onSelect: (topicName: string) => void;
showCounts?: boolean; // تعداد تأمین‌کننده‌های هر تاپیک
counts?: Record<string, number>; // topic name → count
}
// گرید ۴ ستونه‌ی ۸ تاپیک
export function TopicGrid({ selectedTopic, onSelect, showCounts = false, counts }: TopicGridProps) {
// گرید تاپیک‌ها (از /material-categories — نه لیست ثابت)
export function TopicGrid({ topics, selectedTopic, onSelect, showCounts = false, counts }: TopicGridProps) {
if (!topics.length) {
return <EmptyState emoji="🗂️" text="در حال بارگذاری دسته‌بندی‌ها…" />;
}
return (
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
{TOPICS.map((t) => {
{topics.map((t) => {
const selected = selectedTopic != null && normalize(selectedTopic) === normalize(t.name);
const count = counts?.[t.name] ?? 0;
return (