@gelabs/ovr 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/auth-auth.js +1 -1
  2. package/dist/auth.js +1 -1
  3. package/dist/{chunk-MDTRBOPQ.js → chunk-2C3VCTYJ.js} +1 -1
  4. package/dist/chunk-3YKVH4Y7.js +126 -0
  5. package/dist/chunk-6YFZLXFP.js +84 -0
  6. package/dist/{chunk-3NZ2XUBO.js → chunk-AJ2RZTVX.js} +9 -2
  7. package/dist/chunk-BI4EGLPG.js +298 -0
  8. package/dist/{chunk-3KIDW4LT.js → chunk-BVI5XDDA.js} +1 -1
  9. package/dist/{chunk-BIQ2J75Y.js → chunk-GLIK5BHP.js} +2 -2
  10. package/dist/{chunk-5YYR37CF.js → chunk-HGWPA7FU.js} +119 -0
  11. package/dist/{chunk-JEYT63LE.js → chunk-IBZVIUNI.js} +1 -1
  12. package/dist/chunk-IS3THKTE.js +89 -0
  13. package/dist/{chunk-4SZXBT56.js → chunk-NT72CQAI.js} +2 -2
  14. package/dist/{chunk-E2D7QT6N.js → chunk-TJSNVTVB.js} +1 -1
  15. package/dist/{chunk-5Z2IAD5I.js → chunk-TLG4C2XI.js} +2 -2
  16. package/dist/chunk-V7VQVDWS.js +237 -0
  17. package/dist/{chunk-IF5UAVIE.js → chunk-YC7G2IOZ.js} +1 -1
  18. package/dist/{chunk-IB4JVGKJ.js → chunk-YGYA7KEG.js} +47 -3
  19. package/dist/{chunk-GDOCD7LT.js → chunk-ZUMEOZ22.js} +5 -5
  20. package/dist/core-i18n.d.ts +2 -2
  21. package/dist/core-i18n.js +1 -1
  22. package/dist/core.d.ts +61 -1
  23. package/dist/core.js +1 -1
  24. package/dist/data-mock-store.js +263 -9
  25. package/dist/data-prisma-store.js +251 -1
  26. package/dist/data-seed-runner.js +18 -15
  27. package/dist/data.d.ts +53 -3
  28. package/dist/generated/client/edge.js +28 -9
  29. package/dist/generated/client/index-browser.js +25 -6
  30. package/dist/generated/client/index.d.ts +3500 -552
  31. package/dist/generated/client/index.js +28 -9
  32. package/dist/generated/client/package.json +1 -1
  33. package/dist/generated/client/schema.prisma +46 -9
  34. package/dist/generated/client/wasm.js +28 -9
  35. package/dist/index.d.ts +2 -2
  36. package/dist/index.js +1 -1
  37. package/dist/offline.d.ts +34 -1
  38. package/dist/offline.js +2 -2
  39. package/dist/{types-CtBC5-TW.d.ts → types-BOgdk0Jw.d.ts} +119 -0
  40. package/dist/types.d.ts +93 -1
  41. package/dist/types.js +1 -1
  42. package/dist/ui-components-admin/accounts-manager.d.ts +52 -0
  43. package/dist/ui-components-admin/accounts-manager.js +471 -0
  44. package/dist/ui-components-admin/admin-nav.d.ts +15 -1
  45. package/dist/ui-components-admin/admin-nav.js +386 -60
  46. package/dist/ui-components-admin/issuance-form.js +72 -13
  47. package/dist/ui-components-admin/logs-viewer.d.ts +13 -0
  48. package/dist/ui-components-admin/logs-viewer.js +102 -0
  49. package/dist/ui-components-admin/notifications-list.d.ts +5 -0
  50. package/dist/ui-components-admin/notifications-list.js +70 -0
  51. package/dist/ui-components-admin/officers-manager.d.ts +27 -0
  52. package/dist/ui-components-admin/officers-manager.js +271 -0
  53. package/dist/ui-components-admin/roles-manager.d.ts +37 -0
  54. package/dist/ui-components-admin/roles-manager.js +406 -0
  55. package/dist/ui-components-admin/ticket-preview.js +7 -7
  56. package/dist/ui-components-admin/tickets-table.js +56 -33
  57. package/dist/ui-components-citizen/citizen-nav.js +2 -2
  58. package/dist/ui-components-citizen/payment-form.js +5 -5
  59. package/dist/ui-components-citizen/payment-qr-dialog.js +4 -4
  60. package/dist/ui-components-citizen/ticket-not-found.js +2 -2
  61. package/dist/ui-components-citizen/violation-history-table.js +3 -3
  62. package/dist/ui-components-shared/amount-summary.js +4 -4
  63. package/dist/ui-components-shared/money.js +3 -3
  64. package/dist/ui-components-shared/municipal-seal.js +3 -3
  65. package/dist/ui-components-shared/official-header.js +4 -4
  66. package/dist/ui-components-shared/site-header.js +4 -4
  67. package/dist/ui-components-shared/sonner.js +2 -2
  68. package/dist/ui-components-shared/theme-toggle.js +3 -3
  69. package/dist/ui-components-shared/ticket-receipt.js +13 -6
  70. package/dist/ui-components-shared/violations-table.js +4 -4
  71. package/dist/ui-components-ui/badge.d.ts +1 -1
  72. package/dist/ui-components-ui/button.d.ts +1 -1
  73. package/dist/ui-components-ui/dropdown-menu.js +2 -237
  74. package/dist/ui-components-ui/sheet.js +3 -126
  75. package/dist/ui-config.d.ts +1 -1
  76. package/dist/ui-config.js +2 -2
  77. package/dist/ui-server.d.ts +1 -1
  78. package/dist/ui-server.js +2 -2
  79. package/package.json +4 -4
  80. package/prisma/migrations/20260622010000_add_super_admin_role/migration.sql +3 -0
  81. package/prisma/migrations/20260622020000_add_apprehending_enforcer/migration.sql +4 -0
  82. package/prisma/migrations/20260622030000_custom_roles/migration.sql +30 -0
  83. package/prisma/migrations/20260622040000_add_activity_log/migration.sql +18 -0
  84. package/prisma/schema.prisma +46 -9
  85. package/dist/chunk-B634JHKZ.js +0 -181
@@ -0,0 +1,102 @@
1
+ "use client";
2
+ import { Card, CardContent } from '../chunk-SETIN6XP.js';
3
+ import { usePagination, Pagination } from '../chunk-6YFZLXFP.js';
4
+ import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from '../chunk-OWCGEEAZ.js';
5
+ import { Badge } from '../chunk-55FQP2DO.js';
6
+ import { Input } from '../chunk-K3KIBHJF.js';
7
+ import '../chunk-I4WDVYHX.js';
8
+ import { useCopy, useOvrConfig } from '../chunk-TJSNVTVB.js';
9
+ import '../chunk-77QBZC7J.js';
10
+ import { ACTIVITY_ACTION_LABELS } from '../chunk-IS3THKTE.js';
11
+ import '../chunk-BI4EGLPG.js';
12
+ import * as React from 'react';
13
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
14
+
15
+ var fieldClass = "h-9 rounded-lg border border-input bg-transparent px-2.5 text-sm shadow-xs outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 dark:bg-input/30";
16
+ function actionLabel(action) {
17
+ return ACTIVITY_ACTION_LABELS[action] ?? action;
18
+ }
19
+ function LogsViewer({ entries }) {
20
+ const t = useCopy().admin.logsPage;
21
+ const { rules } = useOvrConfig();
22
+ const [action, setAction] = React.useState("");
23
+ const [q, setQ] = React.useState("");
24
+ const fmt = (iso) => new Date(iso).toLocaleString(rules.locale, {
25
+ timeZone: rules.timeZone,
26
+ year: "numeric",
27
+ month: "short",
28
+ day: "2-digit",
29
+ hour: "numeric",
30
+ minute: "2-digit"
31
+ });
32
+ const actions = Array.from(new Set(entries.map((e) => e.action)));
33
+ const query = q.trim().toLowerCase();
34
+ const filtered = entries.filter((e) => {
35
+ if (action && e.action !== action) return false;
36
+ if (query) {
37
+ const hay = `${e.actorUsername ?? ""} ${e.summary}`.toLowerCase();
38
+ if (!hay.includes(query)) return false;
39
+ }
40
+ return true;
41
+ });
42
+ const { pageItems, page, setPage, totalPages, from, to, total } = usePagination(filtered, 20, `${action}|${query}`);
43
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-4xl p-4 sm:p-6 lg:p-8", children: [
44
+ /* @__PURE__ */ jsxs("div", { className: "mb-6", children: [
45
+ /* @__PURE__ */ jsx("h1", { className: "font-heading text-2xl font-semibold tracking-tight", children: t.title }),
46
+ /* @__PURE__ */ jsx("p", { className: "max-w-2xl text-sm text-muted-foreground", children: t.subtitle })
47
+ ] }),
48
+ /* @__PURE__ */ jsxs("div", { className: "mb-4 flex flex-wrap items-center gap-2", children: [
49
+ /* @__PURE__ */ jsxs(
50
+ "select",
51
+ {
52
+ value: action,
53
+ onChange: (e) => setAction(e.target.value),
54
+ className: `${fieldClass} w-auto`,
55
+ "aria-label": t.action,
56
+ children: [
57
+ /* @__PURE__ */ jsx("option", { value: "", children: t.allActions }),
58
+ actions.map((a) => /* @__PURE__ */ jsx("option", { value: a, children: actionLabel(a) }, a))
59
+ ]
60
+ }
61
+ ),
62
+ /* @__PURE__ */ jsx(
63
+ Input,
64
+ {
65
+ value: q,
66
+ onChange: (e) => setQ(e.target.value),
67
+ placeholder: t.search,
68
+ className: "h-9 w-auto min-w-56"
69
+ }
70
+ )
71
+ ] }),
72
+ /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { className: "p-0", children: filtered.length === 0 ? /* @__PURE__ */ jsx("p", { className: "p-8 text-center text-sm text-muted-foreground", children: t.empty }) : /* @__PURE__ */ jsxs(Fragment, { children: [
73
+ /* @__PURE__ */ jsxs(Table, { children: [
74
+ /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
75
+ /* @__PURE__ */ jsx(TableHead, { children: t.when }),
76
+ /* @__PURE__ */ jsx(TableHead, { children: t.actor }),
77
+ /* @__PURE__ */ jsx(TableHead, { children: t.action }),
78
+ /* @__PURE__ */ jsx(TableHead, { children: t.detail })
79
+ ] }) }),
80
+ /* @__PURE__ */ jsx(TableBody, { children: pageItems.map((e) => /* @__PURE__ */ jsxs(TableRow, { children: [
81
+ /* @__PURE__ */ jsx(TableCell, { className: "whitespace-nowrap text-muted-foreground", children: fmt(e.createdAt) }),
82
+ /* @__PURE__ */ jsx(TableCell, { className: "font-medium", children: e.actorUsername ?? t.system }),
83
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(Badge, { variant: "secondary", children: actionLabel(e.action) }) }),
84
+ /* @__PURE__ */ jsx(TableCell, { className: "text-muted-foreground", children: e.summary })
85
+ ] }, e.id)) })
86
+ ] }),
87
+ /* @__PURE__ */ jsx(
88
+ Pagination,
89
+ {
90
+ page,
91
+ totalPages,
92
+ from,
93
+ to,
94
+ total,
95
+ onPage: setPage
96
+ }
97
+ )
98
+ ] }) }) })
99
+ ] });
100
+ }
101
+
102
+ export { LogsViewer };
@@ -0,0 +1,5 @@
1
+ import * as React from 'react';
2
+
3
+ declare function NotificationsList(): React.JSX.Element;
4
+
5
+ export { NotificationsList };
@@ -0,0 +1,70 @@
1
+ "use client";
2
+ import { Card, CardContent } from '../chunk-SETIN6XP.js';
3
+ import { usePagination, Pagination } from '../chunk-6YFZLXFP.js';
4
+ import { Badge } from '../chunk-55FQP2DO.js';
5
+ import '../chunk-I4WDVYHX.js';
6
+ import { useCopy, useOvrConfig } from '../chunk-TJSNVTVB.js';
7
+ import '../chunk-77QBZC7J.js';
8
+ import { useNotifications } from '../chunk-YGYA7KEG.js';
9
+ import '../chunk-BI4EGLPG.js';
10
+ import Link from 'next/link';
11
+ import { AlertTriangle, Clock } from 'lucide-react';
12
+ import { jsxs, jsx } from 'react/jsx-runtime';
13
+
14
+ function NotificationsList() {
15
+ const items = useNotifications();
16
+ const t = useCopy().admin.notifications;
17
+ const { rules } = useOvrConfig();
18
+ const { pageItems, page, setPage, totalPages, from, to, total } = usePagination(items, 20);
19
+ const fmt = (iso) => new Date(iso).toLocaleString(rules.locale, {
20
+ timeZone: rules.timeZone,
21
+ year: "numeric",
22
+ month: "short",
23
+ day: "2-digit",
24
+ hour: "numeric",
25
+ minute: "2-digit"
26
+ });
27
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-3xl p-4 sm:p-6 lg:p-8", children: [
28
+ /* @__PURE__ */ jsxs("div", { className: "mb-6", children: [
29
+ /* @__PURE__ */ jsx("h1", { className: "font-heading text-2xl font-semibold tracking-tight", children: t.title }),
30
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t.subtitle })
31
+ ] }),
32
+ /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "p-0", children: [
33
+ items.length === 0 ? /* @__PURE__ */ jsx("p", { className: "p-8 text-center text-sm text-muted-foreground", children: t.empty }) : /* @__PURE__ */ jsx("ul", { className: "divide-y", children: pageItems.map((n) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
34
+ Link,
35
+ {
36
+ href: n.href,
37
+ className: "flex items-center gap-3 px-4 py-3 transition-colors hover:bg-muted/50",
38
+ children: [
39
+ n.type === "overdue" ? /* @__PURE__ */ jsx(AlertTriangle, { className: "size-4 shrink-0 text-red-500" }) : /* @__PURE__ */ jsx(Clock, { className: "size-4 shrink-0 text-amber-500" }),
40
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
41
+ /* @__PURE__ */ jsx("span", { className: "block truncate text-sm font-medium", children: n.name }),
42
+ /* @__PURE__ */ jsx("span", { className: "block font-mono text-xs text-muted-foreground", children: n.ovrTicketNo })
43
+ ] }),
44
+ /* @__PURE__ */ jsx(
45
+ Badge,
46
+ {
47
+ variant: n.type === "overdue" ? "destructive" : "secondary",
48
+ children: n.type === "overdue" ? t.overdue : t.outstanding
49
+ }
50
+ ),
51
+ /* @__PURE__ */ jsx("span", { className: "hidden whitespace-nowrap text-xs text-muted-foreground sm:block", children: fmt(n.at) })
52
+ ]
53
+ }
54
+ ) }, n.id)) }),
55
+ /* @__PURE__ */ jsx(
56
+ Pagination,
57
+ {
58
+ page,
59
+ totalPages,
60
+ from,
61
+ to,
62
+ total,
63
+ onPage: setPage
64
+ }
65
+ )
66
+ ] }) })
67
+ ] });
68
+ }
69
+
70
+ export { NotificationsList };
@@ -0,0 +1,27 @@
1
+ import * as React from 'react';
2
+ import { NewOfficerInput, Officer } from '../types.js';
3
+
4
+ /**
5
+ * Apprehending-officers management (GE-025). Add / edit / remove the Officer
6
+ * records used in the Issue-Ticket dropdown and linked to enforcer accounts.
7
+ * Server actions are INJECTED by the page (mirrors AccountsManager).
8
+ */
9
+
10
+ type CreateOfficerAction = (input: NewOfficerInput) => Promise<{
11
+ error?: string;
12
+ } | void>;
13
+ type UpdateOfficerAction = (id: string, patch: Partial<NewOfficerInput>) => Promise<{
14
+ error?: string;
15
+ } | void>;
16
+ type DeleteOfficerAction = (id: string) => Promise<{
17
+ error?: string;
18
+ } | void>;
19
+ interface OfficersManagerProps {
20
+ officers: Officer[];
21
+ createAction: CreateOfficerAction;
22
+ updateAction: UpdateOfficerAction;
23
+ deleteAction: DeleteOfficerAction;
24
+ }
25
+ declare function OfficersManager({ officers, createAction, updateAction, deleteAction, }: OfficersManagerProps): React.JSX.Element;
26
+
27
+ export { type CreateOfficerAction, type DeleteOfficerAction, OfficersManager, type OfficersManagerProps, type UpdateOfficerAction };
@@ -0,0 +1,271 @@
1
+ "use client";
2
+ import { Card, CardContent } from '../chunk-SETIN6XP.js';
3
+ import { usePagination, Pagination } from '../chunk-6YFZLXFP.js';
4
+ import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from '../chunk-OWCGEEAZ.js';
5
+ import { Label } from '../chunk-XQTVSNHC.js';
6
+ import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from '../chunk-M35R6JLA.js';
7
+ import { Input } from '../chunk-K3KIBHJF.js';
8
+ import { Button } from '../chunk-I4WDVYHX.js';
9
+ import { useCopy } from '../chunk-TJSNVTVB.js';
10
+ import '../chunk-77QBZC7J.js';
11
+ import '../chunk-BI4EGLPG.js';
12
+ import * as React from 'react';
13
+ import { toast } from 'sonner';
14
+ import { useRouter } from 'next/navigation';
15
+ import { UserPlus, Pencil, Loader2, Trash2 } from 'lucide-react';
16
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
17
+
18
+ function OfficersManager({
19
+ officers,
20
+ createAction,
21
+ updateAction,
22
+ deleteAction
23
+ }) {
24
+ const t = useCopy().admin.officersPage;
25
+ const router = useRouter();
26
+ const { pageItems, page, setPage, totalPages, from, to, total } = usePagination(officers, 12);
27
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-4xl p-4 sm:p-6 lg:p-8", children: [
28
+ /* @__PURE__ */ jsxs("div", { className: "mb-6 flex items-center justify-between gap-3", children: [
29
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
30
+ /* @__PURE__ */ jsx("h1", { className: "font-heading text-2xl font-semibold tracking-tight", children: t.title }),
31
+ /* @__PURE__ */ jsx("p", { className: "max-w-2xl text-sm text-muted-foreground", children: t.subtitle })
32
+ ] }),
33
+ /* @__PURE__ */ jsx(
34
+ OfficerDialog,
35
+ {
36
+ mode: "create",
37
+ createAction,
38
+ onDone: () => router.refresh()
39
+ }
40
+ )
41
+ ] }),
42
+ /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { className: "p-0", children: officers.length === 0 ? /* @__PURE__ */ jsx("p", { className: "p-8 text-center text-sm text-muted-foreground", children: t.empty }) : /* @__PURE__ */ jsxs(Fragment, { children: [
43
+ /* @__PURE__ */ jsxs(Table, { children: [
44
+ /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
45
+ /* @__PURE__ */ jsx(TableHead, { children: t.name }),
46
+ /* @__PURE__ */ jsx(TableHead, { children: t.badge }),
47
+ /* @__PURE__ */ jsx(TableHead, { children: t.office }),
48
+ /* @__PURE__ */ jsx(TableHead, { className: "text-right", children: t.actions })
49
+ ] }) }),
50
+ /* @__PURE__ */ jsx(TableBody, { children: pageItems.map((o) => /* @__PURE__ */ jsxs(TableRow, { children: [
51
+ /* @__PURE__ */ jsx(TableCell, { className: "font-medium", children: o.name }),
52
+ /* @__PURE__ */ jsx(TableCell, { className: "text-muted-foreground", children: o.badgeNo ?? "\u2014" }),
53
+ /* @__PURE__ */ jsx(TableCell, { className: "text-muted-foreground", children: o.office }),
54
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-1.5", children: [
55
+ /* @__PURE__ */ jsx(
56
+ OfficerDialog,
57
+ {
58
+ mode: "edit",
59
+ officer: o,
60
+ updateAction,
61
+ onDone: () => router.refresh()
62
+ }
63
+ ),
64
+ /* @__PURE__ */ jsx(
65
+ DeleteOfficerButton,
66
+ {
67
+ officer: o,
68
+ deleteAction,
69
+ onDone: () => router.refresh()
70
+ }
71
+ )
72
+ ] }) })
73
+ ] }, o.id)) })
74
+ ] }),
75
+ /* @__PURE__ */ jsx(
76
+ Pagination,
77
+ {
78
+ page,
79
+ totalPages,
80
+ from,
81
+ to,
82
+ total,
83
+ onPage: setPage
84
+ }
85
+ )
86
+ ] }) }) })
87
+ ] });
88
+ }
89
+ function OfficerDialog({
90
+ mode,
91
+ officer,
92
+ createAction,
93
+ updateAction,
94
+ onDone
95
+ }) {
96
+ const t = useCopy().admin.officersPage;
97
+ const [open, setOpen] = React.useState(false);
98
+ const [name, setName] = React.useState(officer?.name ?? "");
99
+ const [badgeNo, setBadgeNo] = React.useState(officer?.badgeNo ?? "");
100
+ const [office, setOffice] = React.useState(officer?.office ?? "");
101
+ const [submitting, setSubmitting] = React.useState(false);
102
+ function resetTo(o) {
103
+ setName(o?.name ?? "");
104
+ setBadgeNo(o?.badgeNo ?? "");
105
+ setOffice(o?.office ?? "");
106
+ }
107
+ async function submit(e) {
108
+ e.preventDefault();
109
+ if (!name.trim()) return toast.error(`${t.name} is required.`);
110
+ if (!office.trim()) return toast.error(`${t.office} is required.`);
111
+ setSubmitting(true);
112
+ try {
113
+ const input = {
114
+ name: name.trim(),
115
+ badgeNo: badgeNo.trim() || void 0,
116
+ office: office.trim()
117
+ };
118
+ const res = mode === "create" ? await createAction?.(input) : await updateAction?.(officer.id, input);
119
+ if (res?.error) {
120
+ toast.error(res.error);
121
+ return;
122
+ }
123
+ toast.success(
124
+ `${mode === "create" ? t.create : t.editTitle}: ${name.trim()}`
125
+ );
126
+ setOpen(false);
127
+ onDone();
128
+ } finally {
129
+ setSubmitting(false);
130
+ }
131
+ }
132
+ return /* @__PURE__ */ jsxs(
133
+ Dialog,
134
+ {
135
+ open,
136
+ onOpenChange: (o) => {
137
+ if (o) resetTo(officer);
138
+ setOpen(o);
139
+ },
140
+ children: [
141
+ /* @__PURE__ */ jsxs(
142
+ DialogTrigger,
143
+ {
144
+ render: mode === "create" ? /* @__PURE__ */ jsx(Button, { className: "gap-1.5" }) : /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", className: "gap-1.5" }),
145
+ children: [
146
+ mode === "create" ? /* @__PURE__ */ jsx(UserPlus, { className: "size-4" }) : /* @__PURE__ */ jsx(Pencil, { className: "size-3.5" }),
147
+ /* @__PURE__ */ jsx("span", { className: mode === "create" ? "" : "hidden sm:inline", children: mode === "create" ? t.newOfficer : t.edit })
148
+ ]
149
+ }
150
+ ),
151
+ /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-md", children: [
152
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
153
+ /* @__PURE__ */ jsx(DialogTitle, { children: mode === "create" ? t.newOfficer : t.editTitle }),
154
+ /* @__PURE__ */ jsx(DialogDescription, { children: t.subtitle })
155
+ ] }),
156
+ /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: "space-y-4", children: [
157
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
158
+ /* @__PURE__ */ jsx(Label, { htmlFor: "off-name", children: t.name }),
159
+ /* @__PURE__ */ jsx(
160
+ Input,
161
+ {
162
+ id: "off-name",
163
+ value: name,
164
+ onChange: (e) => setName(e.target.value),
165
+ placeholder: t.namePlaceholder,
166
+ autoComplete: "off",
167
+ autoFocus: true
168
+ }
169
+ )
170
+ ] }),
171
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [
172
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
173
+ /* @__PURE__ */ jsx(Label, { htmlFor: "off-badge", children: t.badge }),
174
+ /* @__PURE__ */ jsx(
175
+ Input,
176
+ {
177
+ id: "off-badge",
178
+ value: badgeNo,
179
+ onChange: (e) => setBadgeNo(e.target.value),
180
+ placeholder: t.badgePlaceholder,
181
+ autoComplete: "off"
182
+ }
183
+ )
184
+ ] }),
185
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
186
+ /* @__PURE__ */ jsx(Label, { htmlFor: "off-office", children: t.office }),
187
+ /* @__PURE__ */ jsx(
188
+ Input,
189
+ {
190
+ id: "off-office",
191
+ value: office,
192
+ onChange: (e) => setOffice(e.target.value),
193
+ placeholder: t.officePlaceholder,
194
+ autoComplete: "off"
195
+ }
196
+ )
197
+ ] })
198
+ ] }),
199
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
200
+ /* @__PURE__ */ jsx(DialogClose, { render: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline" }), children: t.cancel }),
201
+ /* @__PURE__ */ jsxs(Button, { type: "submit", disabled: submitting, className: "gap-1.5", children: [
202
+ submitting ? /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin" }) : null,
203
+ submitting ? mode === "create" ? t.creating : t.saving : mode === "create" ? t.create : t.save
204
+ ] })
205
+ ] })
206
+ ] })
207
+ ] })
208
+ ]
209
+ }
210
+ );
211
+ }
212
+ function DeleteOfficerButton({
213
+ officer,
214
+ deleteAction,
215
+ onDone
216
+ }) {
217
+ const t = useCopy().admin.officersPage;
218
+ const [deleting, setDeleting] = React.useState(false);
219
+ async function remove() {
220
+ setDeleting(true);
221
+ try {
222
+ const res = await deleteAction(officer.id);
223
+ if (res?.error) {
224
+ toast.error(res.error);
225
+ return;
226
+ }
227
+ toast.success(`${t.delete}: ${officer.name}`);
228
+ onDone();
229
+ } finally {
230
+ setDeleting(false);
231
+ }
232
+ }
233
+ return /* @__PURE__ */ jsxs(Dialog, { children: [
234
+ /* @__PURE__ */ jsxs(
235
+ DialogTrigger,
236
+ {
237
+ render: /* @__PURE__ */ jsx(Button, { variant: "destructive", size: "sm", className: "gap-1.5" }),
238
+ children: [
239
+ /* @__PURE__ */ jsx(Trash2, { className: "size-3.5" }),
240
+ /* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: t.delete })
241
+ ]
242
+ }
243
+ ),
244
+ /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-sm", children: [
245
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
246
+ /* @__PURE__ */ jsx(DialogTitle, { children: t.deleteConfirmTitle }),
247
+ /* @__PURE__ */ jsxs(DialogDescription, { children: [
248
+ officer.name,
249
+ " \u2014 ",
250
+ t.deleteConfirmBody
251
+ ] })
252
+ ] }),
253
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
254
+ /* @__PURE__ */ jsx(DialogClose, { render: /* @__PURE__ */ jsx(Button, { variant: "outline" }), children: t.cancel }),
255
+ /* @__PURE__ */ jsxs(
256
+ DialogClose,
257
+ {
258
+ render: /* @__PURE__ */ jsx(Button, { variant: "destructive", className: "gap-2", disabled: deleting }),
259
+ onClick: remove,
260
+ children: [
261
+ /* @__PURE__ */ jsx(Trash2, { className: "size-4" }),
262
+ t.delete
263
+ ]
264
+ }
265
+ )
266
+ ] })
267
+ ] })
268
+ ] });
269
+ }
270
+
271
+ export { OfficersManager };
@@ -0,0 +1,37 @@
1
+ import * as React from 'react';
2
+ import { Permission, RoleDef } from '../types.js';
3
+
4
+ /**
5
+ * Roles & permissions editor (GE-013, custom roles). Each role is a COMPACT card
6
+ * (name + a one-line permission summary); the permission checklist lives in a
7
+ * MODAL opened by "Edit permissions". Super admins create custom roles + delete
8
+ * unused ones. SUPER_ADMIN's management permissions are locked on (anti-lockout)
9
+ * here AND re-asserted server-side.
10
+ */
11
+
12
+ type CreateRoleAction = (input: {
13
+ label: string;
14
+ permissions: Permission[];
15
+ }) => Promise<{
16
+ error?: string;
17
+ } | void>;
18
+ type UpdateRoleAction = (name: string, patch: {
19
+ label?: string;
20
+ permissions?: Permission[];
21
+ }) => Promise<{
22
+ error?: string;
23
+ } | void>;
24
+ type DeleteRoleAction = (name: string) => Promise<{
25
+ error?: string;
26
+ } | void>;
27
+ interface RolesManagerProps {
28
+ roles: RoleDef[];
29
+ /** username count per role name — shown as "N in use" + blocks delete. */
30
+ usage?: Record<string, number>;
31
+ createAction: CreateRoleAction;
32
+ updateAction: UpdateRoleAction;
33
+ deleteAction: DeleteRoleAction;
34
+ }
35
+ declare function RolesManager({ roles, usage, createAction, updateAction, deleteAction, }: RolesManagerProps): React.JSX.Element;
36
+
37
+ export { type CreateRoleAction, type DeleteRoleAction, RolesManager, type RolesManagerProps, type UpdateRoleAction };