- shared AppSwitcherMenu ('پنلها' title, active filled item with ✓ فعلی,
«مدیریت انبار»/«مدیریت تدارکات» switcher, divider + خروج از حساب) used by
both the procurement dashboard and the warehouse hub (PM variant keeps
logout-only); replaced the old raw «بازگشت به دفتر» link
- warehouse header mirrors the procurement pattern: icon + hamburger only
on the hub dashboard; sub-views (تغییر موجودی، کنترل روزانه، انبارها،
کالاهای در راه، سازماندهی) render a minimal header with just the title
and «بازگشت به داشبورد»
- removed the self-referencing «داشبورد انبار» tab from the hub strip
- removed the red per-day transaction-count badges from the daily-control
calendar (backend untouched — purely visual)
- removed the test subtitle «دادهها در پایگاه داده ذخیره میشوند» from the
procurement dashboard header
193 lines
9.0 KiB
TypeScript
193 lines
9.0 KiB
TypeScript
import { useState } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { toast } from "sonner";
|
|
import { Header, PageLayout } from "../../components/layout/Header";
|
|
import { useAuthStore } from "../../store/auth";
|
|
import { inventorySubheader } from "./panelMenu";
|
|
import { apiV26 } from "../../lib/api";
|
|
import { useInventoryData, projName } from "./useInventoryData";
|
|
import { faNum } from "../../lib/formatters";
|
|
import { isoToJalaliStr } from "../../lib/jalali";
|
|
|
|
// انبارها — مدیر پروژه: مشاهده انبارهای پروژه خودش + افزودن/ویرایش/حذف (ادمین دسترسی ندارد)
|
|
export function WarehousesPage() {
|
|
const navigate = useNavigate();
|
|
const user = useAuthStore((s) => s.user);
|
|
const data = useInventoryData();
|
|
const isAdmin = user?.role === "admin" || user?.role === "procurement_manager";
|
|
const isPM = user?.role === "pm";
|
|
|
|
const [newName, setNewName] = useState("");
|
|
const [newDesc, setNewDesc] = useState("");
|
|
const [editId, setEditId] = useState<string | null>(null);
|
|
const [editName, setEditName] = useState("");
|
|
const [editDesc, setEditDesc] = useState("");
|
|
|
|
// ادمین / مدیر تدارکات به بخش «انبارها» دسترسی ندارد — مدیریت انبارها از «سازماندهی»
|
|
if (isAdmin) {
|
|
return (
|
|
<div className="min-h-screen">
|
|
<Header title="انبارها" backTo="/inventory" backLabel="بازگشت به داشبورد" />
|
|
<PageLayout>
|
|
<div className="rounded-xl border border-line bg-panel p-8 text-center">
|
|
<div className="text-3xl">🔒</div>
|
|
<h3 className="mt-2 text-[15px] font-bold">دسترسی ندارد</h3>
|
|
<p className="mt-1 text-[12.5px] text-muted">
|
|
بخش «انبارها» مخصوص مدیران پروژه است. مدیریت انبارها از بخش «سازماندهی» انجام میشود.
|
|
</p>
|
|
</div>
|
|
</PageLayout>
|
|
</div>
|
|
);
|
|
}
|
|
if (!isPM) {
|
|
return (
|
|
<div className="min-h-screen">
|
|
<Header title="انبارها" backTo="/inventory" backLabel="بازگشت به داشبورد" />
|
|
<PageLayout>
|
|
<div className="text-[13px] text-muted">این بخش فقط برای مدیر پروژه است.</div>
|
|
</PageLayout>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const addWarehouse = async () => {
|
|
const name = newName.trim();
|
|
if (!name) return toast.error("نام انبار الزامی است");
|
|
const projectId = user?.project_id;
|
|
if (!projectId) return toast.error("پروژه انبار را انتخاب کنید");
|
|
try {
|
|
await apiV26.warehouses.create({ project_id: projectId, name, description: newDesc.trim() || null });
|
|
setNewName("");
|
|
setNewDesc("");
|
|
toast.success(`انبار «${name}» اضافه شد ✓`);
|
|
await data.refresh();
|
|
} catch (err) {
|
|
toast.error(err instanceof Error ? err.message : "خطا در افزودن انبار");
|
|
}
|
|
};
|
|
|
|
const saveEdit = async (id: string) => {
|
|
if (!editName.trim()) return toast.error("نام انبار نمیتواند خالی باشد");
|
|
try {
|
|
await apiV26.warehouses.update(id, { name: editName.trim(), description: editDesc.trim() || null });
|
|
setEditId(null);
|
|
toast.success("تغییرات ذخیره شد");
|
|
await data.refresh();
|
|
} catch (err) {
|
|
toast.error(err instanceof Error ? err.message : "خطا در ذخیره");
|
|
}
|
|
};
|
|
|
|
const removeWarehouse = async (id: string) => {
|
|
if (!confirm("انبار حذف شود؟ (انبارهای دارای سابقه تراکنش قابل حذف نیستند)")) return;
|
|
try {
|
|
await apiV26.warehouses.delete(id);
|
|
toast.success("انبار حذف شد");
|
|
await data.refresh();
|
|
} catch (err) {
|
|
toast.error(err instanceof Error ? err.message : "خطا در حذف انبار");
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen">
|
|
<Header
|
|
title="انبارها"
|
|
sub={inventorySubheader(user)}
|
|
backTo="/inventory"
|
|
backLabel="بازگشت به داشبورد"
|
|
/>
|
|
<PageLayout className="flex flex-col gap-3">
|
|
{isPM && (
|
|
<div className="rounded-xl border border-line bg-panel p-3">
|
|
<h3 className="mb-2 text-[13px] font-bold">+ افزودن انبار جدید</h3>
|
|
<div className="flex flex-wrap gap-2">
|
|
<input
|
|
className="h-9 w-52 rounded-lg border border-line bg-panel px-3 text-sm"
|
|
placeholder="نام انبار (مثلاً انبار موقت)"
|
|
value={newName}
|
|
onChange={(e) => setNewName(e.target.value)}
|
|
/>
|
|
<input
|
|
className="h-9 w-64 rounded-lg border border-line bg-panel px-3 text-sm"
|
|
placeholder="توضیحات (اختیاری)"
|
|
value={newDesc}
|
|
onChange={(e) => setNewDesc(e.target.value)}
|
|
/>
|
|
<button className="rounded-lg bg-accent px-4 py-2 text-[12.5px] font-bold text-white" onClick={() => void addWarehouse()}>
|
|
+ افزودن
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
|
|
{data.warehouses.map((w) => {
|
|
const cnt = data.inventory.filter((r) => r.warehouse?.id === w.id).length;
|
|
const txc = data.transactions.filter((t) => t.warehouse_id === w.id).length;
|
|
return (
|
|
<div
|
|
key={w.id}
|
|
className="cursor-pointer rounded-xl border border-line bg-panel p-4 transition-colors hover:border-accent/50 hover:bg-accent/5"
|
|
onClick={() => navigate(`/inventory?warehouse=${w.id}`)}
|
|
title="ورود به داشبورد این انبار (فیلترشده)"
|
|
>
|
|
<h3 className="text-[14px] font-bold">🏬 {w.name}</h3>
|
|
<div className="mt-0.5 text-[11.5px] text-muted">{w.description || "—"}</div>
|
|
<div className="mt-2 flex flex-col gap-1 text-[12px] text-muted">
|
|
<span>پروژه: <b className="text-foreground">{projName(data.lookup.projects, w.project_id)}</b></span>
|
|
<span>اقلام موجود: <b className="text-foreground">{faNum(cnt)}</b></span>
|
|
<span>تراکنشها: <b className="text-foreground">{faNum(txc)}</b></span>
|
|
<span>تاریخ ایجاد: <b className="text-foreground">{isoToJalaliStr(w.created_at)}</b></span>
|
|
</div>
|
|
<div className="mt-3 flex items-center gap-1.5 border-t border-line pt-2">
|
|
<button
|
|
className="rounded-lg border border-accent/40 bg-accent/5 px-3 py-1 text-[11.5px] font-bold text-accent"
|
|
onClick={(e) => { e.stopPropagation(); navigate(`/inventory?warehouse=${w.id}`); }}
|
|
>
|
|
📊 مشاهده داشبورد انبار
|
|
</button>
|
|
{isPM && (
|
|
<>
|
|
<button
|
|
className="rounded-lg border border-line px-3 py-1 text-[11.5px]"
|
|
onClick={(e) => { e.stopPropagation(); setEditId(w.id); setEditName(w.name); setEditDesc(w.description || ""); }}
|
|
>
|
|
✏️ ویرایش
|
|
</button>
|
|
<button className="rounded-lg border border-red/30 px-3 py-1 text-[11.5px] text-red" onClick={(e) => { e.stopPropagation(); void removeWarehouse(w.id); }}>
|
|
✕ حذف
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
{editId === w.id && isPM && (
|
|
<div className="mt-2 flex flex-col gap-1.5 rounded-lg border border-dashed border-accent/40 p-2" onClick={(e) => e.stopPropagation()}>
|
|
<input
|
|
className="h-8 rounded-lg border border-line bg-panel px-2 text-sm"
|
|
placeholder="نام انبار"
|
|
value={editName}
|
|
onChange={(e) => setEditName(e.target.value)}
|
|
/>
|
|
<input
|
|
className="h-8 rounded-lg border border-line bg-panel px-2 text-sm"
|
|
placeholder="توضیحات"
|
|
value={editDesc}
|
|
onChange={(e) => setEditDesc(e.target.value)}
|
|
/>
|
|
<div className="flex gap-1.5">
|
|
<button className="rounded-lg bg-accent px-3 py-1 text-[11.5px] font-bold text-white" onClick={() => void saveEdit(w.id)}>ذخیره</button>
|
|
<button className="rounded-lg border border-line px-3 py-1 text-[11.5px]" onClick={() => setEditId(null)}>انصراف</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</PageLayout>
|
|
</div>
|
|
);
|
|
}
|