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:
2026-09-29 13:37:59 +03:30
parent c7d860f431
commit 4a3c226518
35 changed files with 591 additions and 72 deletions
@@ -16,11 +16,13 @@ export function RequirePmProjects({ children }: { children: ReactNode }) {
const setProjects = useAuthStore((s) => s.setProjects);
const logout = useLogout();
const isSupervisor = user?.role === "supervisor";
// پنل انبار limited هم مثل سرپرست: پروژه‌های فعالش از عضویت‌ها اعتبارسنجی می‌شود
const isSupervisor = user?.role === "supervisor" || user?.panels?.inventory === "limited";
const [state, setState] = useState<"loading" | "ok" | "empty">(isSupervisor ? "loading" : "ok");
useEffect(() => {
if (!user || user.role !== "supervisor") {
const scoped = user?.role === "supervisor" || user?.panels?.inventory === "limited";
if (!user || !scoped) {
setState("ok");
return;
}
@@ -45,7 +47,7 @@ export function RequirePmProjects({ children }: { children: ReactNode }) {
alive = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user?.id, user?.role, isDev]);
}, [user?.id, user?.role, user?.panels, isDev]);
if (state === "loading") {
return (