feat(rbac): gate pages and action buttons in the UI by permission
CI/CD / CI · API (dotnet build + test) (push) Successful in 39s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 28s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m8s
CI/CD / CI · Admin Web (tsc) (push) Successful in 37s
CI/CD / CI · Website (tsc) (push) Successful in 46s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Successful in 2m45s

Nav already hides pages a role can't view (NAV_REQUIRED_PERMISSION). This wraps
the sensitive/CRUD action controls in <Can permission> so users only see what
they can do (server still enforces):

- POS/orders: void → VoidOrder, cancel → VoidOrder, transfer → EditOrder,
  pay/split → HandlePayments
- menu/inventory/coupons/customers/reservations/expenses/taxes/branches:
  add/edit/delete buttons → the matching Create/Edit/Delete permission
- reports CSV export → ExportReports; SMS send → SendSms, settings → ManageSmsSettings
- home dashboard: revenue/orders KPI queries gated on ViewReports so non-report
  roles don't 403 on the landing page

(Refund/discount/comp/cash-drawer have no UI control yet — no buttons to gate.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-21 05:58:56 +03:30
parent 53d90fa357
commit 2cff5051ac
15 changed files with 457 additions and 357 deletions
@@ -25,6 +25,7 @@ import { apiGet, apiPost, ApiClientError } from "@/lib/api/client";
import { submitOrderToApi, orderAmountDue, isLocalOrder } from "@/lib/pos/submit-order";
import { requestPosPayment, posDeviceErrorMessage } from "@/lib/api/pos-device";
import { PosCustomerPicker } from "@/components/pos/pos-customer-picker";
import { Can } from "@/components/auth/can";
import type { Customer, MenuItem, Order, TableBoardItem } from "@/lib/api/types";
import { usePos2Categories, usePos2Menu, usePos2Tables, useMenuById } from "@/lib/pos2/use-pos2";
@@ -729,17 +730,21 @@ function Ticket({
className="flex min-h-[56px] items-center justify-center gap-2 rounded-xl bg-primary text-base font-bold text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-40 active:scale-[0.98]">
<Send className="size-5" /> ارسال{pendingCount > 0 ? ` (${fmt(pendingCount)})` : ""}
</button>
<button type="button" disabled={count === 0} onClick={onPay}
className="flex min-h-[56px] items-center justify-center gap-2 rounded-xl border-2 border-primary text-base font-bold text-primary transition-colors hover:bg-primary/10 disabled:opacity-40 active:scale-[0.98]">
<CreditCard className="size-5" /> پرداخت
</button>
<Can permission="HandlePayments">
<button type="button" disabled={count === 0} onClick={onPay}
className="flex min-h-[56px] items-center justify-center gap-2 rounded-xl border-2 border-primary text-base font-bold text-primary transition-colors hover:bg-primary/10 disabled:opacity-40 active:scale-[0.98]">
<CreditCard className="size-5" /> پرداخت
</button>
</Can>
<button type="button" disabled className="flex min-h-[48px] items-center justify-center gap-1.5 rounded-xl bg-muted text-sm font-medium text-muted-foreground opacity-50">
<Pause className="size-4" /> نگهداشتن
</button>
<button type="button" disabled={count === 0} onClick={onSplit}
className="flex min-h-[48px] items-center justify-center gap-1.5 rounded-xl bg-muted text-sm font-medium text-muted-foreground hover:bg-accent disabled:opacity-40">
<SplitSquareHorizontal className="size-4" /> تقسیم
</button>
<Can permission="HandlePayments">
<button type="button" disabled={count === 0} onClick={onSplit}
className="flex min-h-[48px] items-center justify-center gap-1.5 rounded-xl bg-muted text-sm font-medium text-muted-foreground hover:bg-accent disabled:opacity-40">
<SplitSquareHorizontal className="size-4" /> تقسیم
</button>
</Can>
</div>
</div>
);