Files
ERP/frontend/src/pages/Inventory/ArrivalsPage.tsx
T

169 lines
7.3 KiB
TypeScript

import { useEffect, useMemo, useState } from "react";
import { useLocation } from "react-router-dom";
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 { PanelMenu } from "../../components/PanelMenu";
import { panelMenuItems, useLogout } from "./panelMenu";
import { apiV26 } from "../../lib/api";
import type { PendingArrival, Warehouse } from "../../lib/types";
import { faNum } from "../../lib/formatters";
// رسید کالاهای در راه — مدیر پروژه تأیید و بین انبارها تقسیم می‌کند (v2.6)
export function ArrivalsPage() {
const location = useLocation();
const logout = useLogout();
const user = useAuthStore((s) => s.user);
const [items, setItems] = useState<PendingArrival[]>([]);
const [warehouses, setWarehouses] = useState<Warehouse[]>([]);
const [selected, setSelected] = useState<Record<string, string[]>>({}); // decisionId -> warehouseIds
const [qty, setQty] = useState<Record<string, string>>({});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
useEffect(() => {
if (user?.role !== "pm") return;
let alive = true;
void (async () => {
try {
const [arr, whs] = await Promise.all([
apiV26.pm.onTheWay(),
apiV26.warehouses.list(user.project_id || undefined),
]);
if (!alive) return;
setItems(arr);
setWarehouses(whs);
} catch (err) {
if (alive) toast.error(err instanceof Error ? err.message : "خطا در دریافت کالاهای در راه");
} finally {
if (alive) setLoading(false);
}
})();
return () => {
alive = false;
};
}, [user?.role, user?.project_id]);
const pending = useMemo(() => items.filter((i) => i.state !== "arrived"), [items]);
const toggleWarehouse = (decisionId: string, whId: string) => {
setSelected((s) => {
const cur = s[decisionId] || [];
return { ...s, [decisionId]: cur.includes(whId) ? cur.filter((x) => x !== whId) : [...cur, whId] };
});
};
const confirm = async (it: PendingArrival) => {
const qtyStr = qty[it.item_decision_id]?.trim();
const total = parseFloat(qtyStr || "");
if (!total || total <= 0) {
toast.error("تعداد رسیده را وارد کنید.");
return;
}
const allocIds = selected[it.item_decision_id] || [];
if (!allocIds.length) {
toast.error("حداقل یک انبار را برای تقسیم انتخاب کنید.");
return;
}
// تقسیم مساوی بین انبارهای انتخاب‌شده (به‌صورت پیش‌فرض؛ کاربر می‌تواند بعداً اصلاح کند)
const per = Math.round((total / allocIds.length) * 10000) / 10000;
const allocations = allocIds.map((whId, idx) => ({
warehouse_id: whId,
quantity: idx === allocIds.length - 1 ? +(total - per * (allocIds.length - 1)).toFixed(4) : per,
}));
setSaving(true);
try {
const res = await apiV26.pm.confirmReceipt({
item_decision_id: it.item_decision_id,
received_qty: total,
allocations,
});
toast.success(`رسید ثبت شد — ${res.posted_transactions.length} تراکنش خرید ثبت شد.`);
setItems((list) => list.filter((x) => x.item_decision_id !== it.item_decision_id));
} catch (err) {
toast.error(err instanceof Error ? err.message : "خطا در ثبت رسید");
} finally {
setSaving(false);
}
};
if (loading) return <div className="min-h-screen" />;
if (user?.role !== "pm") {
return (
<div className="min-h-screen">
<Header title="رسید کالاها" menu={<PanelMenu title="پنل مدیریت انبار" items={panelMenuItems(location.pathname, user?.role, logout)} />} />
<PageLayout>
<EmptyState emoji="🔒" text="این بخش فقط برای مدیر پروژه است." />
</PageLayout>
</div>
);
}
return (
<div className="min-h-screen">
<Header
title="کالاهای در راه و رسید"
sub={user.project_name || ""}
menu={<PanelMenu title="پنل مدیریت انبار" items={panelMenuItems(location.pathname, user?.role, logout)} />}
/>
<PageLayout className="flex flex-col gap-4">
{!pending.length ? (
<EmptyState emoji="🚚" text="کالای در راهی برای تأیید رسید وجود ندارد." />
) : (
pending.map((it) => (
<div key={it.item_decision_id} className="rounded-xl border border-line bg-panel p-4 shadow-sm">
<div className="flex flex-wrap items-center gap-2">
<span className="text-[14px] font-bold">{it.material.name}</span>
<Badge variant="outline">
{it.state === "on_the_way" ? "در راه" : "رسید ناقص"}
</Badge>
<span className="text-[12px] text-muted">
سفارش: {faNum(parseFloat(it.ordered_qty))} {it.unit?.name_fa || it.unit?.code || ""} — رسیده: {faNum(parseFloat(it.received_qty))}
</span>
</div>
<div className="mt-3 grid gap-3 md:grid-cols-2">
<div className="field">
<label>تعداد رسیده (در واحد قلم)</label>
<input
type="number"
className="h-9 w-full rounded-lg border border-line bg-panel px-3 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
value={qty[it.item_decision_id] || ""}
onChange={(e) => setQty((q) => ({ ...q, [it.item_decision_id]: e.target.value }))}
placeholder={it.ordered_qty}
/>
</div>
<div className="field">
<label>تقسیم بین انبارها (چندتایی)</label>
<div className="flex flex-wrap gap-1.5 pt-1">
{warehouses.map((w) => {
const on = (selected[it.item_decision_id] || []).includes(w.id);
return (
<button
key={w.id}
type="button"
className={`rounded-lg border px-3 py-1.5 text-[12.5px] ${on ? "border-accent bg-accent/10 text-accent" : "border-line bg-panel text-muted"}`}
onClick={() => toggleWarehouse(it.item_decision_id, w.id)}
>
{w.name}
</button>
);
})}
</div>
</div>
</div>
<div className="mt-3 flex justify-end">
<Button size="sm" variant="green" disabled={saving} onClick={() => void confirm(it)}>
ثبت رسید و ورود به انبار
</Button>
</div>
</div>
))
)}
</PageLayout>
</div>
);
}