import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { Header, PageLayout } from "../../components/layout/Header"; import { EmptyState } from "../../components/EmptyState"; import { Badge } from "../../components/ui/badge"; import { Button } from "../../components/ui/button"; import { useAuthStore } from "../../store/auth"; import { inventorySubheader } from "./panelMenu"; import { apiV26 } from "../../lib/api"; import { refreshProcurementStore } from "../../hooks"; import type { PendingArrival, Warehouse } from "../../lib/types"; import { faNum, fmtDate } from "../../lib/formatters"; // کالاهای در راه — داشبورد متصل به سیستم تدارکات (مرحله رسید) // مدیر پروژه و مدیر تدارکات (ادمین) هر دو دسترسی دارند. // هر رکورد یک «سفارش» (پیشنهاد خرید) است؛ با «دریافت»، زیررکوردها (اقلام) // باز می‌شوند و برای هر قلم: تعداد رسیده + ردیف‌های تخصیص (تعداد + انبار پروژه) // تا مانده تخصیص‌نشده به صفر برسد. interface AllocRow { key: number; qty: string; whId: string; } interface ItemDraft { received: string; allocs: AllocRow[]; } export function ArrivalsPage() { const user = useAuthStore((s) => s.user); const isAdmin = user?.role === "admin" || user?.role === "procurement_manager"; const isPM = user?.role === "pm"; const isSupervisor = user?.role === "supervisor"; const canAccess = isAdmin || isPM || isSupervisor; const [items, setItems] = useState([]); const [warehouses, setWarehouses] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(null); // quoteId const [expanded, setExpanded] = useState>(new Set()); const [drafts, setDrafts] = useState>({}); const load = useCallback(async () => { try { const activeProject = (isPM || isSupervisor) ? user?.project_id || undefined : undefined; const [arr, whs] = await Promise.all([ // پروژه‌ی فعال صریحاً ارسال می‌شود؛ بک‌اند تخصیص PM / سرپرست را بررسی می‌کند apiV26.pm.onTheWay(activeProject), apiV26.warehouses.list(activeProject), ]); setItems(arr); setWarehouses(whs); } catch (err) { toast.error(err instanceof Error ? err.message : "خطا در دریافت کالاهای در راه"); } finally { setLoading(false); } }, [isPM, isSupervisor, user?.project_id]); useEffect(() => { if (!canAccess) return; setLoading(true); void load(); }, [canAccess, load]); // سفارش‌ها: گروه‌بندی اقلام به‌ازای هر پیشنهاد (فقط سفارش‌های دارای قلمِ در راه/ناقص) const orders = useMemo(() => { const map = new Map(); for (const it of items) { if (it.state === "arrived") continue; const list = map.get(it.quote_id) || []; list.push(it); map.set(it.quote_id, list); } return [...map.entries()]; }, [items]); const orderItems = (quoteId: string) => items.filter((i) => i.quote_id === quoteId); const whsOfProject = (projectId: string | null | undefined) => warehouses.filter((w) => String(w.project_id) === String(projectId)); // روزهای گذشته از تأیید (تصمیم خرید) — ستون «روز گذشته» const daysElapsed = (iso: string | null): number | null => { if (!iso) return null; const t = new Date(iso).getTime(); if (Number.isNaN(t)) return null; return Math.max(0, Math.floor((Date.now() - t) / 86400000)); }; const orderDaysElapsed = (its: PendingArrival[]): number | null => { const days = its.map((i) => daysElapsed(i.decided_at)).filter((d): d is number => d !== null); return days.length ? Math.min(...days) : null; }; const toggle = (quoteId: string) => { setExpanded((s) => { const next = new Set(s); if (next.has(quoteId)) next.delete(quoteId); else next.add(quoteId); return next; }); }; // ---------- draft helpers ---------- const draftOf = (it: PendingArrival): ItemDraft => drafts[it.item_decision_id] || { received: "", allocs: [{ key: Date.now(), qty: "", whId: "" }] }; const setReceived = (decisionId: string, received: string) => setDrafts((d) => ({ ...d, [decisionId]: { ...(d[decisionId] || { allocs: [] }), received } })); const addAlloc = (decisionId: string) => setDrafts((d) => { const cur = d[decisionId] || { received: "", allocs: [] }; return { ...d, [decisionId]: { ...cur, allocs: [...cur.allocs, { key: Date.now() + Math.random(), qty: "", whId: "" }] } }; }); const setAlloc = (decisionId: string, key: number, patch: Partial) => setDrafts((d) => { const cur = d[decisionId] || { received: "", allocs: [] }; return { ...d, [decisionId]: { ...cur, allocs: cur.allocs.map((a) => (a.key === key ? { ...a, ...patch } : a)) } }; }); const removeAlloc = (decisionId: string, key: number) => setDrafts((d) => { const cur = d[decisionId] || { received: "", allocs: [] }; return { ...d, [decisionId]: { ...cur, allocs: cur.allocs.filter((a) => a.key !== key) } }; }); const round4 = (n: number) => Math.round(n * 10000) / 10000; // مانده‌ی واقعیِ قابل‌دریافت برای این قلم: سفارش − قبلاً‌رسیده (− پیش‌نویس فعلی) const remainingToReceive = (it: PendingArrival): number => round4(parseFloat(it.ordered_qty) - parseFloat(it.received_qty)); const draftReceivedOf = (it: PendingArrival): number => parseFloat(draftOf(it).received) || 0; const allocSumOf = (it: PendingArrival): number => round4(draftOf(it).allocs.reduce((s, a) => s + ((parseFloat(a.qty) || 0) > 0 ? parseFloat(a.qty) || 0 : 0), 0)); // یک قلم برای ثبت «رسید جزیی» آماده است: تعداد > ۰، داخل سقف باقی‌مانده، // ردیف‌های تخصیص معتبر و مجموع تخصیص == تعداد رسیده. const itemSubmittable = (it: PendingArrival): boolean => { const d = draftOf(it); const received = draftReceivedOf(it); if (!(received > 0)) return false; if (received > remainingToReceive(it)) return false; // سقف: بیش از مانده‌ی سفارش نه if (d.allocs.some((a) => (parseFloat(a.qty) || 0) > 0 && !a.whId)) return false; return allocSumOf(it) === received; }; const itemHasErrors = (it: PendingArrival): { ceiling: boolean; sumMismatch: boolean; emptyWh: boolean } => { const d = draftOf(it); const received = draftReceivedOf(it); return { ceiling: received > remainingToReceive(it), sumMismatch: received > 0 && allocSumOf(it) !== received, emptyWh: d.allocs.some((a) => (parseFloat(a.qty) || 0) > 0 && !a.whId), }; }; // برای ثبت رسید، کافی است حداقل یک قلم مقدار معتبر داشته باشد؛ // قلم‌های بدون مقدار «در راه» می‌مانند (رسید جزیی مجاز است). const orderHasSubmittable = (quoteId: string): boolean => { const its = orderItems(quoteId); return its.length > 0 && its.some((it) => it.state !== "arrived" && itemSubmittable(it)); }; const orderHasErrors = (quoteId: string): boolean => orderItems(quoteId).some((it) => { const errs = itemHasErrors(it); return errs.ceiling || errs.sumMismatch || errs.emptyWh; }); // ---------- submit ---------- const submitOrder = async (quoteId: string) => { const its = orderItems(quoteId).filter((it) => it.state !== "arrived"); const submittable = its.filter(itemSubmittable); if (!submittable.length) { toast.error("حداقل یک قلم با تعداد معتبر (داخل مانده‌ی سفارش و تخصیصِ برابر با تعداد رسیده) وارد کنید."); return; } if (orderHasErrors(quoteId)) { toast.error("برخی قلم‌ها خطای اعتبارسنجی دارند — ابتدا سقف رسید و مجموع تخصیص انبارها را اصلاح کنید."); return; } setSaving(quoteId); try { for (const it of submittable) { const d = draftOf(it); const allocations = d.allocs .filter((a) => (parseFloat(a.qty) || 0) > 0 && a.whId) .map((a) => ({ warehouse_id: a.whId, quantity: parseFloat(a.qty) })); await apiV26.pm.confirmReceipt({ item_decision_id: it.item_decision_id, received_qty: draftReceivedOf(it), allocations, }, isPM ? user?.project_id || undefined : undefined); } toast.success("رسید (کامل یا جزیی) ثبت شد و به انبارها وارد شد ✓ — اقلام باقی‌مانده همچنان در راه هستند"); // همگام‌سازی با دفتر تدارکات: store پیشنهادها را تازه کن تا کارت‌های // پیشنهاد (بدون refresh دستی) رسید را نشان دهند. refreshProcurementStore(); setDrafts((d) => { const next = { ...d }; submittable.forEach((it) => delete next[it.item_decision_id]); return next; }); setExpanded((s) => { const next = new Set(s); next.delete(quoteId); return next; }); await load(); } catch (err) { toast.error(err instanceof Error ? err.message : "خطا در ثبت رسید"); } finally { setSaving(null); } }; if (!canAccess) { return (
); } return (
🚚 این بخش به مرحله «رسید» سفارش‌های خرید متصل است: تأیید پیشنهاد و پذیرش در سیستم تدارکات انجام شده و اینجا فقط رسید و تقسیم بین انبارها ثبت می‌شود. رسید جزیی (قلمی و مقداری) مجاز است — هر سفارش را باز کنید، تعداد واقعیِ رسیده هر قلم را وارد کنید و آن را بین انبارهای پروژه تقسیم کنید؛ قلم‌هایی که مقداری برایشان وارد نشود در صف «در راه» می‌مانند. مجموع تخصیص هر قلم باید دقیقاً با تعداد رسیده‌ی همان قلم برابر باشد و از مانده‌ی سفارش بیشتر نشود.
{loading ? (
در حال بارگذاری…
) : !orders.length ? ( ) : ( orders.map(([quoteId, its]) => { const first = its[0]; const open = expanded.has(quoteId); const hasSubmittable = orderHasSubmittable(quoteId); const hasErrors = orderHasErrors(quoteId); const anyPartial = its.some((it) => it.state === "partially_received" || draftReceivedOf(it) > 0); const days = orderDaysElapsed(its); return (
{/* سربرگ سفارش */}
🧾 {first.supplier.name} {faNum(its.length)} قلم {anyPartial ? "رسید جزیی" : "در راه"} {days !== null && ( = 7 ? "offered" : "muted"}> روز گذشته: {faNum(days)} روز )}
تاریخ پیشنهاد: {fmtDate(first.quote_date)} پروژه: {first.project?.name || "—"}
{/* اقلام سفارش */} {open && (
{its.map((it) => { const d = draftOf(it); const remaining = remainingToReceive(it); const draftReceived = draftReceivedOf(it); const inTransit = round4(remaining - draftReceived); const allocSum = allocSumOf(it); const errs = itemHasErrors(it); const submittable = itemSubmittable(it); const whs = whsOfProject(it.project?.id); const itemDays = daysElapsed(it.decided_at); return (
{it.material.name} سفارش: {faNum(parseFloat(it.ordered_qty))} {it.unit?.name_fa || it.unit?.code || ""} 0 || draftReceived > 0 ? "offered" : "outline"}> رسیده: {faNum(parseFloat(it.received_qty) + draftReceived)} / {faNum(parseFloat(it.ordered_qty))} 0 ? "muted" : "outline"}> در راه: {faNum(Math.max(0, inTransit))} {it.state === "partially_received" && رسید ناقص} {itemDays !== null && ( روز گذشته: {faNum(itemDays)} )}
setReceived(it.item_decision_id, e.target.value)} placeholder={String(remaining)} />
{d.allocs.map((a) => (
setAlloc(it.item_decision_id, a.key, { qty: e.target.value })} />
))}
{draftReceived > 0 ? (submittable ? "✓ آماده‌ی ثبت" : "در حال تکمیل…") : "بدون مقدار — در راه می‌ماند"} {errs.ceiling && ( تعداد رسیده نمی‌تواند از مانده‌ی سفارش ({faNum(remaining)}) بیشتر باشد )} {errs.sumMismatch && ( مجموع تخصیص انبارها ({faNum(allocSum)}) باید با تعداد رسیده ({faNum(draftReceived)}) برابر باشد )} {errs.emptyWh && ( برای ردیف‌های دارای تعداد، انبار را انتخاب کنید )}
); })}
رسید جزیی مجاز است: حداقل یک قلم با تعداد معتبر کافی است؛ قلم‌های بدون مقدار در «در راه» می‌مانند.
)}
); }) )}
); }