169 lines
7.3 KiB
TypeScript
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>
|
|
);
|
|
}
|