Merge pm (panel-based access, supervisor role) onto the restored RBAC base
Reverting 253c922 first made this merge small — only two conflicts:
- MobileTabBar: supervisor null-check combined with v7's /review hiding
- ArrivalsPage: pm's PM+supervisor scoping combined with v8's 3-way fetch
(transfers) and its transfer-receive helpers, plus pm's canAccess gate
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { FilePlus2, Info } from "lucide-react";
|
||||
import { Header, PageLayout } from "../../components/layout/Header";
|
||||
import { useAuthStore } from "../../store/auth";
|
||||
import { useProjects } from "../../hooks";
|
||||
import { faNum } from "../../lib/formatters";
|
||||
|
||||
// خانه حداقلی کاربرِ «تدارکات — سطح limited»: فقط افزودن استعلام برای
|
||||
// پروژههای عضویت خودش. هیچ سربرگ دیگری از پنل تدارکات دیده نمیشود.
|
||||
export function RequesterHome() {
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const projects = useProjects();
|
||||
const memberIds = new Set(user?.member_project_ids ?? []);
|
||||
const myProjects = memberIds.size
|
||||
? projects.filter((p) => memberIds.has(p.id))
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Header title="افزودن استعلام" brand />
|
||||
<PageLayout className="flex flex-col gap-4">
|
||||
<div className="rounded-lg border border-blue/20 bg-blue/5 px-3 py-2 text-[12px] leading-6 text-muted">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Info className="h-3.5 w-3.5" />
|
||||
شما فقط مجاز به ثبت استعلام برای پروژههای تخصیصیافتهی خود هستید.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{myProjects.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-line bg-panel px-4 py-8 text-center text-[12.5px] text-muted">
|
||||
هنوز پروژهای به شما تخصیص داده نشده است — با مدیر سیستم تماس بگیرید.
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
{myProjects.map((p) => (
|
||||
<Link
|
||||
key={p.id}
|
||||
to="/wizard"
|
||||
className="flex items-center justify-between rounded-xl border border-line bg-panel p-4 no-underline transition-colors hover:border-accent/50"
|
||||
>
|
||||
<div>
|
||||
<b className="text-[13.5px]">🏢 {p.name}</b>
|
||||
<p className="mt-0.5 text-[11.5px] text-muted">ثبت استعلام خرید برای این پروژه</p>
|
||||
</div>
|
||||
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-accent/10 text-accent">
|
||||
<FilePlus2 className="h-5 w-5" />
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Link to="/wizard" className="btn-line-file no-underline">
|
||||
<FilePlus2 className="h-3.5 w-3.5" />
|
||||
افزودن استعلام جدید ({faNum(myProjects.length)} پروژه فعال)
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</PageLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -23,6 +23,8 @@ export function ArrivalsPage() {
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const isAdmin = user?.role === "admin" || user?.role === "procurement_manager";
|
||||
const isPM = user?.role === "pm";
|
||||
const isSupervisor = user?.role === "supervisor";
|
||||
const canAccess = isAdmin || isPM || isSupervisor;
|
||||
|
||||
const [items, setItems] = useState<PendingArrival[]>([]);
|
||||
const [incomingTransfers, setIncomingTransfers] = useState<ProjectTransfer[]>([]);
|
||||
@@ -36,9 +38,9 @@ export function ArrivalsPage() {
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const activeProject = isPM ? user?.project_id || undefined : undefined;
|
||||
const activeProject = (isPM || isSupervisor) ? user?.project_id || undefined : undefined;
|
||||
const [arr, whs, incoming] = await Promise.all([
|
||||
// پروژهی فعال صریحاً ارسال میشود؛ بکاند تخصیص PM را بررسی میکند
|
||||
// پروژهی فعال صریحاً ارسال میشود؛ بکاند تخصیص PM / سرپرست را بررسی میکند
|
||||
apiV26.pm.onTheWay(activeProject),
|
||||
apiV26.warehouses.list(activeProject),
|
||||
apiV26.projectTransfers.onTheWay(activeProject),
|
||||
@@ -53,13 +55,13 @@ export function ArrivalsPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [isPM, user?.project_id]);
|
||||
}, [isPM, isSupervisor, user?.project_id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAdmin && !isPM) return;
|
||||
if (!canAccess) return;
|
||||
setLoading(true);
|
||||
void load();
|
||||
}, [isAdmin, isPM, load]);
|
||||
}, [canAccess, load]);
|
||||
|
||||
// سفارشها: گروهبندی اقلام بهازای هر پیشنهاد (فقط سفارشهای دارای قلمِ در راه/ناقص)
|
||||
const orders = useMemo(() => {
|
||||
@@ -267,11 +269,14 @@ export function ArrivalsPage() {
|
||||
};
|
||||
|
||||
if (!isAdmin && !isPM) {
|
||||
|
||||
if (!canAccess) {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Header title="کالاهای در راه" backTo="/inventory" backLabel="بازگشت به داشبورد" />
|
||||
<PageLayout>
|
||||
<EmptyState emoji="🔒" text="این بخش فقط برای مدیر پروژه و مدیر تدارکات است." />
|
||||
<EmptyState emoji="🔒" text="این بخش برای نقش کاربری شما مجاز نیست." />
|
||||
</PageLayout>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -41,10 +41,12 @@ export function DailyControlPage() {
|
||||
|
||||
const txByDay = useMemo(() => {
|
||||
const map = new Map<string, InventoryTransaction[]>();
|
||||
const allowedWhIds = new Set(data.warehouses.map((w) => w.id));
|
||||
const projWhIds = projFilter
|
||||
? new Set(data.warehouses.filter((w) => String(w.project_id) === String(projFilter)).map((w) => w.id))
|
||||
: null;
|
||||
data.transactions.forEach((t) => {
|
||||
if (!isAdmin && !allowedWhIds.has(t.warehouse_id)) return;
|
||||
if (projWhIds && !projWhIds.has(t.warehouse_id)) return;
|
||||
const k = gregKey(new Date(t.transaction_date));
|
||||
const arr = map.get(k) || [];
|
||||
@@ -53,7 +55,7 @@ export function DailyControlPage() {
|
||||
});
|
||||
map.forEach((arr) => arr.sort((a, b) => new Date(a.transaction_date).getTime() - new Date(b.transaction_date).getTime()));
|
||||
return map;
|
||||
}, [data.transactions, data.warehouses, projFilter]);
|
||||
}, [data.transactions, data.warehouses, projFilter, isAdmin]);
|
||||
|
||||
const cells = useMemo(() => {
|
||||
const [ty, tm] = gregorianToJalali(anchor.getFullYear(), anchor.getMonth() + 1, anchor.getDate());
|
||||
@@ -173,11 +175,15 @@ export function DailyControlPage() {
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<span className="font-semibold text-slate-800">پروژه فعال</span>
|
||||
<span className="font-semibold text-slate-800">
|
||||
{user?.project_name ? `پروژه: ${user.project_name}` : "پروژه فعال"}
|
||||
</span>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<span className="text-[11.5px] text-slate-400">
|
||||
{projFilter ? "نمایش برای پروژه انتخابی" : "نمایش تمام پروژهها"}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[11.5px] text-slate-400">
|
||||
{projFilter ? "نمایش برای پروژه انتخابی" : "نمایش تمام پروژهها"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
|
||||
@@ -54,7 +54,8 @@ export function InventoryPage() {
|
||||
const isManager = user?.role === "admin" || user?.role === "procurement_manager" || user?.role === "pm";
|
||||
const isSupervisor = user?.role === "supervisor";
|
||||
const isAdmin = user?.role === "admin";
|
||||
const canModify = isSupervisor || isAdmin;
|
||||
// پنل انبار limited: تغییر موجودی مجاز است؛ full: طبق طراحی ممنوع
|
||||
const canModify = isSupervisor || isAdmin || user?.panels?.inventory === "limited";
|
||||
const [params] = useSearchParams();
|
||||
|
||||
const [filters, setFilters] = useState<{ warehouse: string; material: string; project: string }>({
|
||||
|
||||
@@ -12,17 +12,12 @@ export function InventoryTopNav({
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const isAdmin = user?.role === "admin";
|
||||
const isManager = user?.role === "procurement_manager" || user?.role === "pm" || isAdmin;
|
||||
const isSupervisor = user?.role === "supervisor";
|
||||
const canViewTabs = isManager || isSupervisor;
|
||||
|
||||
const items: { label: string; to: string; active: boolean }[] = [];
|
||||
// سرپرست پروژه از دکمه شناور گرد (+) در پایین صفحه استفاده میکند و دکمه مستطیلی بالای صفحه برای او نمایش داده نمیشود
|
||||
if (isAdmin) {
|
||||
items.push({
|
||||
label: "تغییر موجودی",
|
||||
to: "/inventory/change",
|
||||
active: pathname.startsWith("/inventory/change"),
|
||||
});
|
||||
}
|
||||
if (isManager) {
|
||||
// برای تغییر موجودی از همان دکمه شناور دایرهای آبی (+) استفاده میشود و دکمه مستطیلی در بالای صفحه حذف شد
|
||||
if (canViewTabs) {
|
||||
items.push({ label: "کنترل روزانه", to: "/inventory/daily", active: pathname.startsWith("/inventory/daily") });
|
||||
items.push({ label: "انبارها", to: "/inventory/warehouses", active: pathname.startsWith("/inventory/warehouses") });
|
||||
items.push({ label: "کالاهای در راه", to: "/inventory/arrivals", active: pathname.startsWith("/inventory/arrivals") });
|
||||
|
||||
@@ -14,7 +14,8 @@ export function WarehousesPage() {
|
||||
const navigate = useNavigate();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const data = useInventoryData();
|
||||
const canManage = user?.role === "admin" || user?.role === "procurement_manager" || user?.role === "pm";
|
||||
const isSupervisor = user?.role === "supervisor";
|
||||
const canManage = user?.role === "admin" || user?.role === "procurement_manager" || user?.role === "pm" || isSupervisor;
|
||||
|
||||
const [newName, setNewName] = useState("");
|
||||
const [newDesc, setNewDesc] = useState("");
|
||||
@@ -32,7 +33,7 @@ export function WarehousesPage() {
|
||||
<div className="text-3xl">🔒</div>
|
||||
<h3 className="mt-2 text-[15px] font-bold">دسترسی محدود</h3>
|
||||
<p className="mt-1 text-[12.5px] text-muted">
|
||||
مدیریت و تعریف انبارها مخصوص مدیران سیستم است. سرپرست پروژه فقط به ثبت تغییرات موجودی دسترسی دارد.
|
||||
دسترسی به این بخش برای حساب کاربری شما فعال نیست.
|
||||
</p>
|
||||
</div>
|
||||
</PageLayout>
|
||||
@@ -43,7 +44,7 @@ export function WarehousesPage() {
|
||||
const addWarehouse = async () => {
|
||||
const name = newName.trim();
|
||||
if (!name) return toast.error("نام انبار الزامی است");
|
||||
const projectId = selectedProjectId || user?.project_id || data.lookup.projects[0]?.id;
|
||||
const projectId = isSupervisor ? user?.project_id : (selectedProjectId || user?.project_id || data.lookup.projects[0]?.id);
|
||||
if (!projectId) return toast.error("پروژه انبار را انتخاب کنید");
|
||||
try {
|
||||
await apiV26.warehouses.create({ project_id: projectId, name, description: newDesc.trim() || null });
|
||||
@@ -92,7 +93,7 @@ export function WarehousesPage() {
|
||||
<div className="rounded-xl border border-line bg-panel p-3">
|
||||
<h3 className="mb-2 text-[13px] font-bold">+ افزودن انبار جدید</h3>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{data.lookup.projects.length > 1 && (
|
||||
{!isSupervisor && data.lookup.projects.length > 1 && (
|
||||
<select
|
||||
className="h-9 rounded-lg border border-line bg-panel px-3 text-sm"
|
||||
value={selectedProjectId || user?.project_id || data.lookup.projects[0]?.id || ""}
|
||||
|
||||
@@ -61,8 +61,8 @@ export function panelMenuItems(pathname: string, role: string | null | undefined
|
||||
if (isSupervisor) {
|
||||
items.push({ label: "تغییر موجودی", to: "/inventory/change", active: pathname.startsWith("/inventory/change") });
|
||||
}
|
||||
// کنترل روزانه و انبارها و کالاهای در راه برای مدیران
|
||||
if (isManager) {
|
||||
// کنترل روزانه و انبارها و کالاهای در راه برای مدیران و سرپرست پروژه
|
||||
if (isManager || isSupervisor) {
|
||||
items.push({ label: "کنترل روزانه", to: "/inventory/daily", active: pathname.startsWith("/inventory/daily") });
|
||||
items.push({ label: "انبارها", to: "/inventory/warehouses", active: pathname.startsWith("/inventory/warehouses") });
|
||||
items.push({ label: "کالاهای در راه", to: "/inventory/arrivals", active: pathname.startsWith("/inventory/arrivals") });
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -149,6 +150,17 @@ export function WizardPage({ isStandalone = false }: { isStandalone?: boolean })
|
||||
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();
|
||||
@@ -504,13 +516,17 @@ export function WizardPage({ isStandalone = false }: { isStandalone?: boolean })
|
||||
|
||||
// ---------- پروژهها ----------
|
||||
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) => {
|
||||
@@ -950,6 +966,7 @@ export function WizardPage({ isStandalone = false }: { isStandalone?: boolean })
|
||||
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