- All ChangeInventoryPage sections (withdrawal/damage/waste/loss/transfer/ stock-count/project-transfer) become list-based like the withdrawal section: pick a warehouse, see its stock rows, material name is only an optional filter; manual material/brand/unit entry removed everywhere except PURCHASE. - RETURN redesigned: day-range presets (today/yesterday/7d/30d + Jalali custom range) with server-side filters, immutable-ledger returnable remainder per withdrawal (REF:[tx-id] note tag + FIFO attribution for legacy returns, round4 float-safe), destination warehouse selector. - PROJECT_TRANSFER: row-based list with one destination project per batch; sequential submits keep failed rows inline with errors and drop succeeded rows; history dashboard untouched. - PURCHASE: unit locked to catalog (search-only Combobox), wizard-style '+ register new material' option with yellow warning, optional Key-Value specs editor (replaces mandatory text chips), continuous-entry UX (warehouse/date kept, auto-focus, session log, duplicate warning). - Mobile-first: responsive table/cards core (StockTable), numeric keyboards (inputMode+pattern), sticky bottom submit bar with dynamic counter above MobileTabBar incl. safe-area inset, horizontally scrollable touch chips. - Backend fix (prereq of RETURN): GET /inventory/transactions date filters crashed with 500 under asyncpg (string vs timestamptz); parse to aware UTC datetimes (DB runs Etc/UTC). - docs/inventory-ledger-gaps.md: ledger gap report (server-side return cap, standalone TRANSFER_IN, transfer row fallback, audit coverage, legacy REF backfill, UTC day semantics, pagination gaps).
146 lines
7.2 KiB
TypeScript
146 lines
7.2 KiB
TypeScript
// ============================================================
|
||
// جدول «پیدا شده» — مفقودیهای باقیماندهی انبار (LOSS − FOUND)
|
||
// با انتخاب انبار همهی مفقودیها میآیند و نام متریال فقط فیلتر است.
|
||
// ============================================================
|
||
|
||
import type { InventoryTransaction } from "../../../lib/types";
|
||
import { faNum } from "../../../lib/formatters";
|
||
import { cn } from "../../../lib/utils";
|
||
import { specsLine, type RowDraft } from "./StockTable";
|
||
|
||
export interface LossItem { tx: InventoryTransaction; remaining: number; }
|
||
|
||
interface FoundTableProps {
|
||
items: LossItem[];
|
||
rows: RowDraft[];
|
||
setRows: (r: RowDraft[]) => void;
|
||
unitNameFn: (id: number | null | undefined) => string;
|
||
emptyMsg: string;
|
||
loading?: boolean;
|
||
}
|
||
|
||
function parseLocal(s: string): number {
|
||
const v = parseFloat(String(s).replace(/[۰-۹]/g, (d) => String("۰۱۲۳۴۵۶۷۸۹".indexOf(d))).replace(/[،,]/g, ""));
|
||
return isFinite(v) ? v : 0;
|
||
}
|
||
|
||
export function FoundTable({ items, rows, setRows, unitNameFn, emptyMsg, loading }: FoundTableProps) {
|
||
if (loading) {
|
||
return <div className="rounded-lg border border-dashed border-line p-4 text-[12.5px] text-muted">در حال بارگذاری مفقودیها…</div>;
|
||
}
|
||
if (emptyMsg) {
|
||
return <div className="rounded-lg border border-dashed border-line p-4 text-[12.5px] text-muted">{emptyMsg}</div>;
|
||
}
|
||
|
||
return (
|
||
<>
|
||
{/* دسکتاپ */}
|
||
<div className="hidden sm:block">
|
||
<div className="max-h-[min(560px,calc(100vh-320px))] overflow-y-auto overflow-x-auto rounded-lg border border-line">
|
||
<table className="w-full text-[12.5px] min-w-[620px]">
|
||
<thead className="sticky top-0 z-10 border-b border-line bg-panel text-right text-muted shadow-2xs">
|
||
<tr>
|
||
<th className="py-2.5 px-3">متریال</th>
|
||
<th className="py-2.5 px-3">مشخصهها</th>
|
||
<th className="py-2.5 px-3">برند</th>
|
||
<th className="py-2.5 px-3">واحد</th>
|
||
<th className="py-2.5 px-3">مانده مفقودی</th>
|
||
<th className="py-2.5 px-3">تعداد پیداشده</th>
|
||
<th className="py-2.5 px-3">یادداشت</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-line/40">
|
||
{items.map(({ tx, remaining }, i) => {
|
||
const over = parseLocal(rows[i]?.qty || "") > remaining;
|
||
return (
|
||
<tr key={tx.id + "|" + i} className={cn("hover:bg-bg/40 transition-colors", over && "bg-rose-50/40")}>
|
||
<td className="py-2 px-3 font-semibold text-ink">{tx.material?.name || "—"}</td>
|
||
<td className="py-2 px-3 text-muted">{specsLine(tx.specs)}</td>
|
||
<td className="py-2 px-3">{tx.brand?.name || "—"}</td>
|
||
<td className="py-2 px-3">{unitNameFn(tx.unit?.id)}</td>
|
||
<td className="py-2 px-3 font-bold text-rose-600">{faNum(remaining)}</td>
|
||
<td className="py-2 px-3">
|
||
<input
|
||
type="text"
|
||
inputMode="decimal"
|
||
pattern="[0-9]*"
|
||
className={cn(
|
||
"h-8 w-24 rounded-lg border bg-panel px-2 text-sm ltr focus:border-accent",
|
||
over ? "border-red-500 bg-rose-50 text-rose-700" : "border-line",
|
||
)}
|
||
placeholder="۰"
|
||
value={rows[i]?.qty || ""}
|
||
onChange={(e) => setRows(rows.map((x, xi) => (xi === i ? { ...x, qty: e.target.value } : x)))}
|
||
/>
|
||
{over && <div className="mt-0.5 text-[10px] font-bold text-rose-700">بیشتر از مفقودی!</div>}
|
||
</td>
|
||
<td className="py-2 px-3">
|
||
<input
|
||
type="text"
|
||
className="h-8 w-32 rounded-lg border border-line bg-panel px-2 text-sm focus:border-accent"
|
||
placeholder="توضیح"
|
||
value={rows[i]?.note || ""}
|
||
onChange={(e) => setRows(rows.map((x, xi) => (xi === i ? { ...x, note: e.target.value } : x)))}
|
||
/>
|
||
</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
{/* موبایل: کارتها */}
|
||
<div className="sm:hidden">
|
||
<div className="max-h-[min(560px,calc(100vh-320px))] space-y-2 overflow-y-auto pl-1">
|
||
{items.map(({ tx, remaining }, i) => {
|
||
const over = parseLocal(rows[i]?.qty || "") > remaining;
|
||
return (
|
||
<div key={tx.id + "|" + i} className={cn("rounded-xl border bg-panel p-3", over ? "border-red-400" : "border-line")}>
|
||
<div className="flex items-start justify-between gap-2">
|
||
<div className="min-w-0">
|
||
<div className="truncate text-[13.5px] font-bold text-ink">{tx.material?.name || "—"}</div>
|
||
<div className="mt-0.5 text-[11.5px] text-muted">
|
||
{tx.brand?.name || "بدون برند"} · {unitNameFn(tx.unit?.id)}
|
||
</div>
|
||
{tx.specs && Object.keys(tx.specs).length > 0 && (
|
||
<div className="mt-1 text-[11px] leading-4 text-muted">{specsLine(tx.specs)}</div>
|
||
)}
|
||
</div>
|
||
<div className="shrink-0 rounded-lg bg-bg px-2 py-1 text-center">
|
||
<div className="text-[10px] text-muted">مانده مفقودی</div>
|
||
<div className="text-[14px] font-black text-rose-600">{faNum(remaining)}</div>
|
||
</div>
|
||
</div>
|
||
<div className="mt-2 grid grid-cols-[1fr_1.2fr] gap-2">
|
||
<input
|
||
type="text"
|
||
inputMode="decimal"
|
||
pattern="[0-9]*"
|
||
className={cn(
|
||
"h-11 w-full rounded-lg border bg-panel px-3 text-[15px] font-bold ltr focus:border-accent",
|
||
over ? "border-red-500 bg-rose-50 text-rose-700" : "border-line",
|
||
)}
|
||
placeholder="تعداد پیداشده"
|
||
value={rows[i]?.qty || ""}
|
||
onChange={(e) => setRows(rows.map((x, xi) => (xi === i ? { ...x, qty: e.target.value } : x)))}
|
||
/>
|
||
<input
|
||
type="text"
|
||
className="h-11 w-full rounded-lg border border-line bg-panel px-3 text-[13px] focus:border-accent"
|
||
placeholder="توضیح"
|
||
value={rows[i]?.note || ""}
|
||
onChange={(e) => setRows(rows.map((x, xi) => (xi === i ? { ...x, note: e.target.value } : x)))}
|
||
/>
|
||
</div>
|
||
{over && <div className="mt-1 text-[11px] font-bold text-rose-700">بیشتر از مانده مفقودی!</div>}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|