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
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:
@@ -19,6 +19,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Table } from "@/lib/api/types";
|
||||
import { Can } from "@/components/auth/can";
|
||||
|
||||
type ReservationStatus =
|
||||
| "Pending"
|
||||
@@ -194,12 +195,14 @@ export function ReservationsScreen() {
|
||||
/>
|
||||
</LabeledField>
|
||||
<div className="sm:col-span-2 lg:col-span-3">
|
||||
<Button
|
||||
onClick={() => createReservation.mutate()}
|
||||
disabled={!guestName.trim() || createReservation.isPending}
|
||||
>
|
||||
{createReservation.isPending ? "..." : t("create")}
|
||||
</Button>
|
||||
<Can permission="CreateReservation">
|
||||
<Button
|
||||
onClick={() => createReservation.mutate()}
|
||||
disabled={!guestName.trim() || createReservation.isPending}
|
||||
>
|
||||
{createReservation.isPending ? "..." : t("create")}
|
||||
</Button>
|
||||
</Can>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -228,19 +231,23 @@ export function ReservationsScreen() {
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{r.status === "Pending" && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => updateStatus.mutate({ id: r.id, status: "Confirmed" })}
|
||||
>
|
||||
{t("confirm")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => updateStatus.mutate({ id: r.id, status: "Cancelled" })}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Can permission="EditReservation">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => updateStatus.mutate({ id: r.id, status: "Confirmed" })}
|
||||
>
|
||||
{t("confirm")}
|
||||
</Button>
|
||||
</Can>
|
||||
<Can permission="EditReservation">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => updateStatus.mutate({ id: r.id, status: "Cancelled" })}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</Can>
|
||||
</>
|
||||
)}
|
||||
{(r.status === "Confirmed" || r.status === "Seated") && (
|
||||
@@ -249,23 +256,27 @@ export function ReservationsScreen() {
|
||||
</Button>
|
||||
)}
|
||||
{r.status === "Seated" && (
|
||||
<Can permission="EditReservation">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => updateStatus.mutate({ id: r.id, status: "Completed" })}
|
||||
>
|
||||
{t("markCompleted")}
|
||||
</Button>
|
||||
</Can>
|
||||
)}
|
||||
<Can permission="DeleteReservation">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => updateStatus.mutate({ id: r.id, status: "Completed" })}
|
||||
variant="ghost"
|
||||
className="text-red-600 hover:bg-red-50 hover:text-red-700"
|
||||
aria-label={tCommon("delete")}
|
||||
onClick={() => setDeleteTarget(r)}
|
||||
>
|
||||
{t("markCompleted")}
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="text-red-600 hover:bg-red-50 hover:text-red-700"
|
||||
aria-label={tCommon("delete")}
|
||||
onClick={() => setDeleteTarget(r)}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</Can>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user