@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
@@ -1,85 +1,411 @@
1
1
  "use client";
2
- import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from '../chunk-M35R6JLA.js';
2
+ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from '../chunk-V7VQVDWS.js';
3
+ import { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter } from '../chunk-3YKVH4Y7.js';
4
+ import { Label } from '../chunk-XQTVSNHC.js';
5
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from '../chunk-M35R6JLA.js';
6
+ import { Input } from '../chunk-K3KIBHJF.js';
3
7
  import { Button } from '../chunk-I4WDVYHX.js';
4
- import { useCopy } from '../chunk-E2D7QT6N.js';
8
+ import { useCopy } from '../chunk-TJSNVTVB.js';
5
9
  import { cn } from '../chunk-77QBZC7J.js';
6
- import { useIdentity } from '../chunk-IB4JVGKJ.js';
7
- import '../chunk-B634JHKZ.js';
8
- import Link from 'next/link';
10
+ import { hasPermission } from '../chunk-IS3THKTE.js';
11
+ import { useIdentity, useNotificationsState, cacheCredential } from '../chunk-YGYA7KEG.js';
12
+ import '../chunk-BI4EGLPG.js';
13
+ import * as React from 'react';
14
+ import Link2 from 'next/link';
9
15
  import { usePathname } from 'next/navigation';
10
- import { LayoutDashboard, ListChecks, FilePlus2, LogOut } from 'lucide-react';
11
- import { jsxs, jsx } from 'react/jsx-runtime';
16
+ import { LayoutDashboard, ListChecks, Users, ShieldCheck, BadgeCheck, ScrollText, MoreHorizontal, ChevronDown, UserRound, KeyRound, LogOut, Menu, Bell, Loader2 } from 'lucide-react';
17
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
18
+ import { toast } from 'sonner';
12
19
 
20
+ var BELL_LIMIT = 6;
21
+ function NotificationBell() {
22
+ const { items, total, unseen, markSeen } = useNotificationsState();
23
+ const t = useCopy().admin.notifications;
24
+ const hasOverdue = items.some((n) => n.type === "overdue");
25
+ const top = items.slice(0, BELL_LIMIT);
26
+ return /* @__PURE__ */ jsxs(
27
+ DropdownMenu,
28
+ {
29
+ onOpenChange: (open) => {
30
+ if (open) markSeen();
31
+ },
32
+ children: [
33
+ /* @__PURE__ */ jsxs(
34
+ DropdownMenuTrigger,
35
+ {
36
+ render: /* @__PURE__ */ jsx(
37
+ "button",
38
+ {
39
+ type: "button",
40
+ "aria-label": t.title,
41
+ className: "relative flex items-center rounded-md px-2 py-1.5 text-sm font-medium text-gov-foreground/80 transition-colors hover:bg-white/10 hover:text-gov-foreground aria-expanded:bg-white/10 aria-expanded:text-gov-foreground"
42
+ }
43
+ ),
44
+ children: [
45
+ /* @__PURE__ */ jsx(Bell, { className: "size-4 shrink-0" }),
46
+ unseen ? /* @__PURE__ */ jsx(
47
+ "span",
48
+ {
49
+ className: cn(
50
+ "absolute -top-0.5 -right-0.5 flex min-w-4 items-center justify-center rounded-full px-1 text-[0.625rem] leading-4 font-semibold text-white",
51
+ hasOverdue ? "bg-red-500" : "bg-amber-500"
52
+ ),
53
+ children: total > 99 ? "99+" : total
54
+ }
55
+ ) : null
56
+ ]
57
+ }
58
+ ),
59
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", className: "min-w-72", children: [
60
+ /* @__PURE__ */ jsx("div", { className: "px-2 py-1.5 text-sm font-medium", children: t.title }),
61
+ /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
62
+ items.length === 0 ? /* @__PURE__ */ jsx("div", { className: "px-2 py-3 text-center text-sm text-muted-foreground", children: t.empty }) : /* @__PURE__ */ jsxs(Fragment, { children: [
63
+ top.map((n) => /* @__PURE__ */ jsxs(DropdownMenuItem, { render: /* @__PURE__ */ jsx(Link2, { href: n.href }), children: [
64
+ /* @__PURE__ */ jsx(
65
+ "span",
66
+ {
67
+ className: cn(
68
+ "mt-1 size-2 shrink-0 rounded-full",
69
+ n.type === "overdue" ? "bg-red-500" : "bg-amber-500"
70
+ )
71
+ }
72
+ ),
73
+ /* @__PURE__ */ jsxs("span", { className: "flex-1", children: [
74
+ /* @__PURE__ */ jsx("span", { className: "block font-mono text-xs", children: n.ovrTicketNo }),
75
+ /* @__PURE__ */ jsxs("span", { className: "block text-xs text-muted-foreground", children: [
76
+ n.type === "overdue" ? t.overdue : t.outstanding,
77
+ " \xB7 ",
78
+ n.name
79
+ ] })
80
+ ] })
81
+ ] }, n.id)),
82
+ /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
83
+ /* @__PURE__ */ jsx(DropdownMenuItem, { render: /* @__PURE__ */ jsx(Link2, { href: "/admin/notifications" }), children: /* @__PURE__ */ jsxs("span", { className: "flex-1 text-center text-sm", children: [
84
+ t.viewAll,
85
+ total > top.length ? ` (${total})` : ""
86
+ ] }) })
87
+ ] })
88
+ ] })
89
+ ]
90
+ }
91
+ );
92
+ }
93
+ function ChangePasswordDialog({
94
+ open,
95
+ onOpenChange,
96
+ action
97
+ }) {
98
+ const t = useCopy().admin.changePassword;
99
+ const identity = useIdentity();
100
+ const [current, setCurrent] = React.useState("");
101
+ const [next, setNext] = React.useState("");
102
+ const [confirm, setConfirm] = React.useState("");
103
+ const [submitting, setSubmitting] = React.useState(false);
104
+ function reset() {
105
+ setCurrent("");
106
+ setNext("");
107
+ setConfirm("");
108
+ }
109
+ async function submit(e) {
110
+ e.preventDefault();
111
+ if (next.length < 6)
112
+ return toast.error("New password must be at least 6 characters.");
113
+ if (next !== confirm) return toast.error(t.mismatch);
114
+ setSubmitting(true);
115
+ try {
116
+ const res = await action(current, next);
117
+ if (res?.error) {
118
+ toast.error(res.error);
119
+ return;
120
+ }
121
+ if (identity) {
122
+ try {
123
+ await cacheCredential(identity.username, next, identity);
124
+ } catch {
125
+ }
126
+ }
127
+ toast.success(t.success);
128
+ reset();
129
+ onOpenChange(false);
130
+ } finally {
131
+ setSubmitting(false);
132
+ }
133
+ }
134
+ return /* @__PURE__ */ jsx(
135
+ Dialog,
136
+ {
137
+ open,
138
+ onOpenChange: (o) => {
139
+ if (!o) reset();
140
+ onOpenChange(o);
141
+ },
142
+ children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-sm", children: [
143
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
144
+ /* @__PURE__ */ jsx(DialogTitle, { children: t.title }),
145
+ /* @__PURE__ */ jsx(DialogDescription, { children: t.subtitle })
146
+ ] }),
147
+ /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: "space-y-4", children: [
148
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
149
+ /* @__PURE__ */ jsx(Label, { htmlFor: "cp-current", children: t.current }),
150
+ /* @__PURE__ */ jsx(
151
+ Input,
152
+ {
153
+ id: "cp-current",
154
+ type: "password",
155
+ value: current,
156
+ onChange: (e) => setCurrent(e.target.value),
157
+ autoComplete: "current-password",
158
+ autoFocus: true
159
+ }
160
+ )
161
+ ] }),
162
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
163
+ /* @__PURE__ */ jsx(Label, { htmlFor: "cp-new", children: t.new }),
164
+ /* @__PURE__ */ jsx(
165
+ Input,
166
+ {
167
+ id: "cp-new",
168
+ type: "password",
169
+ value: next,
170
+ onChange: (e) => setNext(e.target.value),
171
+ autoComplete: "new-password"
172
+ }
173
+ )
174
+ ] }),
175
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
176
+ /* @__PURE__ */ jsx(Label, { htmlFor: "cp-confirm", children: t.confirm }),
177
+ /* @__PURE__ */ jsx(
178
+ Input,
179
+ {
180
+ id: "cp-confirm",
181
+ type: "password",
182
+ value: confirm,
183
+ onChange: (e) => setConfirm(e.target.value),
184
+ autoComplete: "new-password"
185
+ }
186
+ )
187
+ ] }),
188
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
189
+ /* @__PURE__ */ jsx(DialogClose, { render: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline" }), children: t.cancel }),
190
+ /* @__PURE__ */ jsxs(Button, { type: "submit", disabled: submitting, className: "gap-1.5", children: [
191
+ submitting ? /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin" }) : /* @__PURE__ */ jsx(KeyRound, { className: "size-4" }),
192
+ submitting ? t.saving : t.submit
193
+ ] })
194
+ ] })
195
+ ] })
196
+ ] })
197
+ }
198
+ );
199
+ }
13
200
  function isActive(p, href) {
14
201
  if (href === "/admin") return p === "/admin";
15
202
  if (href === "/admin/tickets") {
16
203
  return p === "/admin/tickets" || p.startsWith("/admin/tickets/") && p !== "/admin/tickets/new";
17
204
  }
205
+ if (href === "/admin/accounts") return p.startsWith("/admin/accounts");
206
+ if (href === "/admin/roles") return p.startsWith("/admin/roles");
207
+ if (href === "/admin/officers") return p.startsWith("/admin/officers");
208
+ if (href === "/admin/logs") return p.startsWith("/admin/logs");
18
209
  return p === href;
19
210
  }
211
+ function itemClass(active) {
212
+ return cn(
213
+ "flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-sm font-medium transition-colors",
214
+ active ? "bg-white/15 text-gov-foreground" : "text-gov-foreground/80 hover:bg-white/10 hover:text-gov-foreground aria-expanded:bg-white/10 aria-expanded:text-gov-foreground"
215
+ );
216
+ }
20
217
  function AdminNav({
21
- signOutAction
218
+ signOutAction,
219
+ changePasswordAction
22
220
  }) {
23
221
  const pathname = usePathname();
24
222
  const copy = useCopy();
25
223
  const identity = useIdentity();
26
- const limited = identity?.role === "ENFORCER";
27
- const nav = [
28
- ...limited ? [] : [{ href: "/admin", label: copy.admin.dashboard, icon: LayoutDashboard }],
29
- { href: "/admin/tickets", label: copy.admin.tickets, icon: ListChecks },
30
- { href: "/admin/tickets/new", label: copy.admin.issueTicket, icon: FilePlus2 }
224
+ const perms = identity?.permissions ?? [];
225
+ const [signOutOpen, setSignOutOpen] = React.useState(false);
226
+ const [changePwOpen, setChangePwOpen] = React.useState(false);
227
+ const [mobileOpen, setMobileOpen] = React.useState(false);
228
+ const primaryAll = [
229
+ { href: "/admin", label: copy.admin.dashboard, icon: LayoutDashboard, perm: "dashboard:view" },
230
+ { href: "/admin/tickets", label: copy.admin.tickets, icon: ListChecks, perm: "tickets:view" }
31
231
  ];
32
- return /* @__PURE__ */ jsxs("nav", { className: "flex items-center gap-0.5 sm:gap-1", children: [
33
- nav.map((it) => {
34
- const active = isActive(pathname, it.href);
35
- const Icon = it.icon;
36
- return /* @__PURE__ */ jsxs(
37
- Link,
38
- {
39
- href: it.href,
40
- className: cn(
41
- "flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-sm font-medium transition-colors",
42
- active ? "bg-white/15 text-gov-foreground" : "text-gov-foreground/80 hover:bg-white/10 hover:text-gov-foreground"
43
- ),
44
- children: [
45
- /* @__PURE__ */ jsx(Icon, { className: "size-4 shrink-0" }),
46
- /* @__PURE__ */ jsx("span", { className: "hidden md:inline", children: it.label })
47
- ]
48
- },
49
- it.href
50
- );
51
- }),
52
- /* @__PURE__ */ jsxs(Dialog, { children: [
53
- /* @__PURE__ */ jsxs(
54
- DialogTrigger,
55
- {
56
- render: /* @__PURE__ */ jsx(
57
- "button",
232
+ const overflowAll = [
233
+ { href: "/admin/accounts", label: copy.admin.accounts, icon: Users, perm: "accounts:manage" },
234
+ { href: "/admin/roles", label: copy.admin.roles, icon: ShieldCheck, perm: "roles:manage" },
235
+ { href: "/admin/officers", label: copy.admin.officers, icon: BadgeCheck, perm: "officers:manage" },
236
+ { href: "/admin/logs", label: copy.admin.logs, icon: ScrollText, perm: "logs:view" }
237
+ ];
238
+ const primary = primaryAll.filter((it) => hasPermission(perms, it.perm));
239
+ const overflow = overflowAll.filter((it) => hasPermission(perms, it.perm));
240
+ const overflowActive = overflow.some((it) => isActive(pathname, it.href));
241
+ const allNav = [...primary, ...overflow];
242
+ const roleLabel = identity?.roleLabel ?? identity?.role ?? void 0;
243
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
244
+ /* @__PURE__ */ jsxs("nav", { className: "hidden items-center gap-0.5 md:flex md:gap-1", children: [
245
+ primary.map((it) => {
246
+ const Icon = it.icon;
247
+ return /* @__PURE__ */ jsxs(Link2, { href: it.href, className: itemClass(isActive(pathname, it.href)), children: [
248
+ /* @__PURE__ */ jsx(Icon, { className: "size-4 shrink-0" }),
249
+ /* @__PURE__ */ jsx("span", { className: "hidden md:inline", children: it.label })
250
+ ] }, it.href);
251
+ }),
252
+ overflow.length > 0 ? /* @__PURE__ */ jsxs(DropdownMenu, { children: [
253
+ /* @__PURE__ */ jsxs(
254
+ DropdownMenuTrigger,
255
+ {
256
+ render: /* @__PURE__ */ jsx("button", { type: "button", className: itemClass(overflowActive) }),
257
+ children: [
258
+ /* @__PURE__ */ jsx(MoreHorizontal, { className: "size-4 shrink-0" }),
259
+ /* @__PURE__ */ jsx("span", { className: "hidden md:inline", children: copy.admin.more }),
260
+ /* @__PURE__ */ jsx(ChevronDown, { className: "size-3.5 shrink-0 opacity-70" })
261
+ ]
262
+ }
263
+ ),
264
+ /* @__PURE__ */ jsx(DropdownMenuContent, { align: "end", className: "min-w-44", children: overflow.map((it) => {
265
+ const Icon = it.icon;
266
+ return /* @__PURE__ */ jsxs(
267
+ DropdownMenuItem,
58
268
  {
59
- type: "button",
60
- className: "flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-sm font-medium text-gov-foreground/80 transition-colors hover:bg-white/10 hover:text-gov-foreground"
61
- }
62
- ),
63
- children: [
269
+ render: /* @__PURE__ */ jsx(Link2, { href: it.href }),
270
+ className: cn(isActive(pathname, it.href) && "bg-accent text-accent-foreground"),
271
+ children: [
272
+ /* @__PURE__ */ jsx(Icon, { className: "size-4 shrink-0" }),
273
+ it.label
274
+ ]
275
+ },
276
+ it.href
277
+ );
278
+ }) })
279
+ ] }) : null,
280
+ hasPermission(perms, "notifications:view") ? /* @__PURE__ */ jsx(NotificationBell, {}) : null,
281
+ /* @__PURE__ */ jsx("span", { className: "mx-1 hidden h-5 w-px bg-white/15 sm:block", "aria-hidden": true }),
282
+ /* @__PURE__ */ jsxs(DropdownMenu, { children: [
283
+ /* @__PURE__ */ jsxs(
284
+ DropdownMenuTrigger,
285
+ {
286
+ render: /* @__PURE__ */ jsx("button", { type: "button", className: itemClass(false) }),
287
+ children: [
288
+ /* @__PURE__ */ jsx(UserRound, { className: "size-4 shrink-0" }),
289
+ /* @__PURE__ */ jsx("span", { className: "hidden max-w-[14ch] truncate md:inline", children: identity?.username ?? copy.admin.portal }),
290
+ /* @__PURE__ */ jsx(ChevronDown, { className: "size-3.5 shrink-0 opacity-70" })
291
+ ]
292
+ }
293
+ ),
294
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", className: "min-w-48", children: [
295
+ /* @__PURE__ */ jsxs("div", { className: "px-2 py-1.5", children: [
296
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium leading-tight", children: identity?.username ?? "\u2014" }),
297
+ roleLabel ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: roleLabel }) : null
298
+ ] }),
299
+ /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
300
+ changePasswordAction ? /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => setChangePwOpen(true), children: [
301
+ /* @__PURE__ */ jsx(KeyRound, { className: "size-4 shrink-0" }),
302
+ copy.admin.changePassword.title
303
+ ] }) : null,
304
+ /* @__PURE__ */ jsxs(DropdownMenuItem, { variant: "destructive", onClick: () => setSignOutOpen(true), children: [
64
305
  /* @__PURE__ */ jsx(LogOut, { className: "size-4 shrink-0" }),
65
- /* @__PURE__ */ jsx("span", { className: "hidden md:inline", children: "Sign out" })
66
- ]
67
- }
68
- ),
69
- /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-sm", children: [
70
- /* @__PURE__ */ jsxs(DialogHeader, { children: [
71
- /* @__PURE__ */ jsx(DialogTitle, { children: "Sign out?" }),
72
- /* @__PURE__ */ jsx(DialogDescription, { children: "You'll need to sign in again to issue or manage tickets." })
73
- ] }),
74
- /* @__PURE__ */ jsxs(DialogFooter, { children: [
75
- /* @__PURE__ */ jsx(DialogClose, { render: /* @__PURE__ */ jsx(Button, { variant: "outline" }), children: "Cancel" }),
76
- /* @__PURE__ */ jsx("form", { action: signOutAction, children: /* @__PURE__ */ jsxs(Button, { type: "submit", variant: "destructive", className: "gap-2", children: [
77
- /* @__PURE__ */ jsx(LogOut, { className: "size-4" }),
78
- "Sign out"
79
- ] }) })
306
+ copy.admin.signOut
307
+ ] })
80
308
  ] })
81
309
  ] })
82
- ] })
310
+ ] }),
311
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 md:hidden", children: [
312
+ hasPermission(perms, "notifications:view") ? /* @__PURE__ */ jsx(NotificationBell, {}) : null,
313
+ /* @__PURE__ */ jsxs(Sheet, { open: mobileOpen, onOpenChange: setMobileOpen, children: [
314
+ /* @__PURE__ */ jsx(
315
+ SheetTrigger,
316
+ {
317
+ render: /* @__PURE__ */ jsx(
318
+ "button",
319
+ {
320
+ type: "button",
321
+ "aria-label": copy.admin.more,
322
+ className: itemClass(false)
323
+ }
324
+ ),
325
+ children: /* @__PURE__ */ jsx(Menu, { className: "size-5 shrink-0" })
326
+ }
327
+ ),
328
+ /* @__PURE__ */ jsxs(SheetContent, { side: "right", className: "w-72 gap-0 p-0", children: [
329
+ /* @__PURE__ */ jsxs(SheetHeader, { className: "border-b", children: [
330
+ /* @__PURE__ */ jsx(SheetTitle, { className: "truncate", children: identity?.username ?? copy.admin.portal }),
331
+ roleLabel ? /* @__PURE__ */ jsx(SheetDescription, { children: roleLabel }) : null
332
+ ] }),
333
+ /* @__PURE__ */ jsx("nav", { className: "flex flex-1 flex-col gap-0.5 overflow-y-auto p-2", children: allNav.map((it) => {
334
+ const Icon = it.icon;
335
+ const active = isActive(pathname, it.href);
336
+ return /* @__PURE__ */ jsxs(
337
+ Link2,
338
+ {
339
+ href: it.href,
340
+ onClick: () => setMobileOpen(false),
341
+ className: cn(
342
+ "flex items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium transition-colors",
343
+ active ? "bg-accent text-accent-foreground" : "text-foreground/80 hover:bg-muted hover:text-foreground"
344
+ ),
345
+ children: [
346
+ /* @__PURE__ */ jsx(Icon, { className: "size-4 shrink-0" }),
347
+ it.label
348
+ ]
349
+ },
350
+ it.href
351
+ );
352
+ }) }),
353
+ /* @__PURE__ */ jsxs(SheetFooter, { className: "border-t", children: [
354
+ changePasswordAction ? /* @__PURE__ */ jsxs(
355
+ Button,
356
+ {
357
+ variant: "outline",
358
+ className: "justify-start gap-2",
359
+ onClick: () => {
360
+ setMobileOpen(false);
361
+ setChangePwOpen(true);
362
+ },
363
+ children: [
364
+ /* @__PURE__ */ jsx(KeyRound, { className: "size-4" }),
365
+ copy.admin.changePassword.title
366
+ ]
367
+ }
368
+ ) : null,
369
+ /* @__PURE__ */ jsxs(
370
+ Button,
371
+ {
372
+ variant: "destructive",
373
+ className: "justify-start gap-2",
374
+ onClick: () => {
375
+ setMobileOpen(false);
376
+ setSignOutOpen(true);
377
+ },
378
+ children: [
379
+ /* @__PURE__ */ jsx(LogOut, { className: "size-4" }),
380
+ copy.admin.signOut
381
+ ]
382
+ }
383
+ )
384
+ ] })
385
+ ] })
386
+ ] })
387
+ ] }),
388
+ /* @__PURE__ */ jsx(Dialog, { open: signOutOpen, onOpenChange: setSignOutOpen, children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-sm", children: [
389
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
390
+ /* @__PURE__ */ jsx(DialogTitle, { children: copy.admin.signOutConfirmTitle }),
391
+ /* @__PURE__ */ jsx(DialogDescription, { children: copy.admin.signOutConfirmBody })
392
+ ] }),
393
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
394
+ /* @__PURE__ */ jsx(DialogClose, { render: /* @__PURE__ */ jsx(Button, { variant: "outline" }), children: copy.admin.cancel }),
395
+ /* @__PURE__ */ jsx("form", { action: signOutAction, children: /* @__PURE__ */ jsxs(Button, { type: "submit", variant: "destructive", className: "gap-2", children: [
396
+ /* @__PURE__ */ jsx(LogOut, { className: "size-4" }),
397
+ copy.admin.signOut
398
+ ] }) })
399
+ ] })
400
+ ] }) }),
401
+ changePasswordAction ? /* @__PURE__ */ jsx(
402
+ ChangePasswordDialog,
403
+ {
404
+ open: changePwOpen,
405
+ onOpenChange: setChangePwOpen,
406
+ action: changePasswordAction
407
+ }
408
+ ) : null
83
409
  ] });
84
410
  }
85
411
 
@@ -1,24 +1,25 @@
1
1
  "use client";
2
2
  import { Skeleton } from '../chunk-EGKFELO3.js';
3
3
  import { Textarea } from '../chunk-QCRVT2SS.js';
4
+ import { Alert, AlertDescription } from '../chunk-EYFZWQ4J.js';
4
5
  import { Checkbox } from '../chunk-BBQBKQA4.js';
5
- import { Input } from '../chunk-K3KIBHJF.js';
6
- import { Label } from '../chunk-XQTVSNHC.js';
7
- import { TicketPreview } from '../chunk-GDOCD7LT.js';
8
- import '../chunk-JEYT63LE.js';
9
- import '../chunk-BIQ2J75Y.js';
6
+ import { TicketPreview } from '../chunk-ZUMEOZ22.js';
7
+ import '../chunk-IBZVIUNI.js';
8
+ import '../chunk-GLIK5BHP.js';
10
9
  import '../chunk-NSCIBSCW.js';
11
- import { Alert, AlertDescription } from '../chunk-EYFZWQ4J.js';
12
- import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '../chunk-SETIN6XP.js';
13
10
  import '../chunk-OE525ZER.js';
11
+ import '../chunk-BVI5XDDA.js';
12
+ import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '../chunk-SETIN6XP.js';
14
13
  import '../chunk-OWCGEEAZ.js';
15
14
  import '../chunk-55FQP2DO.js';
16
- import '../chunk-3KIDW4LT.js';
15
+ import { Label } from '../chunk-XQTVSNHC.js';
17
16
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from '../chunk-M35R6JLA.js';
17
+ import { Input } from '../chunk-K3KIBHJF.js';
18
18
  import { Button } from '../chunk-I4WDVYHX.js';
19
- import { useFormatters, useOvrConfig } from '../chunk-E2D7QT6N.js';
19
+ import { useFormatters, useOvrConfig } from '../chunk-TJSNVTVB.js';
20
20
  import { cn } from '../chunk-77QBZC7J.js';
21
- import { localToManilaISO, toPreviewTicket } from '../chunk-B634JHKZ.js';
21
+ import { useIdentity } from '../chunk-YGYA7KEG.js';
22
+ import { localToManilaISO, toPreviewTicket, listProvinces, citiesOfProvince } from '../chunk-BI4EGLPG.js';
22
23
  import * as React from 'react';
23
24
  import { toast } from 'sonner';
24
25
  import { FilePlus2, TriangleAlert, Loader2 } from 'lucide-react';
@@ -107,6 +108,7 @@ function IssuanceForm({
107
108
  }) {
108
109
  const { formatPeso, nowManilaLocalInput } = useFormatters();
109
110
  const { municipality, rules } = useOvrConfig();
111
+ const identity = useIdentity();
110
112
  const [firstName, setFirstName] = React.useState("");
111
113
  const [middleName, setMiddleName] = React.useState("");
112
114
  const [lastName, setLastName] = React.useState("");
@@ -177,7 +179,9 @@ function IssuanceForm({
177
179
  details: details || void 0
178
180
  })),
179
181
  remarks: remarks || void 0,
180
- issuedBy: issuedBy || void 0
182
+ issuedBy: issuedBy || void 0,
183
+ apprehendingEnforcerId: identity?.userId,
184
+ apprehendingEnforcerName: identity?.username
181
185
  };
182
186
  startTransition(async () => {
183
187
  const res = await createAction(input);
@@ -200,8 +204,51 @@ function IssuanceForm({
200
204
  /* @__PURE__ */ jsx(TextField, { id: "license", label: "License number", value: licenseNumber, onChange: setLicenseNumber, required: true, placeholder: "N03-12-345678" }),
201
205
  /* @__PURE__ */ jsx("div", { className: "sm:col-span-2", children: /* @__PURE__ */ jsx(TextField, { id: "street", label: "Address line", value: street, onChange: setStreet, required: true, placeholder: "House no. / Purok / Zone / Street" }) }),
202
206
  /* @__PURE__ */ jsx("div", { className: "sm:col-span-2", children: /* @__PURE__ */ jsx(TextField, { id: "barangay", label: "Barangay", value: barangay, onChange: setBarangay, placeholder: "e.g. Amungan" }) }),
203
- /* @__PURE__ */ jsx(TextField, { id: "city", label: "City / Municipality", value: cityMunicipality, onChange: setCityMunicipality, required: true }),
204
- /* @__PURE__ */ jsx(TextField, { id: "province", label: "Province", value: province, onChange: setProvince, required: true }),
207
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
208
+ /* @__PURE__ */ jsxs(Label, { htmlFor: "province", children: [
209
+ "Province",
210
+ /* @__PURE__ */ jsx("span", { className: "text-destructive", children: " *" })
211
+ ] }),
212
+ /* @__PURE__ */ jsxs(
213
+ "select",
214
+ {
215
+ id: "province",
216
+ value: province,
217
+ onChange: (e) => {
218
+ const p = e.target.value;
219
+ setProvince(p);
220
+ if (cityMunicipality && !citiesOfProvince(p).includes(cityMunicipality)) {
221
+ setCityMunicipality("");
222
+ }
223
+ },
224
+ className: fieldClass,
225
+ children: [
226
+ /* @__PURE__ */ jsx("option", { value: "", children: "Select province\u2026" }),
227
+ listProvinces().map((p) => /* @__PURE__ */ jsx("option", { value: p, children: p }, p))
228
+ ]
229
+ }
230
+ )
231
+ ] }),
232
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
233
+ /* @__PURE__ */ jsxs(Label, { htmlFor: "city", children: [
234
+ "City / Municipality",
235
+ /* @__PURE__ */ jsx("span", { className: "text-destructive", children: " *" })
236
+ ] }),
237
+ /* @__PURE__ */ jsxs(
238
+ "select",
239
+ {
240
+ id: "city",
241
+ value: cityMunicipality,
242
+ onChange: (e) => setCityMunicipality(e.target.value),
243
+ disabled: !province,
244
+ className: fieldClass,
245
+ children: [
246
+ /* @__PURE__ */ jsx("option", { value: "", children: province ? "Select city / municipality\u2026" : "Select a province first" }),
247
+ citiesOfProvince(province).map((c) => /* @__PURE__ */ jsx("option", { value: c, children: c }, c))
248
+ ]
249
+ }
250
+ )
251
+ ] }),
205
252
  /* @__PURE__ */ jsx(TextField, { id: "plate", label: "Plate number", value: plateNumber, onChange: setPlateNumber, placeholder: "ABC 1234" }),
206
253
  /* @__PURE__ */ jsx(TextField, { id: "contact", label: "Contact number", value: contactNo, onChange: setContactNo, placeholder: "0917 555 0000" })
207
254
  ] })
@@ -243,6 +290,18 @@ function IssuanceForm({
243
290
  }
244
291
  )
245
292
  ] }),
293
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
294
+ /* @__PURE__ */ jsx(Label, { htmlFor: "apprehendingEnforcer", children: "Apprehending enforcer" }),
295
+ /* @__PURE__ */ jsx(
296
+ "p",
297
+ {
298
+ id: "apprehendingEnforcer",
299
+ className: "flex h-9 items-center rounded-lg border border-input bg-muted/40 px-2.5 text-sm text-muted-foreground",
300
+ children: identity?.username ?? "\u2014"
301
+ }
302
+ ),
303
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Auto-recorded from your sign-in." })
304
+ ] }),
246
305
  /* @__PURE__ */ jsx("div", { className: "sm:col-span-2", children: /* @__PURE__ */ jsx(TextField, { id: "place", label: "Place of violation", value: placeOfViolation, onChange: setPlaceOfViolation, placeholder: `Street / landmark, ${municipality.shortName}` }) })
247
306
  ] })
248
307
  ] }),
@@ -0,0 +1,13 @@
1
+ import * as React from 'react';
2
+ import { ActivityLog } from '../types.js';
3
+
4
+ /**
5
+ * Activity-log viewer (GE-022). Read-only audit trail with client-side filters
6
+ * (action dropdown + actor/detail search) over the entries fetched by the page.
7
+ */
8
+
9
+ declare function LogsViewer({ entries }: {
10
+ entries: ActivityLog[];
11
+ }): React.JSX.Element;
12
+
13
+ export { LogsViewer };