Files
ERP/frontend/src/pages/Inventory/change/FoundTable.tsx
T
whqreza 3931baadd5 feat(inventory): list-based ChangeInventoryPage + mobile-first UX + return ledger controls
- 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).
2026-10-03 21:56:46 +03:30

146 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// جدول «پیدا شده» — مفقودی‌های باقی‌مانده‌ی انبار (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>
</>
);
}