import { useMemo, useState } from "react"; import { Link, useLocation, useSearchParams } from "react-router-dom"; import { Plus, Warehouse as WarehouseIcon, SlidersHorizontal, ArrowUpDown, X, ChevronDown } from "lucide-react"; import { Header, PageLayout } from "../../components/layout/Header"; import { EmptyState } from "../../components/EmptyState"; import { useAuthStore } from "../../store/auth"; import { AppSwitcherMenu } from "../../components/AppSwitcherMenu"; import { inventorySubheader } from "./panelMenu"; import { InventoryTopNav } from "./InventoryTopNav"; import { useInventoryData, brandName, materialName, unitName, whName } from "./useInventoryData"; import type { InventoryRow, InventoryTransaction, TransactionType } from "../../lib/types"; import { faNum } from "../../lib/formatters"; import { isoToJalaliStr } from "../../lib/jalali"; import { cn } from "../../lib/utils"; // رنگ‌بندی معنایی تراکنش‌ها const TX_META: Record = { PURCHASE: { label: "خرید", color: "#10b981", icon: "🛒" }, WITHDRAWAL: { label: "برداشت", color: "#e11d48", icon: "➖" }, RETURN: { label: "برگشت", color: "#10b981", icon: "↩️" }, TRANSFER_IN: { label: "انتقال (ورودی)", color: "#2563eb", icon: "⬅️" }, TRANSFER_OUT: { label: "انتقال (خروجی)", color: "#2563eb", icon: "➡️" }, DAMAGE: { label: "ضایعات", color: "#e11d48", icon: "⚠️" }, WASTE: { label: "اتلاف", color: "#e11d48", icon: "🗑️" }, LOSS: { label: "مفقودی", color: "#e11d48", icon: "📦" }, FOUND: { label: "پیدا شده", color: "#10b981", icon: "🔍" }, STOCK_COUNT: { label: "شمارش موجودی", color: "#2563eb", icon: "✅" }, }; interface Group { key: string; whId: string; matId: string; specs: Record; unitId: number | null; qty: number; updatedAt: string; recs: InventoryRow[]; brands: Record; brandOrder: string[]; } function specsHash(specs: Record | null | undefined): string { const s = specs || {}; const keys = Object.keys(s).filter((k) => s[k] !== "" && s[k] != null).sort(); if (!keys.length) return "default"; return keys.map((k) => `${k}=${s[k]}`).join("|"); } export function InventoryPage() { const location = useLocation(); const user = useAuthStore((s) => s.user); const { warehouses, inventory, transactions, lookup, loading } = useInventoryData(); const isAdmin = user?.role === "admin" || user?.role === "procurement_manager"; const isPM = user?.role === "pm"; const [params] = useSearchParams(); const [filters, setFilters] = useState<{ warehouse: string; material: string; project: string }>({ warehouse: params.get("warehouse") || "", material: "", project: "", }); const [sort, setSort] = useState<"asc" | "desc" | "">(""); const [showMobileFilters, setShowMobileFilters] = useState(false); const [expandedKey, setExpandedKey] = useState(null); const [selTx, setSelTx] = useState(null); const [expBrand, setExpBrand] = useState(null); const visibleWarehouses = useMemo(() => { if (isAdmin && filters.project) return warehouses.filter((w) => w.project_id === filters.project); if (isAdmin) return warehouses; return warehouses.filter((w) => String(w.project_id) === String(user?.project_id || "")); }, [warehouses, isAdmin, filters.project, user?.project_id]); const visibleIds = useMemo(() => new Set(visibleWarehouses.map((w) => w.id)), [visibleWarehouses]); const groups = useMemo(() => { const gmap = new Map(); const order: string[] = []; inventory.forEach((r) => { const whId = r.warehouse?.id; if (!whId || !visibleIds.has(whId)) return; if (filters.warehouse && whId !== filters.warehouse) return; if (filters.material && !r.material.name.includes(filters.material.trim())) return; const gk = `${whId}|${r.material.id}|${specsHash(r.specs)}`; let g = gmap.get(gk); if (!g) { g = { key: gk, whId, matId: r.material.id, specs: r.specs || {}, unitId: r.unit?.id ?? null, qty: 0, updatedAt: r.updated_at, recs: [], brands: {}, brandOrder: [], }; gmap.set(gk, g); order.push(gk); } g.qty += r.quantity_number || 0; if (r.updated_at > g.updatedAt) g.updatedAt = r.updated_at; g.recs.push(r); if (r.brand) { if (!(r.brand.id in g.brands)) { g.brands[r.brand.id] = 0; g.brandOrder.push(r.brand.id); } g.brands[r.brand.id] += r.quantity_number || 0; } }); const out = order.map((k) => gmap.get(k)!); if (sort === "asc") out.sort((a, b) => a.qty - b.qty); else if (sort === "desc") out.sort((a, b) => b.qty - a.qty); else out.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); return out; }, [inventory, visibleIds, filters, sort]); const groupTxs = (g: Group) => transactions .filter((t) => g.recs.some((r) => r.id === t.inventory_id)) .slice() .sort((a, b) => new Date(a.transaction_date).getTime() - new Date(b.transaction_date).getTime()); const brandTxs = (g: Group, bid: string) => transactions .filter((t) => t.brand?.id === bid && g.recs.some((r) => r.id === t.inventory_id)) .slice() .sort((a, b) => new Date(a.transaction_date).getTime() - new Date(b.transaction_date).getTime()); const hasFilter = !!(filters.warehouse || (isAdmin && filters.project)); if (loading) return
; if (!user || (!isAdmin && !isPM)) { return (
); } return (
} menu={} actions={
} /> {/* جعبه فیلترها و جستجو */}
setFilters((f) => ({ ...f, material: e.target.value }))} /> {filters.material && ( )}
{/* فیلتر سریع انبارها (چیپ‌های افقی لمسی) */} {visibleWarehouses.length > 1 && (
{visibleWarehouses.map((w) => ( ))}
)} {/* فیلترهای پیشرفته — برچسب‌های لمسی بدون سلکت و بدون بیرون‌زدگی */} {showMobileFilters && (
{/* پروژه‌ها (مخصوص ادمین) */} {isAdmin && lookup.projects.length > 0 && (
پروژه:
{lookup.projects.map((p) => ( ))}
)} {/* دکمه پاک کردن فیلترها */} {hasFilter && (
نمایش {faNum(groups.length)} قلم کالا
)}
)}
{/* ۱. نمای مخصوص موبایل: کارت‌های عمودی کاملاً ریسپانسیو */}
{!groups.length ? (
) : ( groups.map((g) => { const isExp = expandedKey === g.key; return ( p.id === warehouses.find((w) => w.id === g.whId)?.project_id)?.name || ""} unit={unitName(lookup.units, g.unitId)} onToggle={() => { setExpandedKey(isExp ? null : g.key); setSelTx(null); setExpBrand(null); }} timeline={ isExp ? ( brandName(lookup.brands, id)} whNameFn={(id) => whName(warehouses, id)} /> ) : null } /> ); }) )}
{/* ۲. نمای دسکتاپ: جدول سنتی */}
{!groups.length ? ( ) : ( {groups.map((g) => { const isExp = expandedKey === g.key; return ( p.id === warehouses.find((w) => w.id === g.whId)?.project_id)?.name || ""} unit={unitName(lookup.units, g.unitId)} onToggle={() => { setExpandedKey(isExp ? null : g.key); setSelTx(null); setExpBrand(null); }} timeline={ isExp ? ( brandName(lookup.brands, id)} whNameFn={(id) => whName(warehouses, id)} /> ) : null } /> ); })}
متریال دسته‌بندی انبار مشخصه فنی تعداد واحد آخرین به‌روزرسانی
)}
{/* دکمه شناور تغییر موجودی */} {isPM && ( )}
); } // کامپوننت کارت انبار در موبایل function InventoryMobileCard({ g, isExp, isAdmin, matName, wh, projName, unit, onToggle, timeline, }: { g: Group; isExp: boolean; isAdmin: boolean; matName: string; wh: string; projName: string; unit: string; onToggle: () => void; timeline: React.ReactNode; }) { const specsText = Object.entries(g.specs || {}).map(([k, v]) => `${k}: ${v}`).join(" · "); return (

{matName}

{/* کادر شاخص موجودی و انبار */}
موجودی انبار
{faNum(g.qty)} {unit}
محل نگهداری
{wh}
{isAdmin && projName && ( {projName} )}
{/* مشخصات فنی و تاریخ */}
{specsText ? ( {specsText} ) : ( — )}
{isoToJalaliStr(g.updatedAt)}
{isExp && (
{timeline}
)}
); } function GroupRow({ g, isExp, isAdmin, matName, wh, projName, unit, onToggle, timeline }: { g: Group; isExp: boolean; isAdmin: boolean; matName: string; wh: string; projName: string; unit: string; onToggle: () => void; timeline: React.ReactNode; }) { return ( <> {isExp ? "▾" : "▸"} {matName}
{wh}
{isAdmin && {projName}} {Object.entries(g.specs || {}).map(([k, v]) => `${k}: ${v}`).join(" · ") || "—"} {faNum(g.qty)} {unit} {isoToJalaliStr(g.updatedAt)} 🕘 {isExp && ( {timeline} )} ); } function Timeline({ g, txs, allTxs, brandTxs, selTx, setSelTx, expBrand, setExpBrand, matName, unit, brandNameFn, whNameFn }: { g: Group; txs: InventoryTransaction[]; allTxs: InventoryTransaction[]; brandTxs: (g: Group, bid: string) => InventoryTransaction[]; selTx: string | null; setSelTx: (id: string | null) => void; expBrand: string | null; setExpBrand: (id: string | null) => void; matName: string; unit: string; brandNameFn: (id: string | null | undefined) => string; whNameFn: (id: string | null | undefined) => string; }) { const track = (list: InventoryTransaction[]) => { let bal = 0; return list.map((t) => { const meta = TX_META[t.transaction_type] || { label: t.transaction_type, color: "#64748b", icon: "•" }; bal += t.quantity_number || 0; const sel = selTx === t.id; return ( ); }); }; if (!txs.length) return
تراکنشی ثبت نشده است.
; const brandSec = expBrand ? (
جریان برند: {brandNameFn(expBrand)} ({faNum(brandTxs(g, expBrand).length)} تراکنش)
{track(brandTxs(g, expBrand))}
) : null; const selDetail = selTx ? (() => { const t = allTxs.find((x) => x.id === selTx); if (!t) return null; const meta = TX_META[t.transaction_type] || { label: t.transaction_type, color: "#64748b", icon: "•" }; const pairTx = t.transfer_pair_id ? allTxs.find((x) => x.transfer_pair_id === t.transfer_pair_id && x.id !== t.id) : null; return (
{meta.label}
تاریخ و ساعت: {isoToJalaliStr(t.transaction_date)} {t.transaction_type === "TRANSFER_OUT" || t.transaction_type === "TRANSFER_IN" ? ( <> از انبار: {t.transaction_type === "TRANSFER_OUT" ? whNameFn(t.warehouse_id) : whNameFn(pairTx?.warehouse_id)} به انبار: {t.transaction_type === "TRANSFER_IN" ? whNameFn(t.warehouse_id) : whNameFn(pairTx?.warehouse_id)} ) : ( انبار: {whNameFn(t.warehouse_id)} )} {t.brand?.name && برند: {t.brand.name}} {t.performed_by && انجام‌دهنده: {t.performed_by}} {t.note && یادداشت: {t.note}} {t.transaction_type === "STOCK_COUNT" && t.recorded_qty != null && t.physical_qty != null && ( ثبت‌شده: {faNum(t.recorded_qty)} • شمارش‌شده: {faNum(t.physical_qty)} • اختلاف: {faNum(t.physical_qty - t.recorded_qty)} )}
); })() : null; return (
خط زمانی — {matName} {Object.entries(g.specs || {}).length > 0 && ( {Object.entries(g.specs).map(([k, v]) => `${k}: ${v}`).join(" · ")} )} {faNum(g.brandOrder.length)} برند
{g.brandOrder.map((bid) => ( ))}
{brandSec}
جریان کلی رکورد — ترکیب همه برندها ({faNum(txs.length)} تراکنش)
{track(txs)}
{selDetail}
); }