-
- {item.menuItemName} × {item.quantity}
-
-
- {formatCurrency(item.unitPrice * item.quantity, numberLocale)}
-
+
+
+
+ {item.menuItemName} × {item.quantity}
+
+
+ {formatCurrency(item.unitPrice * item.quantity, numberLocale)}
+
+
+ {item.notes && (
+
+ {item.notes}
+
+ )}
))}
diff --git a/web/dashboard/src/components/settings/custom-roles-panel.tsx b/web/dashboard/src/components/settings/custom-roles-panel.tsx
new file mode 100644
index 0000000..af443f2
--- /dev/null
+++ b/web/dashboard/src/components/settings/custom-roles-panel.tsx
@@ -0,0 +1,406 @@
+"use client";
+
+import { useState } from "react";
+import { useTranslations } from "next-intl";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { Plus, Pencil, Trash2, Users, ShieldCheck } from "lucide-react";
+import { apiDelete, apiGet, apiPatch, apiPost } from "@/lib/api/client";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { useConfirm } from "@/components/providers/confirm-provider";
+import { cn } from "@/lib/utils";
+
+// ─── Types ────────────────────────────────────────────────────────────────────
+
+interface CustomRoleDto {
+ id: string;
+ name: string;
+ description?: string | null;
+ color?: string | null;
+ permissions: string[];
+ employeeCount: number;
+ createdAt: string;
+}
+
+// ─── Permission catalogue ─────────────────────────────────────────────────────
+
+interface PermGroup {
+ labelKey: string;
+ perms: string[];
+}
+
+const PERM_GROUPS: PermGroup[] = [
+ {
+ labelKey: "customRoles.groupAdmin",
+ perms: ["ManageCafeSettings", "ManageBilling", "ManageBranches"],
+ },
+ {
+ labelKey: "customRoles.groupMenu",
+ perms: ["ManageMenu", "ManageInventory", "ManageTaxes", "ManagePrintSettings"],
+ },
+ {
+ labelKey: "customRoles.groupStaff",
+ perms: ["ManageStaff", "ManageSalaries", "ReviewLeave"],
+ },
+ {
+ labelKey: "customRoles.groupCustomer",
+ perms: ["ManageReservations", "ManageTables", "ManageCoupons"],
+ },
+ {
+ labelKey: "customRoles.groupReports",
+ perms: ["ViewReports", "ManageExpenses"],
+ },
+ {
+ labelKey: "customRoles.groupOps",
+ perms: ["ProcessOrders", "HandlePayments", "OperateRegister", "ManageQueue"],
+ },
+ {
+ labelKey: "customRoles.groupKitchen",
+ perms: ["ViewKitchen", "HandleDelivery"],
+ },
+];
+
+const PRESET_COLORS = [
+ "#6366F1", "#8B5CF6", "#EC4899", "#F59E0B",
+ "#10B981", "#3B82F6", "#EF4444", "#64748B",
+];
+
+// ─── Permission checkbox matrix ───────────────────────────────────────────────
+
+function PermissionMatrix({
+ selected,
+ onChange,
+ t,
+}: {
+ selected: Set
;
+ onChange: (next: Set) => void;
+ t: ReturnType;
+}) {
+ const toggle = (perm: string) => {
+ const next = new Set(selected);
+ if (next.has(perm)) next.delete(perm);
+ else next.add(perm);
+ onChange(next);
+ };
+
+ const toggleGroup = (perms: string[]) => {
+ const allOn = perms.every((p) => selected.has(p));
+ const next = new Set(selected);
+ if (allOn) perms.forEach((p) => next.delete(p));
+ else perms.forEach((p) => next.add(p));
+ onChange(next);
+ };
+
+ return (
+
+ {PERM_GROUPS.map((group) => {
+ const allOn = group.perms.every((p) => selected.has(p));
+ const someOn = group.perms.some((p) => selected.has(p));
+ return (
+
+
+
+ {group.perms.map((perm) => (
+
+ ))}
+
+
+ );
+ })}
+
+ );
+}
+
+// ─── Create / Edit form ───────────────────────────────────────────────────────
+
+function RoleForm({
+ cafeId,
+ role,
+ onDone,
+ t,
+ tCommon,
+}: {
+ cafeId: string;
+ role?: CustomRoleDto;
+ onDone: () => void;
+ t: ReturnType;
+ tCommon: ReturnType;
+}) {
+ const qc = useQueryClient();
+ const [name, setName] = useState(role?.name ?? "");
+ const [description, setDescription] = useState(role?.description ?? "");
+ const [color, setColor] = useState(role?.color ?? PRESET_COLORS[0]!);
+ const [perms, setPerms] = useState>(new Set(role?.permissions ?? []));
+ const [error, setError] = useState(null);
+
+ const save = useMutation({
+ mutationFn: async () => {
+ const body = {
+ name: name.trim(),
+ description: description.trim() || null,
+ color,
+ permissions: Array.from(perms),
+ };
+ if (role) {
+ return apiPatch(`/api/cafes/${cafeId}/custom-roles/${role.id}`, body);
+ }
+ return apiPost(`/api/cafes/${cafeId}/custom-roles`, body);
+ },
+ onSuccess: () => {
+ qc.invalidateQueries({ queryKey: ["custom-roles", cafeId] });
+ onDone();
+ },
+ onError: () => setError(t("customRoles.saveError")),
+ });
+
+ return (
+
+ {/* Name + color */}
+
+
+
+ setName(e.target.value)}
+ placeholder={t("customRoles.namePlaceholder")}
+ maxLength={100}
+ />
+
+
+
+
+ {PRESET_COLORS.map((c) => (
+
+
+
+
+ {/* Description */}
+
+
+ setDescription(e.target.value)}
+ placeholder={t("customRoles.descriptionPlaceholder")}
+ maxLength={200}
+ />
+
+
+ {/* Permissions */}
+
+
{t("customRoles.permissions")}
+
+
+
+ {error &&
{error}
}
+
+
+
+
+
+
+ );
+}
+
+// ─── Main panel ───────────────────────────────────────────────────────────────
+
+export function CustomRolesPanel({ cafeId }: { cafeId: string }) {
+ const t = useTranslations("settings");
+ const tCommon = useTranslations("common");
+ const qc = useQueryClient();
+ const confirm = useConfirm();
+ const [editing, setEditing] = useState(null);
+
+ const { data: roles = [], isLoading } = useQuery({
+ queryKey: ["custom-roles", cafeId],
+ queryFn: () => apiGet(`/api/cafes/${cafeId}/custom-roles`),
+ });
+
+ const deleteRole = useMutation({
+ mutationFn: (id: string) => apiDelete(`/api/cafes/${cafeId}/custom-roles/${id}`),
+ onSuccess: () => qc.invalidateQueries({ queryKey: ["custom-roles", cafeId] }),
+ });
+
+ const handleDelete = async (role: CustomRoleDto) => {
+ const ok = await confirm({
+ description: t("customRoles.deleteConfirm", { name: role.name }),
+ variant: "destructive",
+ confirmLabel: tCommon("confirm"),
+ });
+ if (!ok) return;
+ deleteRole.mutate(role.id);
+ };
+
+ if (editing !== null) {
+ return (
+
+
+
+ {editing === "new" ? t("customRoles.newRole") : t("customRoles.editRole")}
+
+
+
+ setEditing(null)}
+ t={t}
+ tCommon={tCommon}
+ />
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ {t("customRoles.title")}
+
+
+ {t("customRoles.subtitle")}
+
+
+
+
+
+ {isLoading ? (
+
+ {[0, 1, 2].map((i) => (
+
+ ))}
+
+ ) : roles.length === 0 ? (
+
+ {t("customRoles.empty")}
+
+ ) : (
+
+ {roles.map((role) => (
+
+ {/* Color dot */}
+
+
+
+
+ {role.name}
+ {role.description && (
+
+ — {role.description}
+
+ )}
+
+
+ {/* Permission badges */}
+
+ {role.permissions.slice(0, 6).map((p) => (
+
+ {t(`customRoles.perm.${p}`)}
+
+ ))}
+ {role.permissions.length > 6 && (
+
+ +{role.permissions.length - 6}
+
+ )}
+
+
+
+ {/* Employee count */}
+
+
+ {role.employeeCount}
+
+
+ {/* Actions */}
+
+
+
+
+
+ ))}
+
+ )}
+
+
+
+ );
+}
diff --git a/web/dashboard/src/components/settings/settings-screen.tsx b/web/dashboard/src/components/settings/settings-screen.tsx
index 9069f43..3ebc8b2 100644
--- a/web/dashboard/src/components/settings/settings-screen.tsx
+++ b/web/dashboard/src/components/settings/settings-screen.tsx
@@ -12,6 +12,7 @@ import { SettingsShopPanel } from "@/components/settings/settings-shop-panel";
import { SettingsTerminalsPanel } from "@/components/settings/settings-terminals-panel";
import { SettingsPrinterPanel } from "@/components/settings/settings-printer-panel";
import { SettingsPrintTestPanel } from "@/components/settings/settings-print-test-panel";
+import { CustomRolesPanel } from "@/components/settings/custom-roles-panel";
import {
DEFAULT_SETTINGS_LEAF,
groupForLeaf,
@@ -25,6 +26,7 @@ const LEAF_PAGE_TITLE: Record = {
"shop-discover": "nav.shopDiscover",
"printer-config": "nav.printerSettings",
"print-test": "nav.printTest",
+ "team-custom-roles": "nav.customRoles",
};
export function SettingsScreen() {
@@ -40,7 +42,10 @@ export function SettingsScreen() {
const toggleGroup = (group: SettingsGroupId) => {
setExpandedGroup((prev) => (prev === group ? prev : group));
- const firstChild = group === "shop" ? "shop-general" : "printer-config";
+ const firstChild =
+ group === "shop" ? "shop-general" :
+ group === "team" ? "team-custom-roles" :
+ "printer-config";
if (groupForLeaf(activeLeaf) !== group) {
selectLeaf(firstChild);
}
@@ -98,6 +103,10 @@ export function SettingsScreen() {
onOpenPrinterSettings={() => selectLeaf("printer-config")}
/>
) : null}
+
+ {activeLeaf === "team-custom-roles" ? (
+
+ ) : null}
diff --git a/web/dashboard/src/components/settings/settings-types.ts b/web/dashboard/src/components/settings/settings-types.ts
index 5d025b7..1caefe1 100644
--- a/web/dashboard/src/components/settings/settings-types.ts
+++ b/web/dashboard/src/components/settings/settings-types.ts
@@ -1,11 +1,12 @@
-export type SettingsGroupId = "shop" | "printer";
+export type SettingsGroupId = "shop" | "printer" | "team";
export type SettingsLeafId =
| "shop-general"
| "shop-appearance"
| "shop-discover"
| "printer-config"
- | "print-test";
+ | "print-test"
+ | "team-custom-roles";
export type SettingsNavGroup = {
id: SettingsGroupId;
@@ -31,10 +32,19 @@ export const SETTINGS_NAV: SettingsNavGroup[] = [
{ id: "print-test", labelKey: "nav.printTest" },
],
},
+ {
+ id: "team",
+ labelKey: "nav.team",
+ children: [
+ { id: "team-custom-roles", labelKey: "nav.customRoles" },
+ ],
+ },
];
export const DEFAULT_SETTINGS_LEAF: SettingsLeafId = "shop-general";
export function groupForLeaf(leaf: SettingsLeafId): SettingsGroupId {
- return leaf === "printer-config" || leaf === "print-test" ? "printer" : "shop";
+ if (leaf === "printer-config" || leaf === "print-test") return "printer";
+ if (leaf === "team-custom-roles") return "team";
+ return "shop";
}