@gelabs/ovr 0.2.2 → 0.4.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 (90) 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-DJMUW5T2.js +298 -0
  10. package/dist/{chunk-BIQ2J75Y.js → chunk-GLIK5BHP.js} +2 -2
  11. package/dist/{chunk-JEYT63LE.js → chunk-IBZVIUNI.js} +1 -1
  12. package/dist/{chunk-4SZXBT56.js → chunk-NT72CQAI.js} +2 -2
  13. package/dist/{chunk-E2D7QT6N.js → chunk-TJSNVTVB.js} +1 -1
  14. package/dist/{chunk-5Z2IAD5I.js → chunk-TLG4C2XI.js} +2 -2
  15. package/dist/chunk-V7VQVDWS.js +237 -0
  16. package/dist/chunk-WUNTHINH.js +98 -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 +330 -12
  25. package/dist/data-prisma-store.js +319 -9
  26. package/dist/data-seed-runner.js +18 -15
  27. package/dist/data.d.ts +64 -3
  28. package/dist/generated/client/edge.js +31 -10
  29. package/dist/generated/client/index-browser.js +28 -7
  30. package/dist/generated/client/index.d.ts +3583 -577
  31. package/dist/generated/client/index.js +31 -10
  32. package/dist/generated/client/package.json +1 -1
  33. package/dist/generated/client/schema.prisma +48 -9
  34. package/dist/generated/client/wasm.js +31 -10
  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-B8MopM4b.d.ts +281 -0
  40. package/dist/types.d.ts +104 -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 +388 -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-admin/violations-manager.d.ts +32 -0
  58. package/dist/ui-components-admin/violations-manager.js +385 -0
  59. package/dist/ui-components-citizen/citizen-nav.js +2 -2
  60. package/dist/ui-components-citizen/payment-form.js +5 -5
  61. package/dist/ui-components-citizen/payment-qr-dialog.js +4 -4
  62. package/dist/ui-components-citizen/ticket-not-found.js +2 -2
  63. package/dist/ui-components-citizen/violation-history-table.js +3 -3
  64. package/dist/ui-components-shared/amount-summary.js +4 -4
  65. package/dist/ui-components-shared/money.js +3 -3
  66. package/dist/ui-components-shared/municipal-seal.js +3 -3
  67. package/dist/ui-components-shared/official-header.js +4 -4
  68. package/dist/ui-components-shared/site-header.js +4 -4
  69. package/dist/ui-components-shared/sonner.js +2 -2
  70. package/dist/ui-components-shared/theme-toggle.js +3 -3
  71. package/dist/ui-components-shared/ticket-receipt.js +13 -6
  72. package/dist/ui-components-shared/violations-table.js +4 -4
  73. package/dist/ui-components-ui/badge.d.ts +1 -1
  74. package/dist/ui-components-ui/button.d.ts +1 -1
  75. package/dist/ui-components-ui/dropdown-menu.js +2 -237
  76. package/dist/ui-components-ui/sheet.js +3 -126
  77. package/dist/ui-config.d.ts +1 -1
  78. package/dist/ui-config.js +2 -2
  79. package/dist/ui-server.d.ts +1 -1
  80. package/dist/ui-server.js +2 -2
  81. package/package.json +6 -6
  82. package/prisma/migrations/20260622010000_add_super_admin_role/migration.sql +3 -0
  83. package/prisma/migrations/20260622020000_add_apprehending_enforcer/migration.sql +4 -0
  84. package/prisma/migrations/20260622030000_custom_roles/migration.sql +30 -0
  85. package/prisma/migrations/20260622040000_add_activity_log/migration.sql +18 -0
  86. package/prisma/migrations/20260622050000_violation_catalog_management/migration.sql +5 -0
  87. package/prisma/schema.prisma +48 -9
  88. package/dist/chunk-5YYR37CF.js +0 -146
  89. package/dist/chunk-B634JHKZ.js +0 -181
  90. package/dist/types-CtBC5-TW.d.ts +0 -129
@@ -0,0 +1,471 @@
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 { Label } from '../chunk-XQTVSNHC.js';
7
+ import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from '../chunk-M35R6JLA.js';
8
+ import { Input } from '../chunk-K3KIBHJF.js';
9
+ import { Button } from '../chunk-I4WDVYHX.js';
10
+ import { useCopy, useOvrConfig } from '../chunk-TJSNVTVB.js';
11
+ import '../chunk-77QBZC7J.js';
12
+ import { hasPermission } from '../chunk-WUNTHINH.js';
13
+ import '../chunk-BI4EGLPG.js';
14
+ import * as React from 'react';
15
+ import { toast } from 'sonner';
16
+ import { useRouter } from 'next/navigation';
17
+ import { Loader2, UserPlus, KeyRound, Pencil } from 'lucide-react';
18
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
19
+
20
+ var fieldClass = "h-9 w-full 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 disabled:opacity-50 dark:bg-input/30";
21
+ function AccountsManager({
22
+ users,
23
+ officers,
24
+ roles,
25
+ currentUserId,
26
+ createAction,
27
+ editAction,
28
+ setActiveAction,
29
+ resetPasswordAction
30
+ }) {
31
+ const t = useCopy().admin.accountsPage;
32
+ const { rules } = useOvrConfig();
33
+ const router = useRouter();
34
+ const [busyId, setBusyId] = React.useState(null);
35
+ const { pageItems, page, setPage, totalPages, from, to, total } = usePagination(users, 12);
36
+ const fmtDate = (iso) => new Date(iso).toLocaleDateString(rules.locale, {
37
+ timeZone: rules.timeZone,
38
+ year: "numeric",
39
+ month: "short",
40
+ day: "numeric"
41
+ });
42
+ async function toggleActive(u) {
43
+ setBusyId(u.id);
44
+ try {
45
+ const res = await setActiveAction(u.id, !u.active);
46
+ if (res?.error) {
47
+ toast.error(res.error);
48
+ return;
49
+ }
50
+ router.refresh();
51
+ } finally {
52
+ setBusyId(null);
53
+ }
54
+ }
55
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-5xl p-4 sm:p-6 lg:p-8", children: [
56
+ /* @__PURE__ */ jsxs("div", { className: "mb-6 flex items-center justify-between gap-3", children: [
57
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
58
+ /* @__PURE__ */ jsx("h1", { className: "font-heading text-2xl font-semibold tracking-tight", children: t.title }),
59
+ /* @__PURE__ */ jsx("p", { className: "max-w-2xl text-sm text-muted-foreground", children: t.subtitle })
60
+ ] }),
61
+ /* @__PURE__ */ jsx(
62
+ NewAccountDialog,
63
+ {
64
+ officers,
65
+ roles,
66
+ createAction,
67
+ onCreated: () => router.refresh()
68
+ }
69
+ )
70
+ ] }),
71
+ /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { className: "p-0", children: users.length === 0 ? /* @__PURE__ */ jsx("p", { className: "p-8 text-center text-sm text-muted-foreground", children: t.empty }) : /* @__PURE__ */ jsxs(Fragment, { children: [
72
+ /* @__PURE__ */ jsxs(Table, { children: [
73
+ /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
74
+ /* @__PURE__ */ jsx(TableHead, { children: t.username }),
75
+ /* @__PURE__ */ jsx(TableHead, { children: t.role }),
76
+ /* @__PURE__ */ jsx(TableHead, { children: t.officer }),
77
+ /* @__PURE__ */ jsx(TableHead, { children: t.status }),
78
+ /* @__PURE__ */ jsx(TableHead, { children: t.created }),
79
+ /* @__PURE__ */ jsx(TableHead, { className: "text-right", children: t.actions })
80
+ ] }) }),
81
+ /* @__PURE__ */ jsx(TableBody, { children: pageItems.map((u) => {
82
+ const isSelf = u.id === currentUserId;
83
+ return /* @__PURE__ */ jsxs(TableRow, { children: [
84
+ /* @__PURE__ */ jsxs(TableCell, { className: "font-medium", children: [
85
+ u.username,
86
+ isSelf ? /* @__PURE__ */ jsxs("span", { className: "ml-1.5 text-xs text-muted-foreground", children: [
87
+ "(",
88
+ t.you,
89
+ ")"
90
+ ] }) : null
91
+ ] }),
92
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(Badge, { variant: "secondary", children: u.roleLabel ?? u.role }) }),
93
+ /* @__PURE__ */ jsx(TableCell, { className: "text-muted-foreground", children: u.officerName ?? "\u2014" }),
94
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(Badge, { variant: u.active ? "default" : "outline", children: u.active ? t.active : t.inactive }) }),
95
+ /* @__PURE__ */ jsx(TableCell, { className: "text-muted-foreground", children: fmtDate(u.createdAt) }),
96
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-1.5", children: [
97
+ /* @__PURE__ */ jsx(
98
+ EditAccountDialog,
99
+ {
100
+ user: u,
101
+ officers,
102
+ roles,
103
+ editAction,
104
+ onSaved: () => router.refresh()
105
+ }
106
+ ),
107
+ /* @__PURE__ */ jsx(
108
+ ResetPasswordDialog,
109
+ {
110
+ user: u,
111
+ resetPasswordAction
112
+ }
113
+ ),
114
+ /* @__PURE__ */ jsxs(
115
+ Button,
116
+ {
117
+ variant: u.active ? "destructive" : "outline",
118
+ size: "sm",
119
+ disabled: isSelf || busyId === u.id,
120
+ onClick: () => toggleActive(u),
121
+ title: isSelf ? "You can't deactivate yourself." : void 0,
122
+ children: [
123
+ busyId === u.id ? /* @__PURE__ */ jsx(Loader2, { className: "size-3.5 animate-spin" }) : null,
124
+ u.active ? t.deactivate : t.activate
125
+ ]
126
+ }
127
+ )
128
+ ] }) })
129
+ ] }, u.id);
130
+ }) })
131
+ ] }),
132
+ /* @__PURE__ */ jsx(
133
+ Pagination,
134
+ {
135
+ page,
136
+ totalPages,
137
+ from,
138
+ to,
139
+ total,
140
+ onPage: setPage
141
+ }
142
+ )
143
+ ] }) }) })
144
+ ] });
145
+ }
146
+ function NewAccountDialog({
147
+ officers,
148
+ roles,
149
+ createAction,
150
+ onCreated
151
+ }) {
152
+ const t = useCopy().admin.accountsPage;
153
+ const defaultRole = roles.find((r) => r.name === "ENFORCER")?.name ?? roles[0]?.name ?? "";
154
+ const [open, setOpen] = React.useState(false);
155
+ const [username, setUsername] = React.useState("");
156
+ const [password, setPassword] = React.useState("");
157
+ const [role, setRole] = React.useState(defaultRole);
158
+ const [officerId, setOfficerId] = React.useState("");
159
+ const [submitting, setSubmitting] = React.useState(false);
160
+ const selectedRole = roles.find((r) => r.name === role);
161
+ const showOfficer = hasPermission(selectedRole?.permissions, "tickets:create");
162
+ function reset() {
163
+ setUsername("");
164
+ setPassword("");
165
+ setRole(defaultRole);
166
+ setOfficerId("");
167
+ }
168
+ async function submit(e) {
169
+ e.preventDefault();
170
+ if (!username.trim()) return toast.error(`${t.username} is required.`);
171
+ if (password.length < 6)
172
+ return toast.error("Password must be at least 6 characters.");
173
+ if (!role) return toast.error(`${t.role} is required.`);
174
+ setSubmitting(true);
175
+ try {
176
+ const res = await createAction({
177
+ username: username.trim(),
178
+ password,
179
+ role,
180
+ officerId: showOfficer && officerId ? officerId : null
181
+ });
182
+ if (res?.error) {
183
+ toast.error(res.error);
184
+ return;
185
+ }
186
+ toast.success(`Account "${username.trim()}" created.`);
187
+ reset();
188
+ setOpen(false);
189
+ onCreated();
190
+ } finally {
191
+ setSubmitting(false);
192
+ }
193
+ }
194
+ return /* @__PURE__ */ jsxs(Dialog, { open, onOpenChange: setOpen, children: [
195
+ /* @__PURE__ */ jsxs(DialogTrigger, { render: /* @__PURE__ */ jsx(Button, { className: "gap-1.5" }), children: [
196
+ /* @__PURE__ */ jsx(UserPlus, { className: "size-4" }),
197
+ t.newAccount
198
+ ] }),
199
+ /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-md", children: [
200
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
201
+ /* @__PURE__ */ jsx(DialogTitle, { children: t.newAccount }),
202
+ /* @__PURE__ */ jsx(DialogDescription, { children: t.subtitle })
203
+ ] }),
204
+ /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: "space-y-4", children: [
205
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
206
+ /* @__PURE__ */ jsx(Label, { htmlFor: "acc-username", children: t.username }),
207
+ /* @__PURE__ */ jsx(
208
+ Input,
209
+ {
210
+ id: "acc-username",
211
+ value: username,
212
+ onChange: (e) => setUsername(e.target.value),
213
+ autoComplete: "off",
214
+ autoFocus: true
215
+ }
216
+ )
217
+ ] }),
218
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
219
+ /* @__PURE__ */ jsx(Label, { htmlFor: "acc-password", children: t.password }),
220
+ /* @__PURE__ */ jsx(
221
+ Input,
222
+ {
223
+ id: "acc-password",
224
+ type: "password",
225
+ value: password,
226
+ onChange: (e) => setPassword(e.target.value),
227
+ autoComplete: "new-password"
228
+ }
229
+ )
230
+ ] }),
231
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
232
+ /* @__PURE__ */ jsx(Label, { htmlFor: "acc-role", children: t.role }),
233
+ /* @__PURE__ */ jsx(
234
+ "select",
235
+ {
236
+ id: "acc-role",
237
+ value: role,
238
+ onChange: (e) => setRole(e.target.value),
239
+ className: fieldClass,
240
+ children: roles.map((r) => /* @__PURE__ */ jsx("option", { value: r.name, children: r.label }, r.name))
241
+ }
242
+ )
243
+ ] }),
244
+ showOfficer ? /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
245
+ /* @__PURE__ */ jsx(Label, { htmlFor: "acc-officer", children: t.officer }),
246
+ /* @__PURE__ */ jsxs(
247
+ "select",
248
+ {
249
+ id: "acc-officer",
250
+ value: officerId,
251
+ onChange: (e) => setOfficerId(e.target.value),
252
+ className: fieldClass,
253
+ children: [
254
+ /* @__PURE__ */ jsx("option", { value: "", children: t.officerNone }),
255
+ officers.map((o) => /* @__PURE__ */ jsxs("option", { value: o.id, children: [
256
+ o.name,
257
+ o.badgeNo ? ` (${o.badgeNo})` : ""
258
+ ] }, o.id))
259
+ ]
260
+ }
261
+ ),
262
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t.officerHint })
263
+ ] }) : null,
264
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
265
+ /* @__PURE__ */ jsx(DialogClose, { render: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline" }), children: t.cancel }),
266
+ /* @__PURE__ */ jsxs(Button, { type: "submit", disabled: submitting, className: "gap-1.5", children: [
267
+ submitting ? /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin" }) : /* @__PURE__ */ jsx(UserPlus, { className: "size-4" }),
268
+ submitting ? t.creating : t.create
269
+ ] })
270
+ ] })
271
+ ] })
272
+ ] })
273
+ ] });
274
+ }
275
+ function ResetPasswordDialog({
276
+ user,
277
+ resetPasswordAction
278
+ }) {
279
+ const t = useCopy().admin.accountsPage;
280
+ const [open, setOpen] = React.useState(false);
281
+ const [password, setPassword] = React.useState("");
282
+ const [submitting, setSubmitting] = React.useState(false);
283
+ async function submit(e) {
284
+ e.preventDefault();
285
+ if (password.length < 6)
286
+ return toast.error("Password must be at least 6 characters.");
287
+ setSubmitting(true);
288
+ try {
289
+ const res = await resetPasswordAction(user.id, password);
290
+ if (res?.error) {
291
+ toast.error(res.error);
292
+ return;
293
+ }
294
+ toast.success(`Password reset for "${user.username}".`);
295
+ setPassword("");
296
+ setOpen(false);
297
+ } finally {
298
+ setSubmitting(false);
299
+ }
300
+ }
301
+ return /* @__PURE__ */ jsxs(Dialog, { open, onOpenChange: setOpen, children: [
302
+ /* @__PURE__ */ jsxs(
303
+ DialogTrigger,
304
+ {
305
+ render: /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", className: "gap-1.5" }),
306
+ children: [
307
+ /* @__PURE__ */ jsx(KeyRound, { className: "size-3.5" }),
308
+ /* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: t.resetPassword })
309
+ ]
310
+ }
311
+ ),
312
+ /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-sm", children: [
313
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
314
+ /* @__PURE__ */ jsx(DialogTitle, { children: t.resetPassword }),
315
+ /* @__PURE__ */ jsxs(DialogDescription, { children: [
316
+ t.resetPassword,
317
+ " \u2014 ",
318
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: user.username })
319
+ ] })
320
+ ] }),
321
+ /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: "space-y-4", children: [
322
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
323
+ /* @__PURE__ */ jsx(Label, { htmlFor: `reset-${user.id}`, children: t.newPassword }),
324
+ /* @__PURE__ */ jsx(
325
+ Input,
326
+ {
327
+ id: `reset-${user.id}`,
328
+ type: "password",
329
+ value: password,
330
+ onChange: (e) => setPassword(e.target.value),
331
+ autoComplete: "new-password",
332
+ autoFocus: true
333
+ }
334
+ )
335
+ ] }),
336
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
337
+ /* @__PURE__ */ jsx(DialogClose, { render: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline" }), children: t.cancel }),
338
+ /* @__PURE__ */ jsxs(Button, { type: "submit", disabled: submitting, className: "gap-1.5", children: [
339
+ submitting ? /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin" }) : null,
340
+ submitting ? t.saving : t.save
341
+ ] })
342
+ ] })
343
+ ] })
344
+ ] })
345
+ ] });
346
+ }
347
+ function EditAccountDialog({
348
+ user,
349
+ officers,
350
+ roles,
351
+ editAction,
352
+ onSaved
353
+ }) {
354
+ const t = useCopy().admin.accountsPage;
355
+ const [open, setOpen] = React.useState(false);
356
+ const [username, setUsername] = React.useState(user.username);
357
+ const [role, setRole] = React.useState(user.role);
358
+ const [officerId, setOfficerId] = React.useState(user.officerId ?? "");
359
+ const [submitting, setSubmitting] = React.useState(false);
360
+ const selectedRole = roles.find((r) => r.name === role);
361
+ const showOfficer = hasPermission(selectedRole?.permissions, "tickets:create");
362
+ async function submit(e) {
363
+ e.preventDefault();
364
+ if (!username.trim()) return toast.error(`${t.username} is required.`);
365
+ setSubmitting(true);
366
+ try {
367
+ const res = await editAction(user.id, {
368
+ username: username.trim(),
369
+ role,
370
+ officerId: showOfficer && officerId ? officerId : null
371
+ });
372
+ if (res?.error) {
373
+ toast.error(res.error);
374
+ return;
375
+ }
376
+ toast.success(`${t.editTitle}: ${username.trim()}`);
377
+ setOpen(false);
378
+ onSaved();
379
+ } finally {
380
+ setSubmitting(false);
381
+ }
382
+ }
383
+ return /* @__PURE__ */ jsxs(
384
+ Dialog,
385
+ {
386
+ open,
387
+ onOpenChange: (o) => {
388
+ if (o) {
389
+ setUsername(user.username);
390
+ setRole(user.role);
391
+ setOfficerId(user.officerId ?? "");
392
+ }
393
+ setOpen(o);
394
+ },
395
+ children: [
396
+ /* @__PURE__ */ jsxs(
397
+ DialogTrigger,
398
+ {
399
+ render: /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", className: "gap-1.5" }),
400
+ children: [
401
+ /* @__PURE__ */ jsx(Pencil, { className: "size-3.5" }),
402
+ /* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: t.edit })
403
+ ]
404
+ }
405
+ ),
406
+ /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-md", children: [
407
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
408
+ /* @__PURE__ */ jsx(DialogTitle, { children: t.editTitle }),
409
+ /* @__PURE__ */ jsx(DialogDescription, { children: user.username })
410
+ ] }),
411
+ /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: "space-y-4", children: [
412
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
413
+ /* @__PURE__ */ jsx(Label, { htmlFor: `edit-username-${user.id}`, children: t.username }),
414
+ /* @__PURE__ */ jsx(
415
+ Input,
416
+ {
417
+ id: `edit-username-${user.id}`,
418
+ value: username,
419
+ onChange: (e) => setUsername(e.target.value),
420
+ autoComplete: "off"
421
+ }
422
+ )
423
+ ] }),
424
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
425
+ /* @__PURE__ */ jsx(Label, { htmlFor: `edit-role-${user.id}`, children: t.role }),
426
+ /* @__PURE__ */ jsx(
427
+ "select",
428
+ {
429
+ id: `edit-role-${user.id}`,
430
+ value: role,
431
+ onChange: (e) => setRole(e.target.value),
432
+ className: fieldClass,
433
+ children: roles.map((r) => /* @__PURE__ */ jsx("option", { value: r.name, children: r.label }, r.name))
434
+ }
435
+ )
436
+ ] }),
437
+ showOfficer ? /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
438
+ /* @__PURE__ */ jsx(Label, { htmlFor: `edit-officer-${user.id}`, children: t.officer }),
439
+ /* @__PURE__ */ jsxs(
440
+ "select",
441
+ {
442
+ id: `edit-officer-${user.id}`,
443
+ value: officerId,
444
+ onChange: (e) => setOfficerId(e.target.value),
445
+ className: fieldClass,
446
+ children: [
447
+ /* @__PURE__ */ jsx("option", { value: "", children: t.officerNone }),
448
+ officers.map((o) => /* @__PURE__ */ jsxs("option", { value: o.id, children: [
449
+ o.name,
450
+ o.badgeNo ? ` (${o.badgeNo})` : ""
451
+ ] }, o.id))
452
+ ]
453
+ }
454
+ ),
455
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t.officerHint })
456
+ ] }) : null,
457
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
458
+ /* @__PURE__ */ jsx(DialogClose, { render: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline" }), children: t.cancel }),
459
+ /* @__PURE__ */ jsxs(Button, { type: "submit", disabled: submitting, className: "gap-1.5", children: [
460
+ submitting ? /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin" }) : /* @__PURE__ */ jsx(Pencil, { className: "size-4" }),
461
+ submitting ? t.saving : t.saveChanges
462
+ ] })
463
+ ] })
464
+ ] })
465
+ ] })
466
+ ]
467
+ }
468
+ );
469
+ }
470
+
471
+ export { AccountsManager };
@@ -1,7 +1,21 @@
1
1
  import * as React from 'react';
2
2
 
3
- declare function AdminNav({ signOutAction, }: {
3
+ /**
4
+ * Self-service change-password dialog (GE-024). Controlled by the user menu. On
5
+ * success it re-caches the offline-login verifier with the new password so
6
+ * offline login keeps working. The actual verify+update is the injected server
7
+ * action (argon2 stays server-side).
8
+ */
9
+
10
+ type ChangePasswordAction = (currentPassword: string, newPassword: string) => Promise<{
11
+ error?: string;
12
+ } | void>;
13
+
14
+ declare function AdminNav({ signOutAction, changePasswordAction, }: {
4
15
  signOutAction: () => Promise<void>;
16
+ /** Optional self-service password change (GE-024). When provided, a
17
+ * "Change password" item appears in the user menu. */
18
+ changePasswordAction?: ChangePasswordAction;
5
19
  }): React.JSX.Element;
6
20
 
7
21
  export { AdminNav };