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:
@@ -14,6 +14,8 @@ export interface Step4Props {
|
||||
onAssign: (tempId: string, proj: string) => void;
|
||||
onUnassign: (tempId: string, proj: string) => void;
|
||||
onAddProject: (name: string) => void;
|
||||
/** سطح limited تدارکات: افزودن پروژه جدید ممنوع */
|
||||
canAddProject?: boolean;
|
||||
}
|
||||
|
||||
const ALL_KEY = "__all__";
|
||||
@@ -111,7 +113,7 @@ function DroppableProject({
|
||||
}
|
||||
|
||||
// مرحله ۴ — تخصیص پروژهها با درگاندروپ (یا کلیک-قلم بعد کلیک-پروژه)
|
||||
export function Step4Projects({ items, projects, assignment, onAssign, onUnassign, onAddProject }: Step4Props) {
|
||||
export function Step4Projects({ items, projects, assignment, onAssign, onUnassign, onAddProject, canAddProject = true }: Step4Props) {
|
||||
const [picked, setPicked] = useState<string | null>(null);
|
||||
const [newProj, setNewProj] = useState("");
|
||||
|
||||
@@ -213,8 +215,8 @@ export function Step4Projects({ items, projects, assignment, onAssign, onUnassig
|
||||
{!projects.length && (
|
||||
<div className="dd-empty">هنوز پروژهای تعریف نشده است — از پایین یک پروژه اضافه کنید.</div>
|
||||
)}
|
||||
{/* افزودن پروژه بهصورت درجا — زیر لیست پروژهها */}
|
||||
<div className="mt-3 rounded-md border border-dashed border-line p-2">
|
||||
{/* افزودن پروژه بهصورت درجا — زیر لیست پروژهها (سطح limited: ممنوع) */}
|
||||
<div className="mt-3 rounded-md border border-dashed border-line p-2" hidden={!canAddProject}>
|
||||
<div className="mb-1.5 text-[11.5px] font-semibold text-muted">افزودن پروژهی جدید:</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../../components/ui/dialog";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
|
||||
const TOTAL_STEPS = 5;
|
||||
|
||||
@@ -49,6 +50,17 @@ export function WizardPage() {
|
||||
const suppliers = useSuppliers();
|
||||
const materials = useMaterials();
|
||||
const projects = useProjects();
|
||||
// سطح limited تدارکات: استعلام فقط برای پروژههای عضویت — بدون افزودن پروژه جدید
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const isLimitedProcurement =
|
||||
user?.panels?.procurement === "limited" && user?.role !== "admin";
|
||||
const memberProjectNames = useMemo(
|
||||
() =>
|
||||
projects
|
||||
.filter((p) => (user?.member_project_ids ?? []).includes(p.id))
|
||||
.map((p) => p.name),
|
||||
[projects, user],
|
||||
);
|
||||
const topics = useAppStore((s) => s.topics);
|
||||
const createQuote = useCreateQuote();
|
||||
const addSupplier = useAddSupplier();
|
||||
@@ -131,13 +143,17 @@ export function WizardPage() {
|
||||
|
||||
// ---------- پروژهها ----------
|
||||
const projectNames = useMemo(() => {
|
||||
const names = new Set<string>(projects.map((p) => p.name));
|
||||
const names = new Set<string>(
|
||||
isLimitedProcurement
|
||||
? memberProjectNames
|
||||
: projects.map((p) => p.name),
|
||||
);
|
||||
quotes.forEach((q) => {
|
||||
if (q.project) names.add(q.project);
|
||||
(q.projects || []).forEach((n) => names.add(n));
|
||||
});
|
||||
return [...names];
|
||||
}, [projects, quotes]);
|
||||
}, [projects, quotes, isLimitedProcurement, memberProjectNames]);
|
||||
|
||||
const assign = (tempId: string, proj: string) => {
|
||||
setAssignment((a) => {
|
||||
@@ -471,6 +487,7 @@ export function WizardPage() {
|
||||
assignment={assignment}
|
||||
onAssign={assign}
|
||||
onUnassign={unassign}
|
||||
canAddProject={!isLimitedProcurement}
|
||||
onAddProject={(name) => {
|
||||
void addProject.mutateAsync({ id: nextId("p", projects), name });
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user