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:
@@ -1,10 +1,11 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { Warehouse as WarehouseIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Header, PageLayout } from "../../components/layout/Header";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { PanelMenu } from "../../components/PanelMenu";
|
||||
import { panelMenuItems, accountMenuItems, useLogout } from "./panelMenu";
|
||||
import { accountMenuItems, inventorySubheader, useLogout } from "./panelMenu";
|
||||
import { InventoryTopNav } from "./InventoryTopNav";
|
||||
import { apiV26, type UserRow } from "../../lib/api";
|
||||
import { useInventoryData } from "./useInventoryData";
|
||||
@@ -28,18 +29,16 @@ export function OrganizationPage() {
|
||||
|
||||
const [users, setUsers] = useState<UserRow[]>([]);
|
||||
const [loadingUsers, setLoadingUsers] = useState(true);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
|
||||
const [newWh, setNewWh] = useState<Record<string, string>>({});
|
||||
const [newWhDesc, setNewWhDesc] = useState<Record<string, string>>({});
|
||||
|
||||
// کاربر جدید (تعریف مدیر پروژه)
|
||||
const [newUserFirst, setNewUserFirst] = useState("");
|
||||
const [newUserLast, setNewUserLast] = useState("");
|
||||
const [newUserCasdoor, setNewUserCasdoor] = useState("");
|
||||
const [newUserRole, setNewUserRole] = useState<"pm" | "procurement_manager">("pm");
|
||||
|
||||
const canManage = user?.role === "admin" || user?.role === "procurement_manager";
|
||||
// فقط ادمین میتواند نقشها را تغییر دهد/ارتقا دهد (RBAC) — مدیر تدارکات فقط
|
||||
// پروژهی مدیران پروژه را تعیین/جابهجا میکند.
|
||||
const canChangeRoles = user?.role === "admin";
|
||||
|
||||
const loadUsers = useCallback(async () => {
|
||||
try {
|
||||
@@ -56,10 +55,28 @@ export function OrganizationPage() {
|
||||
if (canManage) void loadUsers();
|
||||
}, [canManage, loadUsers]);
|
||||
|
||||
const syncUsers = async () => {
|
||||
setSyncing(true);
|
||||
try {
|
||||
const { users: synced, created } = await apiV26.users.sync();
|
||||
setUsers(synced);
|
||||
toast.success(
|
||||
created > 0
|
||||
? `همگامسازی انجام شد — ${faNum(created)} کاربر جدید از Casdoor اضافه شد ✓`
|
||||
: "همگامسازی انجام شد — کاربر جدیدی یافت نشد",
|
||||
);
|
||||
await data.refresh();
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "خطا در همگامسازی با Casdoor");
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!canManage) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Header title="سازماندهی" menu={<PanelMenu title="پنل مدیریت انبار" items={panelMenuItems(location.pathname, user?.role, logout)} />} />
|
||||
<Header title="سازماندهی" menu={<PanelMenu title="پنل مدیریت انبار" items={accountMenuItems(logout)} />} />
|
||||
<PageLayout>
|
||||
<div className="text-[13px] text-muted">این بخش فقط برای ادمین / مدیر تدارکات است.</div>
|
||||
</PageLayout>
|
||||
@@ -69,26 +86,6 @@ export function OrganizationPage() {
|
||||
|
||||
// ---------- actions: users ----------
|
||||
|
||||
const addUser = async () => {
|
||||
const first = newUserFirst.trim();
|
||||
if (!first) return toast.error("نام کاربر الزامی است");
|
||||
try {
|
||||
await apiV26.users.create({
|
||||
first_name: first,
|
||||
last_name: newUserLast.trim() || "",
|
||||
casdoor_id: newUserCasdoor.trim() || null,
|
||||
role: newUserRole,
|
||||
});
|
||||
toast.success(`کاربر «${first}» تعریف شد ✓`);
|
||||
setNewUserFirst("");
|
||||
setNewUserLast("");
|
||||
setNewUserCasdoor("");
|
||||
await loadUsers();
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "خطا در تعریف کاربر");
|
||||
}
|
||||
};
|
||||
|
||||
const changeRole = async (u: UserRow, role: Role) => {
|
||||
setBusyId(u.id);
|
||||
try {
|
||||
@@ -160,62 +157,30 @@ export function OrganizationPage() {
|
||||
<div className="min-h-screen">
|
||||
<Header
|
||||
title="سازماندهی"
|
||||
sub="ادمین / مدیر تدارکات"
|
||||
sub={inventorySubheader(user)}
|
||||
backTo="/inventory"
|
||||
backLabel="بازگشت به داشبورد"
|
||||
icon={<WarehouseIcon className="h-4.5 w-4.5" strokeWidth={2} />}
|
||||
menu={<PanelMenu title="پنل مدیریت انبار" items={accountMenuItems(logout)} />}
|
||||
actions={<InventoryTopNav pathname={location.pathname} onLogout={logout} variant="admin" />}
|
||||
actions={<InventoryTopNav pathname={location.pathname} variant="admin" />}
|
||||
/>
|
||||
<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">
|
||||
ℹ️ پروژهها از سیستم تدارکات میآیند. اینجا مشخص میکنید هر پروژه با کدام مدیر و انبار(ها) کار میکند؛
|
||||
ℹ️ کاربران از Casdoor همگام میشوند و نقشها اینجا (فقط توسط ادمین) تعیین میشوند؛
|
||||
مدیر تدارکات فقط پروژهی مدیران پروژه را تعیین/جابهجا میکند. پروژهها از سیستم تدارکات میآیند و
|
||||
هر مدیر پروژه به همه انبارهای پروژه خودش دسترسی دارد. همه تغییرات فوراً در داشبورد و فرمها اعمال میشود.
|
||||
</div>
|
||||
|
||||
{/* ---------- کاربران و مدیران پروژه ---------- */}
|
||||
<div className="rounded-xl border border-line bg-panel p-4">
|
||||
<h3 className="mb-2 text-[14px] font-bold">👥 کاربران و مدیران پروژه</h3>
|
||||
|
||||
{/* تعریف کاربر جدید */}
|
||||
<div className="mb-3 flex flex-wrap items-end gap-2 rounded-lg border border-dashed border-accent/40 bg-accent/5 p-3">
|
||||
<div className="field mb-0">
|
||||
<label>نام *</label>
|
||||
<input
|
||||
className="h-8 w-36 rounded-lg border border-line bg-panel px-2 text-sm"
|
||||
placeholder="مثلاً رضایی"
|
||||
value={newUserFirst}
|
||||
onChange={(e) => setNewUserFirst(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="field mb-0">
|
||||
<label>نام خانوادگی</label>
|
||||
<input
|
||||
className="h-8 w-36 rounded-lg border border-line bg-panel px-2 text-sm"
|
||||
placeholder="(اختیاری)"
|
||||
value={newUserLast}
|
||||
onChange={(e) => setNewUserLast(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="field mb-0">
|
||||
<label>نقش</label>
|
||||
<select
|
||||
className="h-8 rounded-lg border border-line bg-panel px-2 text-sm"
|
||||
value={newUserRole ?? ""}
|
||||
onChange={(e) => setNewUserRole(e.target.value as "pm" | "procurement_manager")}
|
||||
>
|
||||
<option value="pm">مدیر پروژه</option>
|
||||
<option value="procurement_manager">مدیر تدارکات</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="field mb-0">
|
||||
<label>شناسه Casdoor</label>
|
||||
<input
|
||||
className="h-8 w-44 rounded-lg border border-line bg-panel px-2 text-sm ltr"
|
||||
placeholder="(اختیاری — برای کاربر واقعی)"
|
||||
value={newUserCasdoor}
|
||||
onChange={(e) => setNewUserCasdoor(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button className="rounded-lg bg-accent px-3 py-1.5 text-[12px] font-bold text-white" onClick={() => void addUser()}>
|
||||
+ تعریف کاربر
|
||||
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<h3 className="text-[14px] font-bold">👥 کاربران و مدیران پروژه</h3>
|
||||
<button
|
||||
onClick={() => void syncUsers()}
|
||||
disabled={syncing}
|
||||
className="rounded-lg border border-accent/40 bg-accent/5 px-3 py-1.5 text-[12px] font-bold text-accent disabled:opacity-50"
|
||||
>
|
||||
{syncing ? "در حال همگامسازی…" : "🔄 همگامسازی از Casdoor"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -224,7 +189,7 @@ export function OrganizationPage() {
|
||||
) : !users.length ? (
|
||||
<div className="rounded-lg border border-dashed border-line px-3 py-3 text-[12px] text-muted">
|
||||
کاربری ثبت نشده است — کاربران پس از اولین ورود (Casdoor) بهصورت خودکار ساخته میشوند؛
|
||||
یا از فرم بالا یک مدیر پروژه تعریف کنید.
|
||||
یا دکمهی «همگامسازی از Casdoor» را بزنید.
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
@@ -245,17 +210,26 @@ export function OrganizationPage() {
|
||||
<small className="block text-[10.5px] text-muted ltr">{u.casdoor_id}</small>
|
||||
</td>
|
||||
<td className="py-1.5 pe-3">
|
||||
<select
|
||||
className="h-7 rounded-lg border border-line bg-panel px-2 text-[12px]"
|
||||
value={u.role ?? ""}
|
||||
disabled={busyId === u.id}
|
||||
onChange={(e) => void changeRole(u, (e.target.value || null) as Role)}
|
||||
>
|
||||
<option value="">— بدون نقش —</option>
|
||||
<option value="pm">مدیر پروژه</option>
|
||||
<option value="procurement_manager">مدیر تدارکات</option>
|
||||
<option value="admin">ادمین</option>
|
||||
</select>
|
||||
{canChangeRoles ? (
|
||||
<select
|
||||
className="h-7 rounded-lg border border-line bg-panel px-2 text-[12px]"
|
||||
value={u.role ?? ""}
|
||||
disabled={busyId === u.id}
|
||||
onChange={(e) => void changeRole(u, (e.target.value || null) as Role)}
|
||||
>
|
||||
<option value="">— بدون نقش —</option>
|
||||
<option value="pm">مدیر پروژه</option>
|
||||
<option value="procurement_manager">مدیر تدارکات</option>
|
||||
<option value="admin">ادمین</option>
|
||||
</select>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
{!canChangeRoles && u.role && (
|
||||
<small className="block text-[10px] text-muted">(تغییر نقش فقط توسط ادمین)</small>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-1.5 pe-3">
|
||||
{u.role === "pm" ? (
|
||||
|
||||
Reference in New Issue
Block a user