@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
@@ -1,85 +1,413 @@
1
1
  "use client";
2
- import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from '../chunk-M35R6JLA.js';
2
+ import { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter } from '../chunk-3YKVH4Y7.js';
3
+ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator } from '../chunk-V7VQVDWS.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-WUNTHINH.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, Gavel, 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/violations") return p.startsWith("/admin/violations");
209
+ if (href === "/admin/logs") return p.startsWith("/admin/logs");
18
210
  return p === href;
19
211
  }
212
+ function itemClass(active) {
213
+ return cn(
214
+ "flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-sm font-medium transition-colors",
215
+ 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"
216
+ );
217
+ }
20
218
  function AdminNav({
21
- signOutAction
219
+ signOutAction,
220
+ changePasswordAction
22
221
  }) {
23
222
  const pathname = usePathname();
24
223
  const copy = useCopy();
25
224
  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 }
225
+ const perms = identity?.permissions ?? [];
226
+ const [signOutOpen, setSignOutOpen] = React.useState(false);
227
+ const [changePwOpen, setChangePwOpen] = React.useState(false);
228
+ const [mobileOpen, setMobileOpen] = React.useState(false);
229
+ const primaryAll = [
230
+ { href: "/admin", label: copy.admin.dashboard, icon: LayoutDashboard, perm: "dashboard:view" },
231
+ { href: "/admin/tickets", label: copy.admin.tickets, icon: ListChecks, perm: "tickets:view" }
31
232
  ];
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",
233
+ const overflowAll = [
234
+ { href: "/admin/accounts", label: copy.admin.accounts, icon: Users, perm: "accounts:manage" },
235
+ { href: "/admin/roles", label: copy.admin.roles, icon: ShieldCheck, perm: "roles:manage" },
236
+ { href: "/admin/officers", label: copy.admin.officers, icon: BadgeCheck, perm: "officers:manage" },
237
+ { href: "/admin/violations", label: copy.admin.violations, icon: Gavel, perm: "violations:manage" },
238
+ { href: "/admin/logs", label: copy.admin.logs, icon: ScrollText, perm: "logs:view" }
239
+ ];
240
+ const primary = primaryAll.filter((it) => hasPermission(perms, it.perm));
241
+ const overflow = overflowAll.filter((it) => hasPermission(perms, it.perm));
242
+ const overflowActive = overflow.some((it) => isActive(pathname, it.href));
243
+ const allNav = [...primary, ...overflow];
244
+ const roleLabel = identity?.roleLabel ?? identity?.role ?? void 0;
245
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
246
+ /* @__PURE__ */ jsxs("nav", { className: "hidden items-center gap-0.5 md:flex md:gap-1", children: [
247
+ primary.map((it) => {
248
+ const Icon = it.icon;
249
+ return /* @__PURE__ */ jsxs(Link2, { href: it.href, className: itemClass(isActive(pathname, it.href)), children: [
250
+ /* @__PURE__ */ jsx(Icon, { className: "size-4 shrink-0" }),
251
+ /* @__PURE__ */ jsx("span", { className: "hidden md:inline", children: it.label })
252
+ ] }, it.href);
253
+ }),
254
+ overflow.length > 0 ? /* @__PURE__ */ jsxs(DropdownMenu, { children: [
255
+ /* @__PURE__ */ jsxs(
256
+ DropdownMenuTrigger,
257
+ {
258
+ render: /* @__PURE__ */ jsx("button", { type: "button", className: itemClass(overflowActive) }),
259
+ children: [
260
+ /* @__PURE__ */ jsx(MoreHorizontal, { className: "size-4 shrink-0" }),
261
+ /* @__PURE__ */ jsx("span", { className: "hidden md:inline", children: copy.admin.more }),
262
+ /* @__PURE__ */ jsx(ChevronDown, { className: "size-3.5 shrink-0 opacity-70" })
263
+ ]
264
+ }
265
+ ),
266
+ /* @__PURE__ */ jsx(DropdownMenuContent, { align: "end", className: "min-w-44", children: overflow.map((it) => {
267
+ const Icon = it.icon;
268
+ return /* @__PURE__ */ jsxs(
269
+ DropdownMenuItem,
58
270
  {
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: [
271
+ render: /* @__PURE__ */ jsx(Link2, { href: it.href }),
272
+ className: cn(isActive(pathname, it.href) && "bg-accent text-accent-foreground"),
273
+ children: [
274
+ /* @__PURE__ */ jsx(Icon, { className: "size-4 shrink-0" }),
275
+ it.label
276
+ ]
277
+ },
278
+ it.href
279
+ );
280
+ }) })
281
+ ] }) : null,
282
+ hasPermission(perms, "notifications:view") ? /* @__PURE__ */ jsx(NotificationBell, {}) : null,
283
+ /* @__PURE__ */ jsx("span", { className: "mx-1 hidden h-5 w-px bg-white/15 sm:block", "aria-hidden": true }),
284
+ /* @__PURE__ */ jsxs(DropdownMenu, { children: [
285
+ /* @__PURE__ */ jsxs(
286
+ DropdownMenuTrigger,
287
+ {
288
+ render: /* @__PURE__ */ jsx("button", { type: "button", className: itemClass(false) }),
289
+ children: [
290
+ /* @__PURE__ */ jsx(UserRound, { className: "size-4 shrink-0" }),
291
+ /* @__PURE__ */ jsx("span", { className: "hidden max-w-[14ch] truncate md:inline", children: identity?.username ?? copy.admin.portal }),
292
+ /* @__PURE__ */ jsx(ChevronDown, { className: "size-3.5 shrink-0 opacity-70" })
293
+ ]
294
+ }
295
+ ),
296
+ /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", className: "min-w-48", children: [
297
+ /* @__PURE__ */ jsxs("div", { className: "px-2 py-1.5", children: [
298
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium leading-tight", children: identity?.username ?? "\u2014" }),
299
+ roleLabel ? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: roleLabel }) : null
300
+ ] }),
301
+ /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
302
+ changePasswordAction ? /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => setChangePwOpen(true), children: [
303
+ /* @__PURE__ */ jsx(KeyRound, { className: "size-4 shrink-0" }),
304
+ copy.admin.changePassword.title
305
+ ] }) : null,
306
+ /* @__PURE__ */ jsxs(DropdownMenuItem, { variant: "destructive", onClick: () => setSignOutOpen(true), children: [
64
307
  /* @__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
- ] }) })
308
+ copy.admin.signOut
309
+ ] })
80
310
  ] })
81
311
  ] })
82
- ] })
312
+ ] }),
313
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 md:hidden", children: [
314
+ hasPermission(perms, "notifications:view") ? /* @__PURE__ */ jsx(NotificationBell, {}) : null,
315
+ /* @__PURE__ */ jsxs(Sheet, { open: mobileOpen, onOpenChange: setMobileOpen, children: [
316
+ /* @__PURE__ */ jsx(
317
+ SheetTrigger,
318
+ {
319
+ render: /* @__PURE__ */ jsx(
320
+ "button",
321
+ {
322
+ type: "button",
323
+ "aria-label": copy.admin.more,
324
+ className: itemClass(false)
325
+ }
326
+ ),
327
+ children: /* @__PURE__ */ jsx(Menu, { className: "size-5 shrink-0" })
328
+ }
329
+ ),
330
+ /* @__PURE__ */ jsxs(SheetContent, { side: "right", className: "w-72 gap-0 p-0", children: [
331
+ /* @__PURE__ */ jsxs(SheetHeader, { className: "border-b", children: [
332
+ /* @__PURE__ */ jsx(SheetTitle, { className: "truncate", children: identity?.username ?? copy.admin.portal }),
333
+ roleLabel ? /* @__PURE__ */ jsx(SheetDescription, { children: roleLabel }) : null
334
+ ] }),
335
+ /* @__PURE__ */ jsx("nav", { className: "flex flex-1 flex-col gap-0.5 overflow-y-auto p-2", children: allNav.map((it) => {
336
+ const Icon = it.icon;
337
+ const active = isActive(pathname, it.href);
338
+ return /* @__PURE__ */ jsxs(
339
+ Link2,
340
+ {
341
+ href: it.href,
342
+ onClick: () => setMobileOpen(false),
343
+ className: cn(
344
+ "flex items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium transition-colors",
345
+ active ? "bg-accent text-accent-foreground" : "text-foreground/80 hover:bg-muted hover:text-foreground"
346
+ ),
347
+ children: [
348
+ /* @__PURE__ */ jsx(Icon, { className: "size-4 shrink-0" }),
349
+ it.label
350
+ ]
351
+ },
352
+ it.href
353
+ );
354
+ }) }),
355
+ /* @__PURE__ */ jsxs(SheetFooter, { className: "border-t", children: [
356
+ changePasswordAction ? /* @__PURE__ */ jsxs(
357
+ Button,
358
+ {
359
+ variant: "outline",
360
+ className: "justify-start gap-2",
361
+ onClick: () => {
362
+ setMobileOpen(false);
363
+ setChangePwOpen(true);
364
+ },
365
+ children: [
366
+ /* @__PURE__ */ jsx(KeyRound, { className: "size-4" }),
367
+ copy.admin.changePassword.title
368
+ ]
369
+ }
370
+ ) : null,
371
+ /* @__PURE__ */ jsxs(
372
+ Button,
373
+ {
374
+ variant: "destructive",
375
+ className: "justify-start gap-2",
376
+ onClick: () => {
377
+ setMobileOpen(false);
378
+ setSignOutOpen(true);
379
+ },
380
+ children: [
381
+ /* @__PURE__ */ jsx(LogOut, { className: "size-4" }),
382
+ copy.admin.signOut
383
+ ]
384
+ }
385
+ )
386
+ ] })
387
+ ] })
388
+ ] })
389
+ ] }),
390
+ /* @__PURE__ */ jsx(Dialog, { open: signOutOpen, onOpenChange: setSignOutOpen, children: /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-sm", children: [
391
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
392
+ /* @__PURE__ */ jsx(DialogTitle, { children: copy.admin.signOutConfirmTitle }),
393
+ /* @__PURE__ */ jsx(DialogDescription, { children: copy.admin.signOutConfirmBody })
394
+ ] }),
395
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
396
+ /* @__PURE__ */ jsx(DialogClose, { render: /* @__PURE__ */ jsx(Button, { variant: "outline" }), children: copy.admin.cancel }),
397
+ /* @__PURE__ */ jsx("form", { action: signOutAction, children: /* @__PURE__ */ jsxs(Button, { type: "submit", variant: "destructive", className: "gap-2", children: [
398
+ /* @__PURE__ */ jsx(LogOut, { className: "size-4" }),
399
+ copy.admin.signOut
400
+ ] }) })
401
+ ] })
402
+ ] }) }),
403
+ changePasswordAction ? /* @__PURE__ */ jsx(
404
+ ChangePasswordDialog,
405
+ {
406
+ open: changePwOpen,
407
+ onOpenChange: setChangePwOpen,
408
+ action: changePasswordAction
409
+ }
410
+ ) : null
83
411
  ] });
84
412
  }
85
413
 
@@ -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 };