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:
@@ -10,7 +10,7 @@ import {
|
||||
refConverted,
|
||||
specsLabel,
|
||||
} from "../../lib/quoteUtils";
|
||||
import { fmtDate, fmtNum } from "../../lib/formatters";
|
||||
import { fmtDate, fmtDateTime, fmtNum } from "../../lib/formatters";
|
||||
import { groupCommas } from "../../lib/numutil";
|
||||
import { FlowDiagram, type FlowRefs } from "./FlowDiagram";
|
||||
import { itemProjects } from "../../lib/quoteUtils";
|
||||
@@ -69,6 +69,15 @@ export function QuoteDetail({ store, q }: QuoteDetailProps) {
|
||||
const projs = itemProjects(q, li);
|
||||
const conv = isRejected ? null : refConverted(store, li);
|
||||
const specText = specsLabel(materialSpecs(m));
|
||||
|
||||
// رسید جزیی → قلم به دو بخش «رسید شده» و «در راه» شکسته میشود (نمایشی):
|
||||
// مجموع دو بخش همیشه با تعداد/مبلغ اصلی برابر است (عدم تغییر جمع کل پیشنهاد).
|
||||
const qty = li.quantity ?? 0;
|
||||
const recv = !isRejected ? li.received_qty || 0 : 0;
|
||||
const remaining = Math.max(0, qty - recv);
|
||||
const whNames = (li.received_warehouses || []).map((w) => w.name);
|
||||
const showSplit = !isRejected && q.status !== "offered" && qty > 0;
|
||||
const fullyReceived = showSplit && recv >= qty;
|
||||
return (
|
||||
<div
|
||||
key={li.id}
|
||||
@@ -81,27 +90,47 @@ export function QuoteDetail({ store, q }: QuoteDetailProps) {
|
||||
flowRefs.items.delete(li.id);
|
||||
}
|
||||
}}
|
||||
className={`flow-box flow-item${isRejected ? " rejected" : ""}${li.arrived_date ? " deal" : ""}`}
|
||||
className={`flow-box flow-item${isRejected ? " rejected" : ""}${li.arrived_date ? " deal" : ""}${
|
||||
fullyReceived ? " fb-fully-received" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="fb-name">
|
||||
<Package className="me-1 inline h-3.5 w-3.5 text-muted" />
|
||||
<Package className="me-1 inline h-3 w-3 text-muted" />
|
||||
{m.category} {m.brand}
|
||||
{specText ? ` · ${specText}` : ""}
|
||||
</div>
|
||||
<div className="fb-meta">
|
||||
<Building2 className="me-1 inline h-3 w-3" /> {projs.length ? projs.join("، ") : "—"}
|
||||
</div>
|
||||
<div className="fb-nums">
|
||||
<span>
|
||||
{fmtNum(li.quantity)} {li.unit || ""}
|
||||
</span>
|
||||
<span>× {groupCommas(li.unit_price, true)}</span>
|
||||
<span className="fb-total">= {total != null ? groupCommas(total, true) + " تومان" : "—"}</span>
|
||||
</div>
|
||||
{li.received_qty != null && li.received_qty > 0 && li.quantity != null && (
|
||||
<div className="fb-receipt">
|
||||
<Package className="me-1 inline h-3 w-3" />
|
||||
رسیده: {fmtNum(li.received_qty)} از {fmtNum(li.quantity)} — در راه {fmtNum(Math.max(0, li.quantity - li.received_qty))}
|
||||
{showSplit ? (
|
||||
<div className="fb-nums">
|
||||
{recv > 0 && (
|
||||
<span className="pill-recvd">
|
||||
رسید شده ✓ {fmtNum(recv)} {li.unit || ""}
|
||||
</span>
|
||||
)}
|
||||
{remaining > 0 && (
|
||||
<span className="pill-pend">
|
||||
در راه ⏳ {fmtNum(remaining)} {li.unit || ""}
|
||||
</span>
|
||||
)}
|
||||
<span>× {groupCommas(li.unit_price, true)}</span>
|
||||
<span className="fb-total">= {total != null ? groupCommas(total, true) + " تومان" : "—"}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="fb-nums">
|
||||
<span>
|
||||
{fmtNum(li.quantity)} {li.unit || ""}
|
||||
</span>
|
||||
<span>× {groupCommas(li.unit_price, true)}</span>
|
||||
<span className="fb-total">= {total != null ? groupCommas(total, true) + " تومان" : "—"}</span>
|
||||
</div>
|
||||
)}
|
||||
{showSplit && recv > 0 && (
|
||||
<div className="fb-receipt-meta">
|
||||
<span className="pill-recvd-mini">✓ {fmtNum(recv)} {li.unit || ""}</span>
|
||||
{li.arrived_date && <span>تاریخ رسید: {fmtDateTime(li.arrived_date)}</span>}
|
||||
{whNames.length > 0 && <span>به انبار: {whNames.join("، ")}</span>}
|
||||
</div>
|
||||
)}
|
||||
{conv && (
|
||||
|
||||
Reference in New Issue
Block a user