feat(frontend): unified warehouse UI, calendar fix, partial receipts, RBAC views, admin panel, dev-mode 3-step
- header: icon box + dynamic subheader '[user] | [role] – [project/همه پروژهها]' on every warehouse page; module context kept in the hamburger (items 1-2) - project manager view: office button + inline logout removed; hamburger contains only «خروج از حساب»; sub-views get «بازگشت به داشبورد» (Header backTo/goBack); «افزایش موجود» primary button on the dashboard (item 8) - daily control: cells render the day-of-month (was the Jalali year), month nav uses real Jalali months, header is pure Jalali «امروز ۸ شهریور ۱۴۰۵», today gets a blue accent (item 3) - arrivals: partial (item/qty) receipt — any valid item is enough to submit, ceiling + allocation-sum validation with inline errors, Received/In-Transit badges; ledger shows «رسیده X از Y» / «در راه Z» + «رسید جزیی» badge (item 4) - api client: multipart uploads no longer send Content-Type json (fixes the invoice/proforma 400 «دادهی ارسالشده نامعتبر است»); undefined query params are dropped (fixes empty admin project filter, item 5) - organization: user-creation card removed, role column read-only for procurement manager, Casdoor sync button (item 6) - admin panel (/admin): Casdoor user sync + grant/revoke «مدیر تدارکات»; Developer Mode is a 3-step admin-gated flow on the login page (item 7)
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ShieldCheck, Warehouse as WarehouseIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Header, PageLayout } from "../../components/layout/Header";
|
||||
import { EmptyState } from "../../components/EmptyState";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { PanelMenu } from "../../components/PanelMenu";
|
||||
import { accountMenuItems, inventorySubheader, useLogout } from "../Inventory/panelMenu";
|
||||
import { apiV26, type UserRow } from "../../lib/api";
|
||||
import { faNum } from "../../lib/formatters";
|
||||
|
||||
const ROLE_LABELS: Record<string, string> = {
|
||||
pm: "مدیر پروژه",
|
||||
procurement_manager: "مدیر تدارکات",
|
||||
admin: "ادمین",
|
||||
};
|
||||
|
||||
// پنل مدیریت ادمین (فقط ادمین) — همگامسازی کاربران از Casdoor و
|
||||
// اعطا/لغو نقش «مدیر تدارکات». تخصیص پروژه در این پنل نیست
|
||||
// (مدیریت پروژهها در «سازماندهی» توسط مدیر تدارکات انجام میشود).
|
||||
export function AdminPanel() {
|
||||
const navigate = useNavigate();
|
||||
const logout = useLogout();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
|
||||
const [users, setUsers] = useState<UserRow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
|
||||
const isAdmin = user?.role === "admin";
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
setUsers(await apiV26.users.list());
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "خطا در دریافت کاربران");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAdmin) void load();
|
||||
}, [isAdmin, load]);
|
||||
|
||||
const sync = async () => {
|
||||
setSyncing(true);
|
||||
try {
|
||||
const { users: synced, created } = await apiV26.users.sync();
|
||||
setUsers(synced);
|
||||
toast.success(
|
||||
created > 0 ? `همگامسازی انجام شد — ${faNum(created)} کاربر جدید از Casdoor اضافه شد ✓` : "همگامسازی انجام شد — کاربر جدیدی یافت نشد",
|
||||
);
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "خطا در همگامسازی با Casdoor");
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleProcurementManager = async (u: UserRow) => {
|
||||
setBusyId(u.id);
|
||||
const target = u.role === "procurement_manager" ? null : "procurement_manager";
|
||||
try {
|
||||
await apiV26.users.updateRole(u.id, target as "procurement_manager" | null);
|
||||
toast.success(
|
||||
target === "procurement_manager"
|
||||
? `«${u.first_name} ${u.last_name}» مدیر تدارکات شد ✓`
|
||||
: `نقش مدیر تدارکات از «${u.first_name} ${u.last_name}» گرفته شد`,
|
||||
);
|
||||
await load();
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "خطا در تغییر نقش");
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isAdmin) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Header title="پنل مدیریت ادمین" menu={<PanelMenu title="پنل مدیریت انبار" items={accountMenuItems(logout)} />} />
|
||||
<PageLayout>
|
||||
<EmptyState emoji="🔒" text="این پنل فقط برای ادمین (سوپر یوزر) است." />
|
||||
</PageLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Header
|
||||
title="پنل مدیریت ادمین"
|
||||
sub={inventorySubheader(user)}
|
||||
backTo="/inventory"
|
||||
backLabel="بازگشت به داشبورد انبار"
|
||||
icon={<ShieldCheck className="h-4.5 w-4.5" strokeWidth={2} />}
|
||||
menu={<PanelMenu title="پنل مدیریت انبار" items={accountMenuItems(logout)} />}
|
||||
actions={
|
||||
<button
|
||||
onClick={() => void sync()}
|
||||
disabled={syncing}
|
||||
className="btn-line-file cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
{syncing ? "در حال همگامسازی…" : "🔄 همگامسازی از Casdoor"}
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
<PageLayout className="flex flex-col gap-3">
|
||||
<div className="rounded-lg border border-blue/20 bg-blue/5 px-3 py-2 text-[12px] leading-6 text-muted">
|
||||
🛡️ وظیفهی اصلی ادمین، مدیریت مدیران تدارکات است: با دکمهی هر کاربر میتوانید نقش
|
||||
«مدیر تدارکات» را اعطا یا لغو کنید. تخصیص پروژه به مدیران پروژه در بخش «سازماندهی» انجام میشود
|
||||
(این پنل فقط نقشها را مدیریت میکند).
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="py-6 text-center text-[12.5px] text-muted">در حال بارگذاری کاربران…</div>
|
||||
) : !users.length ? (
|
||||
<EmptyState emoji="👥" text="کاربری یافت نشد — دکمهی «همگامسازی از Casdoor» را بزنید." />
|
||||
) : (
|
||||
<div className="overflow-hidden rounded-xl border border-line bg-panel">
|
||||
<table className="w-full text-[12.5px]">
|
||||
<thead>
|
||||
<tr className="border-b border-line bg-panel text-right text-muted">
|
||||
<th className="py-2 pe-3">نام کامل</th>
|
||||
<th className="py-2 pe-3">شناسه Casdoor</th>
|
||||
<th className="py-2 pe-3">نقش فعلی</th>
|
||||
<th className="py-2">مدیریت مدیر تدارکات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((u) => {
|
||||
const isPMgr = u.role === "procurement_manager";
|
||||
return (
|
||||
<tr key={u.id} className="border-b border-line/50">
|
||||
<td className="py-2 pe-3 font-semibold">
|
||||
{u.first_name} {u.last_name}
|
||||
</td>
|
||||
<td className="py-2 pe-3 text-muted ltr">{u.casdoor_id}</td>
|
||||
<td className="py-2 pe-3">
|
||||
<span className={`rounded-lg px-2 py-1 text-[11px] font-bold ${u.role ? "bg-accent-bg text-accent" : "bg-slate-bg text-muted"}`}>
|
||||
{u.role ? ROLE_LABELS[u.role] : "بدون نقش"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2">
|
||||
<button
|
||||
disabled={busyId === u.id}
|
||||
onClick={() => void toggleProcurementManager(u)}
|
||||
className={`rounded-lg border px-3 py-1 text-[11.5px] font-bold ${
|
||||
isPMgr
|
||||
? "border-red/30 bg-red/5 text-red hover:bg-red/10"
|
||||
: "border-green/30 bg-green/5 text-green hover:bg-green/10"
|
||||
} disabled:opacity-50`}
|
||||
>
|
||||
{busyId === u.id ? "…" : isPMgr ? "لغو نقش مدیر تدارکات" : "اعطای نقش مدیر تدارکات"}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="border-t border-line bg-panel px-4 py-2 text-[11px] text-muted">
|
||||
{faNum(users.length)} کاربر — نقشها فقط توسط ادمین قابل تغییرند (RBAC).
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => navigate("/inventory")}
|
||||
className="flex w-fit cursor-pointer items-center gap-1.5 rounded-lg border border-line bg-panel px-3 py-1.5 text-[12px] text-muted hover:bg-slate-bg hover:text-ink"
|
||||
>
|
||||
<WarehouseIcon className="h-3.5 w-3.5" />
|
||||
بازگشت به داشبورد انبار
|
||||
</button>
|
||||
</PageLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user