feat(rbac): enforce procurement write restrictions and support supervisor role in SSO

This commit is contained in:
2026-09-26 12:32:01 +03:30
parent 730d0bd496
commit 5a9b543040
31 changed files with 480 additions and 139 deletions
+23 -14
View File
@@ -18,12 +18,16 @@ import { LedgerTable } from "./LedgerTable";
import { AppSwitcherMenu } from "../../components/AppSwitcherMenu";
import { AcceptModal } from "./AcceptModal";
import { EditModal } from "./EditModal";
import { useAuthStore } from "../../store/auth";
const EMPTY_FILTERS: Filters = { supplier: "", status: "", search: "" };
export function DashboardPage() {
useStoreData();
const user = useAuthStore((s) => s.user);
const canWrite = user?.role === "admin" || user?.role === "procurement_manager";
const quotes = useQuotes();
const suppliers = useSuppliers();
const materials = useMaterials();
@@ -114,13 +118,15 @@ export function DashboardPage() {
</Link>
{/* دکمه اصلی: افزودن پیشنهاد جدید */}
<Link
to="/wizard"
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white text-xs font-semibold shadow-sm hover:shadow transition-all no-underline"
>
<Plus className="h-4 w-4" />
<span>افزودن پیشنهاد جدید</span>
</Link>
{canWrite && (
<Link
to="/wizard"
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white text-xs font-semibold shadow-sm hover:shadow transition-all no-underline"
>
<Plus className="h-4 w-4" />
<span>افزودن پیشنهاد جدید</span>
</Link>
)}
</div>
}
/>
@@ -148,17 +154,20 @@ export function DashboardPage() {
onToggleExpand={toggleExpand}
onAccept={setAcceptId}
onEdit={setEditId}
canWrite={canWrite}
/>
</PageLayout>
{/* دکمه شناور اکشن (FAB) مخصوص نسخه موبایل */}
<Link
to="/wizard"
aria-label="افزودن پیشنهاد جدید"
className="md:hidden fixed bottom-20 left-4 z-40 flex h-14 w-14 items-center justify-center rounded-full bg-accent text-white shadow-xl shadow-accent/30 active:scale-95 transition-transform"
>
<Plus className="h-7 w-7" />
</Link>
{canWrite && (
<Link
to="/wizard"
aria-label="افزودن پیشنهاد جدید"
className="md:hidden fixed bottom-20 left-4 z-40 flex h-14 w-14 items-center justify-center rounded-full bg-accent text-white shadow-xl shadow-accent/30 active:scale-95 transition-transform"
>
<Plus className="h-7 w-7" />
</Link>
)}
<AcceptModal quoteId={acceptId} onClose={() => setAcceptId(null)} />
<EditModal quoteId={editId} onClose={() => setEditId(null)} />
@@ -10,6 +10,7 @@ export interface LedgerTableProps {
onToggleExpand: (id: string) => void;
onAccept: (id: string) => void;
onEdit: (id: string) => void;
canWrite?: boolean;
}
// دفتر پیشنهادها — فهرست کارت‌ها (واکنش‌گرا و بهینه برای موبایل)
@@ -20,6 +21,7 @@ export function LedgerTable({
onToggleExpand,
onAccept,
onEdit,
canWrite = true,
}: LedgerTableProps) {
if (!quotes.length) {
return (
@@ -40,6 +42,7 @@ export function LedgerTable({
onToggleExpand={() => onToggleExpand(q.id)}
onAccept={() => onAccept(q.id)}
onEdit={() => onEdit(q.id)}
canWrite={canWrite}
/>
))}
<div className="px-2 py-3 text-center text-[10.5px] leading-5 text-muted sm:py-1 sm:text-[11px]">
+20 -15
View File
@@ -24,6 +24,7 @@ export interface QuoteCardProps {
onToggleExpand: () => void;
onAccept: () => void;
onEdit: () => void;
canWrite?: boolean;
}
function InfoCell({ label, children, className }: { label: string; children: React.ReactNode; className?: string }) {
@@ -35,7 +36,7 @@ function InfoCell({ label, children, className }: { label: string; children: Rea
);
}
export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit }: QuoteCardProps) {
export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit, canWrite = true }: QuoteCardProps) {
const sup = supplierById(store.suppliers, q.supplier_id);
const expired = isExpired(q);
const total = quoteTotal(q);
@@ -46,7 +47,7 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit
// ستون / دکمه پذیرش
let acceptedCell: React.ReactNode;
if (q.status === "offered") {
acceptedCell = (
acceptedCell = canWrite ? (
<button
onClick={(e) => {
e.stopPropagation();
@@ -57,6 +58,8 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit
<Check className="h-3.5 w-3.5" />
<span>پذیرش</span>
</button>
) : (
<span className="text-muted text-xs font-medium">در انتظار پذیرش</span>
);
} else if (q.accepted_date) {
acceptedCell = <span className="tabular-nums font-medium">{fmtDateTime(q.accepted_date)}</span>;
@@ -185,19 +188,21 @@ export function QuoteCard({ store, q, expanded, onToggleExpand, onAccept, onEdit
)}
{/* دکمه ویرایش */}
<button
type="button"
className="inline-flex h-7 items-center justify-center gap-1 rounded-md border border-line bg-panel px-2 text-xs font-medium text-ink hover:bg-slate-50 dark:hover:bg-zinc-800 transition-colors sm:h-8 sm:px-2.5"
onClick={(e) => {
e.stopPropagation();
onEdit();
}}
title="ویرایش پیشنهاد"
aria-label="ویرایش"
>
<Pencil className="h-3 w-3 text-muted" />
<span className="hidden sm:inline">ویرایش</span>
</button>
{canWrite && (
<button
type="button"
className="inline-flex h-7 items-center justify-center gap-1 rounded-md border border-line bg-panel px-2 text-xs font-medium text-ink hover:bg-slate-50 dark:hover:bg-zinc-800 transition-colors sm:h-8 sm:px-2.5"
onClick={(e) => {
e.stopPropagation();
onEdit();
}}
title="ویرایش پیشنهاد"
aria-label="ویرایش"
>
<Pencil className="h-3 w-3 text-muted" />
<span className="hidden sm:inline">ویرایش</span>
</button>
)}
{/* دکمه فلش اکسپند در هدر */}
<button