Supervisors are scoped by project membership (and are blocked at login without one), but the editor was hidden unless the user had a panel — a coordinator could never be given a project. Backend always allowed it.
600 lines
32 KiB
TypeScript
600 lines
32 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
||
import { toast } from "sonner";
|
||
import { Header, PageLayout } from "../../components/layout/Header";
|
||
import { useAuthStore } from "../../store/auth";
|
||
import { api, apiV26, type UserRow } from "../../lib/api";
|
||
import type { Project } from "../../lib/types";
|
||
import { refreshProcurementStore, useProjects } from "../../hooks";
|
||
import { faNum } from "../../lib/formatters";
|
||
import { ProjectMultiSelect } from "../../components/ProjectMultiSelect";
|
||
import { AppSwitcherMenu } from "../../components/AppSwitcherMenu";
|
||
|
||
type Role = "pm" | "procurement_manager" | "admin" | "supervisor" | null;
|
||
|
||
const ROLE_LABELS: Record<string, string> = {
|
||
admin: "ادمین",
|
||
procurement_manager: "مدیر تدارکات",
|
||
pm: "مدیر پروژه",
|
||
supervisor: "سرپرست پروژه",
|
||
};
|
||
|
||
// پنل مستقل سازماندهی (ادمین، مدیر تدارکات، مدیر پروژه) — مدیریت کاربران، نقشها و تخصیص پروژهها و پنلها
|
||
export function OrganizationPage() {
|
||
const user = useAuthStore((s) => s.user);
|
||
const storeProjects = useProjects();
|
||
const [directProjects, setDirectProjects] = useState<Project[]>([]);
|
||
const [loadingProjects, setLoadingProjects] = useState(false);
|
||
|
||
const [users, setUsers] = useState<UserRow[]>([]);
|
||
const [loadingUsers, setLoadingUsers] = useState(true);
|
||
const [syncing, setSyncing] = useState(false);
|
||
const [busyId, setBusyId] = useState<string | null>(null);
|
||
|
||
const canManage = user?.role === "admin" || user?.role === "procurement_manager" || user?.role === "pm";
|
||
const canChangeRoles = canManage;
|
||
|
||
const fetchProjects = useCallback(async () => {
|
||
try {
|
||
setLoadingProjects(true);
|
||
const list = await api.projects.list();
|
||
if (list && Array.isArray(list)) {
|
||
setDirectProjects(list);
|
||
}
|
||
} catch {
|
||
// fallback silently
|
||
} finally {
|
||
setLoadingProjects(false);
|
||
}
|
||
}, []);
|
||
|
||
const loadUsers = useCallback(async () => {
|
||
try {
|
||
setLoadingUsers(true);
|
||
const list = await apiV26.users.list();
|
||
setUsers(list);
|
||
} catch (err) {
|
||
toast.error(err instanceof Error ? err.message : "خطا در دریافت کاربران");
|
||
} finally {
|
||
setLoadingUsers(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (canManage) {
|
||
void loadUsers();
|
||
void fetchProjects();
|
||
}
|
||
}, [canManage, loadUsers, fetchProjects]);
|
||
|
||
const projects = storeProjects.length > 0 ? storeProjects : directProjects;
|
||
|
||
const syncUsers = async () => {
|
||
setSyncing(true);
|
||
try {
|
||
const { users: synced, created } = await apiV26.users.sync();
|
||
setUsers(synced);
|
||
toast.success(
|
||
created > 0
|
||
? `همگامسازی انجام شد — ${faNum(created)} کاربر جدید از Casdoor اضافه شد ✓`
|
||
: "همگامسازی انجام شد — کاربر جدیدی یافت نشد",
|
||
);
|
||
refreshProcurementStore();
|
||
void fetchProjects();
|
||
} catch (err) {
|
||
toast.error(err instanceof Error ? err.message : "خطا در همگامسازی با Casdoor");
|
||
} finally {
|
||
setSyncing(false);
|
||
}
|
||
};
|
||
|
||
if (!canManage) {
|
||
return (
|
||
<div className="min-h-screen">
|
||
<Header title="سازماندهی" backTo="/" backLabel="بازگشت به دفتر" />
|
||
<PageLayout>
|
||
<div className="text-[13px] text-muted">این بخش فقط برای ادمین، مدیر تدارکات و مدیر پروژه است.</div>
|
||
</PageLayout>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ---------- actions: users ----------
|
||
|
||
const changeRole = async (u: UserRow, role: Role) => {
|
||
setBusyId(u.id);
|
||
try {
|
||
await apiV26.users.updateRole(u.id, role);
|
||
toast.success(`نقش «${ROLE_LABELS[role ?? ""] ?? "بدون نقش"}» برای ${u.first_name} ${u.last_name} ثبت شد ✓`);
|
||
await loadUsers();
|
||
refreshProcurementStore();
|
||
} catch (err) {
|
||
toast.error(err instanceof Error ? err.message : "خطا در تغییر نقش");
|
||
} finally {
|
||
setBusyId(null);
|
||
}
|
||
};
|
||
|
||
// ---------- لایه پنلها: تخصیص پنل + سطح دسترسی و عضویت پروژهای ----------
|
||
const setPanel = async (u: UserRow, panel: "procurement" | "inventory" | "pm", 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(
|
||
(u) =>
|
||
(u.role === "supervisor" || u.role === "pm" || Boolean(u.panels?.pm)) &&
|
||
(u.projects?.some((p) => p.id === projectId) ||
|
||
u.project_id === projectId ||
|
||
u.member_project_ids?.includes(projectId)),
|
||
);
|
||
|
||
return (
|
||
<div className="min-h-screen">
|
||
<Header
|
||
title="سازماندهی"
|
||
brand
|
||
menu={<AppSwitcherMenu activeModule="organization" />}
|
||
/>
|
||
<PageLayout className="flex flex-col gap-5">
|
||
<div className="rounded-lg border border-blue/20 bg-blue/5 px-3.5 py-2.5 text-[12px] leading-6 text-muted">
|
||
ℹ️ کاربران در سامانه احراز هویت (Casdoor) تعریف شده و با کلیک بر روی دکمه «همگامسازی» اضافه میشوند. نقشهای سازمانی و سطوح دسترسی هر پنل در جدول زیر توسط ادمین تعیین میگردد.
|
||
</div>
|
||
|
||
{/* ---------- بخش ۱: جدول یکپارچه کاربران، نقشها و سطوح دسترسی پنلها ---------- */}
|
||
<div className="rounded-xl border border-line bg-panel p-4">
|
||
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||
<div>
|
||
<h3 className="text-[14px] font-bold">👥 کاربران، نقشها و سطوح دسترسی پنلها</h3>
|
||
<p className="mt-0.5 text-[11.5px] text-muted">
|
||
تعیین نقش سازمانی، تخصیص پنلها (تدارکات، انبار، PM) و مشخص نمودن پروژههای عضویت کاربر
|
||
</p>
|
||
</div>
|
||
<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 hover:bg-accent/10 transition-colors disabled:opacity-50"
|
||
>
|
||
{syncing ? "در حال همگامسازی…" : "🔄 همگامسازی از Casdoor"}
|
||
</button>
|
||
</div>
|
||
|
||
{loadingUsers ? (
|
||
<div className="py-8 text-center text-[12.5px] text-muted">در حال بارگذاری کاربران…</div>
|
||
) : !users.length ? (
|
||
<div className="rounded-lg border border-dashed border-line px-3 py-6 text-center text-[12px] text-muted">
|
||
کاربری ثبت نشده است — دکمهی «همگامسازی از Casdoor» را بزنید.
|
||
</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.5 pe-3">نام و مشخصات</th>
|
||
<th className="py-2.5 pe-3">نقش سازمانی</th>
|
||
<th className="py-2.5 pe-3">پنل تدارکات</th>
|
||
<th className="py-2.5 pe-3">پنل انبار</th>
|
||
<th className="py-2.5 pe-3">پنل مدیریت پروژه (PM)</th>
|
||
<th className="py-2.5">پروژههای عضویت (محدوده سطح محدود)</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{users.map((u) => {
|
||
const isAdmin = u.role === "admin";
|
||
const hasPanel = Boolean(u.panels?.procurement || u.panels?.inventory || u.panels?.pm);
|
||
// سرپرست/مدیر پروژه بدون پنل هم عضویت پروژهای میخواهد —
|
||
// ورود سرپرست بدون پروژه مسدود است، پس ویرایشگر باید باز باشد
|
||
const canAssignProjects = hasPanel || u.role === "supervisor" || u.role === "pm";
|
||
|
||
return (
|
||
<tr key={u.id} className="border-b border-line/50 transition-colors hover:bg-canvas/30">
|
||
{/* ۱. نام و نام کاربری */}
|
||
<td className="py-2.5 pe-3">
|
||
<div className="flex flex-col">
|
||
<div className="flex items-center gap-1.5">
|
||
<span className="font-bold text-foreground">
|
||
{u.first_name} {u.last_name}
|
||
</span>
|
||
<span className="font-mono text-[10px] text-muted">
|
||
({u.username || "—"})
|
||
</span>
|
||
</div>
|
||
{busyId === u.id && (
|
||
<span className="text-[10px] text-accent animate-pulse font-medium">
|
||
در حال ذخیره تغییرات…
|
||
</span>
|
||
)}
|
||
</div>
|
||
</td>
|
||
|
||
{/* ۲. نقش سازمانی */}
|
||
<td className="py-2.5 pe-3">
|
||
{canChangeRoles ? (
|
||
<select
|
||
className="h-7 rounded-lg border border-line bg-panel px-2 text-[12px] focus:outline-none focus:ring-1 focus:ring-accent"
|
||
value={u.role ?? ""}
|
||
disabled={busyId === u.id}
|
||
onChange={(e) => void changeRole(u, (e.target.value || null) as Role)}
|
||
>
|
||
<option value="">— بدون نقش —</option>
|
||
<option value="admin">ادمین</option>
|
||
<option value="procurement_manager">مدیر تدارکات</option>
|
||
<option value="pm">مدیر پروژه</option>
|
||
<option value="supervisor">سرپرست پروژه</option>
|
||
</select>
|
||
) : (
|
||
<span className="rounded bg-panel px-2 py-0.5 text-[11px] font-semibold text-muted">
|
||
{ROLE_LABELS[u.role ?? ""] ?? "بدون نقش"}
|
||
</span>
|
||
)}
|
||
</td>
|
||
|
||
{/* ۳. پنل تدارکات */}
|
||
<td className="py-2.5 pe-3">
|
||
{isAdmin ? (
|
||
<span className="text-[11px] text-muted">کامل (ادمین)</span>
|
||
) : canChangeRoles ? (
|
||
<select
|
||
className="h-7 rounded-lg border border-line bg-panel px-2 text-[12px] focus:outline-none focus:ring-1 focus:ring-accent"
|
||
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">
|
||
{isAdmin ? (
|
||
<span className="text-[11px] text-muted">کامل (ادمین)</span>
|
||
) : canChangeRoles ? (
|
||
<select
|
||
className="h-7 rounded-lg border border-line bg-panel px-2 text-[12px] focus:outline-none focus:ring-1 focus:ring-accent"
|
||
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>
|
||
|
||
{/* ۵. پنل مدیریت پروژه (PM) */}
|
||
<td className="py-2.5 pe-3">
|
||
{isAdmin ? (
|
||
<span className="text-[11px] text-muted">کامل (ادمین)</span>
|
||
) : canChangeRoles ? (
|
||
<select
|
||
className="h-7 rounded-lg border border-line bg-panel px-2 text-[12px] focus:outline-none focus:ring-1 focus:ring-accent"
|
||
value={u.panels?.pm ?? ""}
|
||
disabled={busyId === u.id}
|
||
onChange={(e) => void setPanel(u, "pm", 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?.pm)}</span>
|
||
)}
|
||
</td>
|
||
|
||
{/* ۶. پروژههای عضویت */}
|
||
<td className="py-2.5">
|
||
{isAdmin ? (
|
||
<span className="text-[11px] text-muted">دسترسی خودکار به تمام پروژهها</span>
|
||
) : canAssignProjects ? (
|
||
<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>
|
||
|
||
{/* ---------- بخش ۲: کارتهای پروژهها و سرپرستان مسئول ---------- */}
|
||
<div>
|
||
<div className="mb-2 flex items-center justify-between">
|
||
<div>
|
||
<h3 className="text-[14px] font-bold">🏢 پروژهها و سرپرستان مسئول</h3>
|
||
<p className="text-[11.5px] text-muted">
|
||
مشاهده وضعیت پروژهها و سرپرستان یا اعضای تخصیصیافته به هر پروژه
|
||
</p>
|
||
</div>
|
||
{loadingProjects && (
|
||
<span className="text-[11px] text-muted animate-pulse">در حال بهروزرسانی پروژهها…</span>
|
||
)}
|
||
</div>
|
||
|
||
{loadingProjects && !projects.length ? (
|
||
<div className="rounded-xl border border-line bg-panel p-6 text-center text-xs text-muted">
|
||
در حال دریافت اطلاعات پروژهها…
|
||
</div>
|
||
) : !projects.length ? (
|
||
<div className="rounded-xl border border-dashed border-line bg-panel p-6 text-center text-xs text-muted flex flex-col items-center gap-2">
|
||
<span>پروژهای برای نمایش یافت نشد یا در دسترس نیست.</span>
|
||
<button
|
||
type="button"
|
||
onClick={() => void fetchProjects()}
|
||
className="rounded-lg bg-accent/10 px-3 py-1 text-xs font-semibold text-accent hover:bg-accent/20 transition-colors"
|
||
>
|
||
🔄 تلاش مجدد برای دریافت پروژهها
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||
{projects.map((p) => {
|
||
const sups = supervisorsOf(p.id);
|
||
return (
|
||
<div key={p.id} className="rounded-xl border border-line bg-panel p-4 flex flex-col justify-between">
|
||
<div>
|
||
<div className="mb-2 flex items-center justify-between">
|
||
<h4 className="text-[14px] font-bold text-foreground">🏢 {p.name}</h4>
|
||
<span className="rounded-lg bg-canvas px-2 py-0.5 text-[11px] text-muted font-medium border border-line">
|
||
{p.status || "پروژه فعال"}
|
||
</span>
|
||
</div>
|
||
{p.description && (
|
||
<p className="mb-3 text-[12px] text-muted line-clamp-2">{p.description}</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="pt-2 border-t border-line/50">
|
||
<h5 className="mb-1.5 text-[11.5px] font-bold text-muted">👤 سرپرست(های) مسئول:</h5>
|
||
{!sups.length ? (
|
||
<div className="rounded-lg border border-dashed border-line px-3 py-2 text-[11.5px] text-amber-600 dark:text-amber-400 bg-amber-500/5">
|
||
⚠️ سرپرستی تعیین نشده است — از جدول بالا، این پروژه را به عنوان عضویت تخصیص دهید.
|
||
</div>
|
||
) : (
|
||
<div className="flex flex-col gap-1">
|
||
{sups.map((s) => (
|
||
<div key={s.id} className="flex items-center justify-between rounded-lg border border-line bg-canvas/40 px-2.5 py-1.5 text-[12px]">
|
||
<span className="font-semibold text-foreground">{s.first_name} {s.last_name}</span>
|
||
<span className="text-[10px] text-muted font-mono">({s.username || ROLE_LABELS[s.role ?? ""] || "عضو"})</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* ---------- بخش ۴: راهنما و توضیحات سطوح دسترسی پنلها ---------- */}
|
||
<div className="rounded-xl border border-line bg-panel p-5">
|
||
<div className="mb-4 flex items-center justify-between border-b border-line pb-3">
|
||
<div>
|
||
<h3 className="flex items-center gap-2 text-[15px] font-bold">
|
||
<span className="flex h-7 w-7 items-center justify-center rounded-lg bg-accent/10 text-accent">
|
||
📖
|
||
</span>
|
||
توضیحات پنلها و راهنمای سطوح دسترسی
|
||
</h3>
|
||
<p className="mt-1 text-[12px] text-muted">
|
||
مفهوم، اختیارات و محدوده کاری هر سطح دسترسی (کامل / محدود) در هر یک از پنلهای سامانه
|
||
</p>
|
||
</div>
|
||
<div className="hidden sm:flex items-center gap-2 text-[11px] text-muted bg-panel border border-line px-3 py-1.5 rounded-lg">
|
||
<span className="h-2 w-2 rounded-full bg-emerald-500 animate-pulse" />
|
||
مدل دسترسی ماژولار مبتنی بر پنل
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||
{/* کارت پنل تدارکات */}
|
||
<div className="flex flex-col rounded-xl border border-line bg-canvas/40 p-4 transition-all hover:border-sky-500/40">
|
||
<div className="mb-3 flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-sky-500/10 text-[18px]">
|
||
🛒
|
||
</span>
|
||
<div>
|
||
<h4 className="text-[13.5px] font-bold text-foreground">پنل تدارکات</h4>
|
||
<span className="text-[10.5px] text-muted font-mono">Procurement</span>
|
||
</div>
|
||
</div>
|
||
<span className="rounded-md bg-sky-500/10 px-2 py-0.5 text-[11px] font-medium text-sky-600 dark:text-sky-400">
|
||
خرید و بازرگانی
|
||
</span>
|
||
</div>
|
||
|
||
<div className="flex-1 space-y-3">
|
||
<div className="rounded-lg border border-line/60 bg-panel/70 p-3">
|
||
<div className="mb-1 flex items-center gap-1.5">
|
||
<span className="inline-block h-2 w-2 rounded-full bg-emerald-500" />
|
||
<span className="text-[12px] font-bold text-emerald-600 dark:text-emerald-400">سطح کامل (Full)</span>
|
||
</div>
|
||
<ul className="space-y-1 pe-2 text-[11.5px] leading-relaxed text-muted list-disc list-inside">
|
||
<li>دسترسی کامل مدیریتی به تمامی بخشهای تدارکات</li>
|
||
<li>ثبت و ویرایش پیشفاکتورها، شرایط پرداخت و فاکتورها</li>
|
||
<li>مدیریت تأمینکنندگان، اقلام و تصمیمگیری خرید در همه پروژهها</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div className="rounded-lg border border-line/60 bg-panel/70 p-3">
|
||
<div className="mb-1 flex items-center gap-1.5">
|
||
<span className="inline-block h-2 w-2 rounded-full bg-amber-500" />
|
||
<span className="text-[12px] font-bold text-amber-600 dark:text-amber-400">سطح محدود (Limited - متقاضی)</span>
|
||
</div>
|
||
<ul className="space-y-1 pe-2 text-[11.5px] leading-relaxed text-muted list-disc list-inside">
|
||
<li>فقط امکان ثبت درخواست استعلام قیمت و پیشفاکتور اولیه</li>
|
||
<li>محدود منحصراً به پروژههایی که کاربر در آنها عضویت دارد</li>
|
||
<li>عدم دسترسی به تأییدات نهایی، تغییر قیمتها یا پروژههای غیرعضو</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* کارت پنل انبار */}
|
||
<div className="flex flex-col rounded-xl border border-line bg-canvas/40 p-4 transition-all hover:border-amber-500/40">
|
||
<div className="mb-3 flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-amber-500/10 text-[18px]">
|
||
📦
|
||
</span>
|
||
<div>
|
||
<h4 className="text-[13.5px] font-bold text-foreground">پنل انبار</h4>
|
||
<span className="text-[10.5px] text-muted font-mono">Inventory</span>
|
||
</div>
|
||
</div>
|
||
<span className="rounded-md bg-amber-500/10 px-2 py-0.5 text-[11px] font-medium text-amber-600 dark:text-amber-400">
|
||
انبارداری و موجودی
|
||
</span>
|
||
</div>
|
||
|
||
<div className="flex-1 space-y-3">
|
||
<div className="rounded-lg border border-line/60 bg-panel/70 p-3">
|
||
<div className="mb-1 flex items-center gap-1.5">
|
||
<span className="inline-block h-2 w-2 rounded-full bg-emerald-500" />
|
||
<span className="text-[12px] font-bold text-emerald-600 dark:text-emerald-400">سطح کامل (Full - مدیر/ناظر)</span>
|
||
</div>
|
||
<ul className="space-y-1 pe-2 text-[11.5px] leading-relaxed text-muted list-disc list-inside">
|
||
<li>مشاهده موجودی و کاردکس کالا در تمامی انبارها و پروژهها</li>
|
||
<li>پایش و نظارت بر گردش کالا در کل سازمان و گزارشگیری کلان</li>
|
||
<li className="font-semibold text-amber-700 dark:text-amber-300">بدون امکان تغییر مستقیم موجودی (جهت حفظ سلامت داده و کنترل نظارتی)</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div className="rounded-lg border border-line/60 bg-panel/70 p-3">
|
||
<div className="mb-1 flex items-center gap-1.5">
|
||
<span className="inline-block h-2 w-2 rounded-full bg-amber-500" />
|
||
<span className="text-[12px] font-bold text-amber-600 dark:text-amber-400">سطح محدود (Limited - انباردار کارگاه)</span>
|
||
</div>
|
||
<ul className="space-y-1 pe-2 text-[11.5px] leading-relaxed text-muted list-disc list-inside">
|
||
<li>دسترسی فقط به انبارهای پروژههای عضویت کاربر</li>
|
||
<li>امکان ثبت و تغییر موجودی، رسید کالا، حواله مصرف و تطبیق فیزیکی</li>
|
||
<li>تفکیک دقیق موجودی و تراکنشهای انبار بر اساس پروژه</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* کارت پنل مدیریت پروژه (PM) */}
|
||
<div className="flex flex-col rounded-xl border border-line bg-canvas/40 p-4 transition-all hover:border-indigo-500/40">
|
||
<div className="mb-3 flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-indigo-500/10 text-[18px]">
|
||
📊
|
||
</span>
|
||
<div>
|
||
<h4 className="text-[13.5px] font-bold text-foreground">پنل مدیریت پروژه (PM)</h4>
|
||
<span className="text-[10.5px] text-muted font-mono">Project Management</span>
|
||
</div>
|
||
</div>
|
||
<span className="rounded-md bg-indigo-500/10 px-2 py-0.5 text-[11px] font-medium text-indigo-600 dark:text-indigo-400">
|
||
کنترل پروژه و اجرا
|
||
</span>
|
||
</div>
|
||
|
||
<div className="flex-1 space-y-3">
|
||
<div className="rounded-lg border border-line/60 bg-panel/70 p-3">
|
||
<div className="mb-1 flex items-center gap-1.5">
|
||
<span className="inline-block h-2 w-2 rounded-full bg-emerald-500" />
|
||
<span className="text-[12px] font-bold text-emerald-600 dark:text-emerald-400">سطح کامل (Full - مدیر پروژه)</span>
|
||
</div>
|
||
<ul className="space-y-1 pe-2 text-[11.5px] leading-relaxed text-muted list-disc list-inside">
|
||
<li>مدیریت جامع ساختار شکست کار (WBS)، زمانبندی و گانت چارت تعاملی</li>
|
||
<li>تعریف و ویرایش فعالیتها، ثبت خط مبنا (Baseline)، کنترل هزینه و منابع</li>
|
||
<li>پایش شاخصهای پیشرفت زمانی و مالی (EVM) و گزارشهای تحلیلی</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div className="rounded-lg border border-line/60 bg-panel/70 p-3">
|
||
<div className="mb-1 flex items-center gap-1.5">
|
||
<span className="inline-block h-2 w-2 rounded-full bg-amber-500" />
|
||
<span className="text-[12px] font-bold text-amber-600 dark:text-amber-400">سطح محدود (Limited - سرپرست/کارشناس)</span>
|
||
</div>
|
||
<ul className="space-y-1 pe-2 text-[11.5px] leading-relaxed text-muted list-disc list-inside">
|
||
<li>مشاهده فعالیتها و تسکهای مربوط به پروژههای عضویت کاربر</li>
|
||
<li>بهروزرسانی درصد پیشرفت فیزیکی و وضعیت انجام تسکها</li>
|
||
<li>تکمیل چکلیستهای کنترل کیفیت (QC) و ثبت گزارشهای اجرایی روزانه</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* نوار توضیحات تکمیلی و راهنمای عمومی */}
|
||
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-3 pt-3 border-t border-line text-[11.5px]">
|
||
<div className="flex items-start gap-2.5 rounded-lg border border-line/60 bg-canvas/30 p-2.5">
|
||
<span className="text-[16px] leading-none">👑</span>
|
||
<div>
|
||
<span className="font-bold text-foreground">دسترسی ادمین (Admin):</span>
|
||
<p className="mt-0.5 text-muted leading-relaxed">
|
||
کاربر با نقش ادمین بالاترین سطح دسترسی سیستمی را داشته و به تمامی پنلها و پروژهها دسترسی کامل دارد و نیازی به تخصیص جداگانه پنلها ندارد.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-start gap-2.5 rounded-lg border border-line/60 bg-canvas/30 p-2.5">
|
||
<span className="text-[16px] leading-none">🎯</span>
|
||
<div>
|
||
<span className="font-bold text-foreground">کارکرد «پروژههای عضویت»:</span>
|
||
<p className="mt-0.5 text-muted leading-relaxed">
|
||
هرگاه برای کاربری در هر پنلی سطح «محدود» انتخاب شود، دامنه دادهها و اختیارات وی دقیقاً به پروژههای انتخابی در ستون «پروژههای عضویت» محدود خواهد شد.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</PageLayout>
|
||
</div>
|
||
);
|
||
}
|