feat(org): panel-based access system replacing roles (procurement/inventory)
- new tables user_panels (panel: procurement/inventory/pm + level full/limited)
and user_project_members (M:N membership, separate from pm responsibility)
- UserContext carries panels + member_project_ids; /users/me and the users
list include them
- admin endpoints: PUT /users/{id}/panels, PUT /users/{id}/panel-projects
- inventory scope: panel inventory 'full' -> manager scope (all projects, NO
stock change); 'limited' -> supervisor scope (own memberships, stock change
allowed); legacy roles kept as migration fallback; admin untouched
- procurement writes: _procurement_writer is now panel-aware (admin, legacy
procurement_manager, or procurement 'full') across all 18 write routers;
quote CREATE additionally allows 'limited' users restricted to their member
projects (validated server-side)
- frontend: panel-aware HomeRoute (inventory -> warehouse panel, procurement
limited -> minimal RequesterHome), wizard project options restricted for
limited users, change-inventory button/route gated to inventory 'limited',
organization panel gains the panels & access-levels management section
This commit is contained in:
@@ -122,6 +122,41 @@ export function OrganizationPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// ---------- لایه پنلها: تخصیص پنل + سطح دسترسی و عضویت پروژهای ----------
|
||||
const setPanel = async (u: UserRow, panel: "procurement" | "inventory", level: "" | "full" | "limited") => {
|
||||
setBusyId(u.id);
|
||||
const next = { ...(u.panels ?? {}) };
|
||||
if (level) next[panel] = level;
|
||||
else delete next[panel];
|
||||
try {
|
||||
await apiV26.users.setPanels(u.id, next);
|
||||
toast.success(`دسترسی «${u.first_name} ${u.last_name}» بهروزرسانی شد ✓`);
|
||||
await loadUsers();
|
||||
refreshProcurementStore();
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "خطا در ثبت دسترسی پنل");
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const setMemberProjects = async (u: UserRow, nextIds: string[]) => {
|
||||
setBusyId(u.id);
|
||||
try {
|
||||
await apiV26.users.setPanelProjects(u.id, nextIds);
|
||||
toast.success(`عضویت پروژهای «${u.first_name} ${u.last_name}» بهروزرسانی شد ✓`);
|
||||
await loadUsers();
|
||||
refreshProcurementStore();
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : "خطا در ثبت عضویت پروژهها");
|
||||
} finally {
|
||||
setBusyId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const panelLabel = (level: string | undefined) =>
|
||||
level === "full" ? "کامل" : level === "limited" ? "محدود" : "—";
|
||||
|
||||
// سرپرستان تخصیصیافته به یک پروژه
|
||||
const supervisorsOf = (projectId: string) =>
|
||||
users.filter(
|
||||
@@ -285,6 +320,99 @@ export function OrganizationPage() {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{/* ---------- بخش ۳: پنلها و سطوح دسترسی (جایگزین نقشها) ---------- */}
|
||||
<div className="rounded-xl border border-line bg-panel p-4">
|
||||
<div className="mb-3">
|
||||
<h3 className="text-[14px] font-bold">🔑 پنلها و سطوح دسترسی</h3>
|
||||
<p className="mt-0.5 text-[11.5px] leading-5 text-muted">
|
||||
به هر شخص پنل «تدارکات» یا «انبار» را تخصیص دهید. تدارکات — کامل: تغییر در کل پنل؛ محدود: فقط افزودن
|
||||
استعلام برای پروژههای عضویت. انبار — کامل: همه پروژهها بدون تغییر موجودی؛ محدود: فقط پروژههای عضویت با
|
||||
امکان تغییر موجودی. ادمین دستنخورده است.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-[12.5px]">
|
||||
<thead>
|
||||
<tr className="border-b border-line text-right text-muted">
|
||||
<th className="py-2 pe-3">نام</th>
|
||||
<th className="py-2 pe-3">پنل تدارکات</th>
|
||||
<th className="py-2 pe-3">پنل انبار</th>
|
||||
<th className="py-2">پروژههای عضویت (محدوده سطح محدود)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((u) => {
|
||||
if (u.role === "admin") {
|
||||
return (
|
||||
<tr key={u.id} className="border-b border-line/50">
|
||||
<td className="py-2.5 pe-3"><b>{u.first_name} {u.last_name}</b></td>
|
||||
<td colSpan={3} className="py-2.5 text-[11px] text-muted">
|
||||
ادمین — دسترسی کامل به همه پنلها (دستنخورده)
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
const hasPanel = Boolean(u.panels?.procurement || u.panels?.inventory);
|
||||
return (
|
||||
<tr key={u.id} className="border-b border-line/50">
|
||||
<td className="py-2.5 pe-3">
|
||||
<b>{u.first_name} {u.last_name}</b>
|
||||
<span className="ms-1 font-mono text-[10px] text-muted">({u.username || "—"})</span>
|
||||
</td>
|
||||
<td className="py-2.5 pe-3">
|
||||
{canChangeRoles ? (
|
||||
<select
|
||||
className="h-7 rounded-lg border border-line bg-panel px-2 text-[12px]"
|
||||
value={u.panels?.procurement ?? ""}
|
||||
disabled={busyId === u.id}
|
||||
onChange={(e) => void setPanel(u, "procurement", e.target.value as "" | "full" | "limited")}
|
||||
>
|
||||
<option value="">— بدون تخصیص —</option>
|
||||
<option value="full">کامل</option>
|
||||
<option value="limited">محدود (فقط استعلام)</option>
|
||||
</select>
|
||||
) : (
|
||||
<span className="text-[11px]">{panelLabel(u.panels?.procurement)}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2.5 pe-3">
|
||||
{canChangeRoles ? (
|
||||
<select
|
||||
className="h-7 rounded-lg border border-line bg-panel px-2 text-[12px]"
|
||||
value={u.panels?.inventory ?? ""}
|
||||
disabled={busyId === u.id}
|
||||
onChange={(e) => void setPanel(u, "inventory", e.target.value as "" | "full" | "limited")}
|
||||
>
|
||||
<option value="">— بدون تخصیص —</option>
|
||||
<option value="full">کامل (بدون تغییر موجودی)</option>
|
||||
<option value="limited">محدود (با تغییر موجودی)</option>
|
||||
</select>
|
||||
) : (
|
||||
<span className="text-[11px]">{panelLabel(u.panels?.inventory)}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-2.5">
|
||||
{hasPanel ? (
|
||||
<div className="w-72">
|
||||
<ProjectMultiSelect
|
||||
options={projects.map((p) => ({ id: p.id, name: p.name }))}
|
||||
values={u.member_project_ids ?? []}
|
||||
onChange={(nextIds) => void setMemberProjects(u, nextIds)}
|
||||
disabled={busyId === u.id}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-[11px] text-muted">— ابتدا پنل تخصیص دهید —</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user