feat(rbac): enforce procurement write restrictions and support supervisor role in SSO
This commit is contained in:
@@ -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]">
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user