@gelabs/ovr 0.4.3 → 0.4.4

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 (75) hide show
  1. package/dist/accounts.d.ts +52 -0
  2. package/dist/accounts.js +13 -0
  3. package/dist/audit.d.ts +13 -0
  4. package/dist/audit.js +11 -0
  5. package/dist/auth-auth.d.ts +1 -1
  6. package/dist/auth.d.ts +1 -1
  7. package/dist/chunk-4EDMZRGS.js +98 -0
  8. package/dist/chunk-4JMHQKMK.js +268 -0
  9. package/dist/chunk-6662IONX.js +69 -0
  10. package/dist/chunk-6WMPBAUH.js +18 -0
  11. package/dist/chunk-7GVZZWK3.js +74 -0
  12. package/dist/chunk-ACXED4UH.js +403 -0
  13. package/dist/chunk-BBTAG3FD.js +165 -0
  14. package/dist/chunk-HX7QT2FE.js +452 -0
  15. package/dist/chunk-LC3S47FM.js +468 -0
  16. package/dist/chunk-NPTR7GFZ.js +432 -0
  17. package/dist/chunk-SLQRZBMR.js +66 -0
  18. package/dist/chunk-UBUXPJLN.js +29 -0
  19. package/dist/chunk-UN6Z4WGF.js +31 -0
  20. package/dist/chunk-YE3D2DYY.js +83 -0
  21. package/dist/citizen.d.ts +5 -0
  22. package/dist/citizen.js +12 -0
  23. package/dist/config.d.ts +2 -2
  24. package/dist/core-i18n.d.ts +1 -1
  25. package/dist/core.d.ts +2 -2
  26. package/dist/dashboard.d.ts +10 -0
  27. package/dist/dashboard.js +3 -0
  28. package/dist/data-mock-store.d.ts +1 -1
  29. package/dist/{format-C7MSwUHK.d.ts → format-VyCUfF8R.d.ts} +1 -1
  30. package/dist/generated/client/edge.js +2 -2
  31. package/dist/generated/client/index.js +2 -2
  32. package/dist/generated/client/wasm.js +2 -2
  33. package/dist/index.d.ts +2 -2
  34. package/dist/notifications.d.ts +6 -0
  35. package/dist/notifications.js +11 -0
  36. package/dist/officers.d.ts +27 -0
  37. package/dist/officers.js +11 -0
  38. package/dist/offline.d.ts +1 -1
  39. package/dist/payments.d.ts +15 -0
  40. package/dist/payments.js +11 -0
  41. package/dist/roles.d.ts +37 -0
  42. package/dist/roles.js +12 -0
  43. package/dist/runtime.d.ts +1 -1
  44. package/dist/tickets.d.ts +6 -0
  45. package/dist/tickets.js +24 -0
  46. package/dist/ui-components-admin/accounts-manager.d.ts +3 -52
  47. package/dist/ui-components-admin/accounts-manager.js +11 -468
  48. package/dist/ui-components-admin/admin-nav.js +9 -81
  49. package/dist/ui-components-admin/issuance-form.js +15 -432
  50. package/dist/ui-components-admin/logs-viewer.d.ts +3 -13
  51. package/dist/ui-components-admin/logs-viewer.js +8 -98
  52. package/dist/ui-components-admin/notifications-list.js +6 -66
  53. package/dist/ui-components-admin/officers-manager.d.ts +3 -27
  54. package/dist/ui-components-admin/officers-manager.js +9 -268
  55. package/dist/ui-components-admin/roles-manager.d.ts +3 -37
  56. package/dist/ui-components-admin/roles-manager.js +10 -403
  57. package/dist/ui-components-admin/stat-card.d.ts +2 -10
  58. package/dist/ui-components-admin/stat-card.js +3 -29
  59. package/dist/ui-components-admin/ticket-preview.js +2 -2
  60. package/dist/ui-components-admin/tickets-table.js +7 -69
  61. package/dist/ui-components-admin/violations-manager.js +13 -452
  62. package/dist/ui-components-citizen/citizen-nav.js +3 -31
  63. package/dist/ui-components-citizen/payment-form.d.ts +3 -14
  64. package/dist/ui-components-citizen/payment-form.js +9 -165
  65. package/dist/ui-components-citizen/payment-qr-dialog.js +2 -2
  66. package/dist/ui-components-citizen/ticket-not-found.js +4 -18
  67. package/dist/ui-components-citizen/violation-history-table.js +6 -74
  68. package/dist/ui-config.d.ts +2 -2
  69. package/dist/ui-server.d.ts +2 -2
  70. package/dist/violations.d.ts +3 -0
  71. package/dist/violations.js +14 -0
  72. package/package.json +42 -2
  73. package/dist/{chunk-ZUMEOZ22.js → chunk-JTSTNZAB.js} +1 -1
  74. package/dist/{chunk-TLG4C2XI.js → chunk-QCAURREW.js} +1 -1
  75. package/dist/{schema-CdsFQxIg.d.ts → schema-BUhh_mKX.d.ts} +108 -108
@@ -0,0 +1,52 @@
1
+ import * as React from 'react';
2
+ import { UserAccount, Officer, RoleDef } from './types.js';
3
+
4
+ /**
5
+ * Account management UI (GE-013). Super admins create & manage who can sign in to
6
+ * the enforcer portal; each account's role decides what it can see/do. The server
7
+ * actions (create / activate / reset password) are INJECTED by the page so this
8
+ * component stays free of any app-specific data wiring — mirrors IssuanceForm.
9
+ *
10
+ * Online-only: account management needs the server (argon2 + DB). After a mutation
11
+ * we refresh the route so the server-rendered list reflects the change.
12
+ */
13
+
14
+ /** What the create form submits — the page hashes the password server-side. */
15
+ interface CreateAccountInput {
16
+ username: string;
17
+ password: string;
18
+ role: string;
19
+ officerId: string | null;
20
+ }
21
+ type CreateAccountAction = (input: CreateAccountInput) => Promise<{
22
+ error?: string;
23
+ } | void>;
24
+ type SetAccountActiveAction = (id: string, active: boolean) => Promise<{
25
+ error?: string;
26
+ } | void>;
27
+ type ResetAccountPasswordAction = (id: string, password: string) => Promise<{
28
+ error?: string;
29
+ } | void>;
30
+ interface EditAccountInput {
31
+ username?: string;
32
+ role?: string;
33
+ officerId?: string | null;
34
+ }
35
+ type EditAccountAction = (id: string, patch: EditAccountInput) => Promise<{
36
+ error?: string;
37
+ } | void>;
38
+ interface AccountsManagerProps {
39
+ users: UserAccount[];
40
+ officers: Officer[];
41
+ /** Roles assignable to accounts (system + custom). */
42
+ roles: RoleDef[];
43
+ /** The signed-in user's id — so they can't deactivate their own account. */
44
+ currentUserId?: string;
45
+ createAction: CreateAccountAction;
46
+ editAction: EditAccountAction;
47
+ setActiveAction: SetAccountActiveAction;
48
+ resetPasswordAction: ResetAccountPasswordAction;
49
+ }
50
+ declare function AccountsManager({ users, officers, roles, currentUserId, createAction, editAction, setActiveAction, resetPasswordAction, }: AccountsManagerProps): React.JSX.Element;
51
+
52
+ export { AccountsManager, type AccountsManagerProps, type CreateAccountAction, type CreateAccountInput, type EditAccountAction, type EditAccountInput, type ResetAccountPasswordAction, type SetAccountActiveAction };
@@ -0,0 +1,13 @@
1
+ export { AccountsManager } from './chunk-LC3S47FM.js';
2
+ import './chunk-XQTVSNHC.js';
3
+ import './chunk-M35R6JLA.js';
4
+ import './chunk-6YFZLXFP.js';
5
+ import './chunk-K3KIBHJF.js';
6
+ import './chunk-QZRRFE6E.js';
7
+ import './chunk-OWCGEEAZ.js';
8
+ import './chunk-55FQP2DO.js';
9
+ import './chunk-I4WDVYHX.js';
10
+ import './chunk-TJSNVTVB.js';
11
+ import './chunk-SETIN6XP.js';
12
+ import './chunk-77QBZC7J.js';
13
+ import './chunk-BI4EGLPG.js';
@@ -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 };
package/dist/audit.js ADDED
@@ -0,0 +1,11 @@
1
+ export { LogsViewer } from './chunk-4EDMZRGS.js';
2
+ import './chunk-6YFZLXFP.js';
3
+ import './chunk-K3KIBHJF.js';
4
+ import './chunk-QZRRFE6E.js';
5
+ import './chunk-OWCGEEAZ.js';
6
+ import './chunk-55FQP2DO.js';
7
+ import './chunk-I4WDVYHX.js';
8
+ import './chunk-TJSNVTVB.js';
9
+ import './chunk-SETIN6XP.js';
10
+ import './chunk-77QBZC7J.js';
11
+ import './chunk-BI4EGLPG.js';
@@ -1,4 +1,4 @@
1
- import { D as DemoAdminConfig } from './schema-CdsFQxIg.js';
1
+ import { D as DemoAdminConfig } from './schema-BUhh_mKX.js';
2
2
  import { SessionData } from './auth-session.js';
3
3
  import 'zod';
4
4
 
package/dist/auth.d.ts CHANGED
@@ -3,5 +3,5 @@ export { SESSION_COOKIE, SessionData, cookieSecure, createSession, destroySessio
3
3
  export { RateLimitResult, isRateLimitDisabled, rateLimit } from './auth-rate-limit.js';
4
4
  export { ADMIN_COOKIE, Auth, AuthDeps, createAuth } from './auth-auth.js';
5
5
  import 'ioredis';
6
- import './schema-CdsFQxIg.js';
6
+ import './schema-BUhh_mKX.js';
7
7
  import 'zod';
@@ -0,0 +1,98 @@
1
+ import { usePagination, Pagination } from './chunk-6YFZLXFP.js';
2
+ import { Input } from './chunk-K3KIBHJF.js';
3
+ import { ACTIVITY_ACTION_LABELS } from './chunk-QZRRFE6E.js';
4
+ import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from './chunk-OWCGEEAZ.js';
5
+ import { Badge } from './chunk-55FQP2DO.js';
6
+ import { useCopy, useOvrConfig } from './chunk-TJSNVTVB.js';
7
+ import { Card, CardContent } from './chunk-SETIN6XP.js';
8
+ import * as React from 'react';
9
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
10
+
11
+ 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";
12
+ function actionLabel(action) {
13
+ return ACTIVITY_ACTION_LABELS[action] ?? action;
14
+ }
15
+ function LogsViewer({ entries }) {
16
+ const t = useCopy().admin.logsPage;
17
+ const { rules } = useOvrConfig();
18
+ const [action, setAction] = React.useState("");
19
+ const [q, setQ] = React.useState("");
20
+ const fmt = (iso) => new Date(iso).toLocaleString(rules.locale, {
21
+ timeZone: rules.timeZone,
22
+ year: "numeric",
23
+ month: "short",
24
+ day: "2-digit",
25
+ hour: "numeric",
26
+ minute: "2-digit"
27
+ });
28
+ const actions = Array.from(new Set(entries.map((e) => e.action)));
29
+ const query = q.trim().toLowerCase();
30
+ const filtered = entries.filter((e) => {
31
+ if (action && e.action !== action) return false;
32
+ if (query) {
33
+ const hay = `${e.actorUsername ?? ""} ${e.summary}`.toLowerCase();
34
+ if (!hay.includes(query)) return false;
35
+ }
36
+ return true;
37
+ });
38
+ const { pageItems, page, setPage, totalPages, from, to, total } = usePagination(filtered, 20, `${action}|${query}`);
39
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-4xl p-4 sm:p-6 lg:p-8", children: [
40
+ /* @__PURE__ */ jsxs("div", { className: "mb-6", children: [
41
+ /* @__PURE__ */ jsx("h1", { className: "font-heading text-2xl font-semibold tracking-tight", children: t.title }),
42
+ /* @__PURE__ */ jsx("p", { className: "max-w-2xl text-sm text-muted-foreground", children: t.subtitle })
43
+ ] }),
44
+ /* @__PURE__ */ jsxs("div", { className: "mb-4 flex flex-wrap items-center gap-2", children: [
45
+ /* @__PURE__ */ jsxs(
46
+ "select",
47
+ {
48
+ value: action,
49
+ onChange: (e) => setAction(e.target.value),
50
+ className: `${fieldClass} w-auto`,
51
+ "aria-label": t.action,
52
+ children: [
53
+ /* @__PURE__ */ jsx("option", { value: "", children: t.allActions }),
54
+ actions.map((a) => /* @__PURE__ */ jsx("option", { value: a, children: actionLabel(a) }, a))
55
+ ]
56
+ }
57
+ ),
58
+ /* @__PURE__ */ jsx(
59
+ Input,
60
+ {
61
+ value: q,
62
+ onChange: (e) => setQ(e.target.value),
63
+ placeholder: t.search,
64
+ className: "h-9 w-auto min-w-56"
65
+ }
66
+ )
67
+ ] }),
68
+ /* @__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: [
69
+ /* @__PURE__ */ jsxs(Table, { children: [
70
+ /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
71
+ /* @__PURE__ */ jsx(TableHead, { children: t.when }),
72
+ /* @__PURE__ */ jsx(TableHead, { children: t.actor }),
73
+ /* @__PURE__ */ jsx(TableHead, { children: t.action }),
74
+ /* @__PURE__ */ jsx(TableHead, { children: t.detail })
75
+ ] }) }),
76
+ /* @__PURE__ */ jsx(TableBody, { children: pageItems.map((e) => /* @__PURE__ */ jsxs(TableRow, { children: [
77
+ /* @__PURE__ */ jsx(TableCell, { className: "whitespace-nowrap text-muted-foreground", children: fmt(e.createdAt) }),
78
+ /* @__PURE__ */ jsx(TableCell, { className: "font-medium", children: e.actorUsername ?? t.system }),
79
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(Badge, { variant: "secondary", children: actionLabel(e.action) }) }),
80
+ /* @__PURE__ */ jsx(TableCell, { className: "text-muted-foreground", children: e.summary })
81
+ ] }, e.id)) })
82
+ ] }),
83
+ /* @__PURE__ */ jsx(
84
+ Pagination,
85
+ {
86
+ page,
87
+ totalPages,
88
+ from,
89
+ to,
90
+ total,
91
+ onPage: setPage
92
+ }
93
+ )
94
+ ] }) }) })
95
+ ] });
96
+ }
97
+
98
+ export { LogsViewer };
@@ -0,0 +1,268 @@
1
+ import { Label } from './chunk-XQTVSNHC.js';
2
+ import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from './chunk-M35R6JLA.js';
3
+ import { usePagination, Pagination } from './chunk-6YFZLXFP.js';
4
+ import { Input } from './chunk-K3KIBHJF.js';
5
+ import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from './chunk-OWCGEEAZ.js';
6
+ import { Button } from './chunk-I4WDVYHX.js';
7
+ import { useCopy } from './chunk-TJSNVTVB.js';
8
+ import { Card, CardContent } from './chunk-SETIN6XP.js';
9
+ import * as React from 'react';
10
+ import { toast } from 'sonner';
11
+ import { useRouter } from 'next/navigation';
12
+ import { UserPlus, Pencil, Loader2, Trash2 } from 'lucide-react';
13
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
14
+
15
+ function OfficersManager({
16
+ officers,
17
+ createAction,
18
+ updateAction,
19
+ deleteAction
20
+ }) {
21
+ const t = useCopy().admin.officersPage;
22
+ const router = useRouter();
23
+ const { pageItems, page, setPage, totalPages, from, to, total } = usePagination(officers, 12);
24
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-4xl p-4 sm:p-6 lg:p-8", children: [
25
+ /* @__PURE__ */ jsxs("div", { className: "mb-6 flex items-center justify-between gap-3", children: [
26
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
27
+ /* @__PURE__ */ jsx("h1", { className: "font-heading text-2xl font-semibold tracking-tight", children: t.title }),
28
+ /* @__PURE__ */ jsx("p", { className: "max-w-2xl text-sm text-muted-foreground", children: t.subtitle })
29
+ ] }),
30
+ /* @__PURE__ */ jsx(
31
+ OfficerDialog,
32
+ {
33
+ mode: "create",
34
+ createAction,
35
+ onDone: () => router.refresh()
36
+ }
37
+ )
38
+ ] }),
39
+ /* @__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: [
40
+ /* @__PURE__ */ jsxs(Table, { children: [
41
+ /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
42
+ /* @__PURE__ */ jsx(TableHead, { children: t.name }),
43
+ /* @__PURE__ */ jsx(TableHead, { children: t.badge }),
44
+ /* @__PURE__ */ jsx(TableHead, { children: t.office }),
45
+ /* @__PURE__ */ jsx(TableHead, { className: "text-right", children: t.actions })
46
+ ] }) }),
47
+ /* @__PURE__ */ jsx(TableBody, { children: pageItems.map((o) => /* @__PURE__ */ jsxs(TableRow, { children: [
48
+ /* @__PURE__ */ jsx(TableCell, { className: "font-medium", children: o.name }),
49
+ /* @__PURE__ */ jsx(TableCell, { className: "text-muted-foreground", children: o.badgeNo ?? "\u2014" }),
50
+ /* @__PURE__ */ jsx(TableCell, { className: "text-muted-foreground", children: o.office }),
51
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-1.5", children: [
52
+ /* @__PURE__ */ jsx(
53
+ OfficerDialog,
54
+ {
55
+ mode: "edit",
56
+ officer: o,
57
+ updateAction,
58
+ onDone: () => router.refresh()
59
+ }
60
+ ),
61
+ /* @__PURE__ */ jsx(
62
+ DeleteOfficerButton,
63
+ {
64
+ officer: o,
65
+ deleteAction,
66
+ onDone: () => router.refresh()
67
+ }
68
+ )
69
+ ] }) })
70
+ ] }, o.id)) })
71
+ ] }),
72
+ /* @__PURE__ */ jsx(
73
+ Pagination,
74
+ {
75
+ page,
76
+ totalPages,
77
+ from,
78
+ to,
79
+ total,
80
+ onPage: setPage
81
+ }
82
+ )
83
+ ] }) }) })
84
+ ] });
85
+ }
86
+ function OfficerDialog({
87
+ mode,
88
+ officer,
89
+ createAction,
90
+ updateAction,
91
+ onDone
92
+ }) {
93
+ const t = useCopy().admin.officersPage;
94
+ const [open, setOpen] = React.useState(false);
95
+ const [name, setName] = React.useState(officer?.name ?? "");
96
+ const [badgeNo, setBadgeNo] = React.useState(officer?.badgeNo ?? "");
97
+ const [office, setOffice] = React.useState(officer?.office ?? "");
98
+ const [submitting, setSubmitting] = React.useState(false);
99
+ function resetTo(o) {
100
+ setName(o?.name ?? "");
101
+ setBadgeNo(o?.badgeNo ?? "");
102
+ setOffice(o?.office ?? "");
103
+ }
104
+ async function submit(e) {
105
+ e.preventDefault();
106
+ if (!name.trim()) return toast.error(`${t.name} is required.`);
107
+ if (!office.trim()) return toast.error(`${t.office} is required.`);
108
+ setSubmitting(true);
109
+ try {
110
+ const input = {
111
+ name: name.trim(),
112
+ badgeNo: badgeNo.trim() || void 0,
113
+ office: office.trim()
114
+ };
115
+ const res = mode === "create" ? await createAction?.(input) : await updateAction?.(officer.id, input);
116
+ if (res?.error) {
117
+ toast.error(res.error);
118
+ return;
119
+ }
120
+ toast.success(
121
+ `${mode === "create" ? t.create : t.editTitle}: ${name.trim()}`
122
+ );
123
+ setOpen(false);
124
+ onDone();
125
+ } finally {
126
+ setSubmitting(false);
127
+ }
128
+ }
129
+ return /* @__PURE__ */ jsxs(
130
+ Dialog,
131
+ {
132
+ open,
133
+ onOpenChange: (o) => {
134
+ if (o) resetTo(officer);
135
+ setOpen(o);
136
+ },
137
+ children: [
138
+ /* @__PURE__ */ jsxs(
139
+ DialogTrigger,
140
+ {
141
+ render: mode === "create" ? /* @__PURE__ */ jsx(Button, { className: "gap-1.5" }) : /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", className: "gap-1.5" }),
142
+ children: [
143
+ mode === "create" ? /* @__PURE__ */ jsx(UserPlus, { className: "size-4" }) : /* @__PURE__ */ jsx(Pencil, { className: "size-3.5" }),
144
+ /* @__PURE__ */ jsx("span", { className: mode === "create" ? "" : "hidden sm:inline", children: mode === "create" ? t.newOfficer : t.edit })
145
+ ]
146
+ }
147
+ ),
148
+ /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-md", children: [
149
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
150
+ /* @__PURE__ */ jsx(DialogTitle, { children: mode === "create" ? t.newOfficer : t.editTitle }),
151
+ /* @__PURE__ */ jsx(DialogDescription, { children: t.subtitle })
152
+ ] }),
153
+ /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: "space-y-4", children: [
154
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
155
+ /* @__PURE__ */ jsx(Label, { htmlFor: "off-name", children: t.name }),
156
+ /* @__PURE__ */ jsx(
157
+ Input,
158
+ {
159
+ id: "off-name",
160
+ value: name,
161
+ onChange: (e) => setName(e.target.value),
162
+ placeholder: t.namePlaceholder,
163
+ autoComplete: "off",
164
+ autoFocus: true
165
+ }
166
+ )
167
+ ] }),
168
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [
169
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
170
+ /* @__PURE__ */ jsx(Label, { htmlFor: "off-badge", children: t.badge }),
171
+ /* @__PURE__ */ jsx(
172
+ Input,
173
+ {
174
+ id: "off-badge",
175
+ value: badgeNo,
176
+ onChange: (e) => setBadgeNo(e.target.value),
177
+ placeholder: t.badgePlaceholder,
178
+ autoComplete: "off"
179
+ }
180
+ )
181
+ ] }),
182
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
183
+ /* @__PURE__ */ jsx(Label, { htmlFor: "off-office", children: t.office }),
184
+ /* @__PURE__ */ jsx(
185
+ Input,
186
+ {
187
+ id: "off-office",
188
+ value: office,
189
+ onChange: (e) => setOffice(e.target.value),
190
+ placeholder: t.officePlaceholder,
191
+ autoComplete: "off"
192
+ }
193
+ )
194
+ ] })
195
+ ] }),
196
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
197
+ /* @__PURE__ */ jsx(DialogClose, { render: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline" }), children: t.cancel }),
198
+ /* @__PURE__ */ jsxs(Button, { type: "submit", disabled: submitting, className: "gap-1.5", children: [
199
+ submitting ? /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin" }) : null,
200
+ submitting ? mode === "create" ? t.creating : t.saving : mode === "create" ? t.create : t.save
201
+ ] })
202
+ ] })
203
+ ] })
204
+ ] })
205
+ ]
206
+ }
207
+ );
208
+ }
209
+ function DeleteOfficerButton({
210
+ officer,
211
+ deleteAction,
212
+ onDone
213
+ }) {
214
+ const t = useCopy().admin.officersPage;
215
+ const [deleting, setDeleting] = React.useState(false);
216
+ async function remove() {
217
+ setDeleting(true);
218
+ try {
219
+ const res = await deleteAction(officer.id);
220
+ if (res?.error) {
221
+ toast.error(res.error);
222
+ return;
223
+ }
224
+ toast.success(`${t.delete}: ${officer.name}`);
225
+ onDone();
226
+ } finally {
227
+ setDeleting(false);
228
+ }
229
+ }
230
+ return /* @__PURE__ */ jsxs(Dialog, { children: [
231
+ /* @__PURE__ */ jsxs(
232
+ DialogTrigger,
233
+ {
234
+ render: /* @__PURE__ */ jsx(Button, { variant: "destructive", size: "sm", className: "gap-1.5" }),
235
+ children: [
236
+ /* @__PURE__ */ jsx(Trash2, { className: "size-3.5" }),
237
+ /* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: t.delete })
238
+ ]
239
+ }
240
+ ),
241
+ /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-sm", children: [
242
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
243
+ /* @__PURE__ */ jsx(DialogTitle, { children: t.deleteConfirmTitle }),
244
+ /* @__PURE__ */ jsxs(DialogDescription, { children: [
245
+ officer.name,
246
+ " \u2014 ",
247
+ t.deleteConfirmBody
248
+ ] })
249
+ ] }),
250
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
251
+ /* @__PURE__ */ jsx(DialogClose, { render: /* @__PURE__ */ jsx(Button, { variant: "outline" }), children: t.cancel }),
252
+ /* @__PURE__ */ jsxs(
253
+ DialogClose,
254
+ {
255
+ render: /* @__PURE__ */ jsx(Button, { variant: "destructive", className: "gap-2", disabled: deleting }),
256
+ onClick: remove,
257
+ children: [
258
+ /* @__PURE__ */ jsx(Trash2, { className: "size-4" }),
259
+ t.delete
260
+ ]
261
+ }
262
+ )
263
+ ] })
264
+ ] })
265
+ ] });
266
+ }
267
+
268
+ export { OfficersManager };
@@ -0,0 +1,69 @@
1
+ import { usePagination, Pagination } from './chunk-6YFZLXFP.js';
2
+ import { StatusBadge } from './chunk-OE525ZER.js';
3
+ import { Money } from './chunk-BVI5XDDA.js';
4
+ import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from './chunk-OWCGEEAZ.js';
5
+ import { useFormatters } from './chunk-TJSNVTVB.js';
6
+ import { formalName } from './chunk-BI4EGLPG.js';
7
+ import { useRouter } from 'next/navigation';
8
+ import { ChevronRight } from 'lucide-react';
9
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
10
+
11
+ function TicketsTable({ tickets }) {
12
+ const { formatDate } = useFormatters();
13
+ const router = useRouter();
14
+ const { pageItems, page, setPage, totalPages, from, to, total } = usePagination(tickets, 15);
15
+ if (tickets.length === 0) {
16
+ return /* @__PURE__ */ jsx("div", { className: "px-4 py-10 text-center text-sm text-muted-foreground", children: "No tickets match this view." });
17
+ }
18
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
19
+ /* @__PURE__ */ jsxs(Table, { children: [
20
+ /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
21
+ /* @__PURE__ */ jsx(TableHead, { children: "Ticket No." }),
22
+ /* @__PURE__ */ jsx(TableHead, { children: "Violator" }),
23
+ /* @__PURE__ */ jsx(TableHead, { className: "hidden sm:table-cell", children: "Apprehended" }),
24
+ /* @__PURE__ */ jsx(TableHead, { children: "Status" }),
25
+ /* @__PURE__ */ jsx(TableHead, { className: "text-right", children: "Amount" }),
26
+ /* @__PURE__ */ jsx(TableHead, { className: "w-8" })
27
+ ] }) }),
28
+ /* @__PURE__ */ jsx(TableBody, { children: pageItems.map((t) => {
29
+ const href = `/admin/tickets/${encodeURIComponent(t.ovrTicketNo)}`;
30
+ const amount = t.status === "PAID" ? t.payment?.amount ?? 0 : t.totalAmountDue;
31
+ return /* @__PURE__ */ jsxs(
32
+ TableRow,
33
+ {
34
+ onClick: () => router.push(href),
35
+ onKeyDown: (e) => {
36
+ if (e.key === "Enter") router.push(href);
37
+ },
38
+ role: "link",
39
+ tabIndex: 0,
40
+ "aria-label": `Open ticket ${t.ovrTicketNo}`,
41
+ className: "cursor-pointer",
42
+ children: [
43
+ /* @__PURE__ */ jsx(TableCell, { className: "font-mono text-xs", children: t.ovrTicketNo }),
44
+ /* @__PURE__ */ jsx(TableCell, { className: "font-medium", children: formalName(t.violator) }),
45
+ /* @__PURE__ */ jsx(TableCell, { className: "hidden text-muted-foreground sm:table-cell", children: formatDate(t.apprehendedAt) }),
46
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(StatusBadge, { status: t.status }) }),
47
+ /* @__PURE__ */ jsx(TableCell, { className: "text-right", children: /* @__PURE__ */ jsx(Money, { value: amount }) }),
48
+ /* @__PURE__ */ jsx(TableCell, { className: "text-muted-foreground", children: /* @__PURE__ */ jsx(ChevronRight, { className: "size-4" }) })
49
+ ]
50
+ },
51
+ t.ovrTicketNo
52
+ );
53
+ }) })
54
+ ] }),
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
+ export { TicketsTable };
@@ -0,0 +1,18 @@
1
+ import { buttonVariants } from './chunk-I4WDVYHX.js';
2
+ import { useCopy } from './chunk-TJSNVTVB.js';
3
+ import { Card, CardContent } from './chunk-SETIN6XP.js';
4
+ import Link from 'next/link';
5
+ import { FileQuestion } from 'lucide-react';
6
+ import { jsx, jsxs } from 'react/jsx-runtime';
7
+
8
+ function TicketNotFound() {
9
+ const copy = useCopy();
10
+ return /* @__PURE__ */ jsx("div", { className: "mx-auto w-full max-w-lg px-4 py-16 text-center", children: /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "flex flex-col items-center gap-3 py-8", children: [
11
+ /* @__PURE__ */ jsx("div", { className: "flex size-12 items-center justify-center rounded-full bg-muted text-muted-foreground", children: /* @__PURE__ */ jsx(FileQuestion, { className: "size-6" }) }),
12
+ /* @__PURE__ */ jsx("h1", { className: "font-heading text-lg font-semibold", children: copy.citizen.notFoundTitle }),
13
+ /* @__PURE__ */ jsx("p", { className: "max-w-sm text-sm text-muted-foreground", children: copy.citizen.notFoundBody }),
14
+ /* @__PURE__ */ jsx(Link, { href: "/citizen/search", className: buttonVariants({}), children: copy.citizen.ticket.backToSearch })
15
+ ] }) }) });
16
+ }
17
+
18
+ export { TicketNotFound };
@@ -0,0 +1,74 @@
1
+ import { StatusBadge } from './chunk-OE525ZER.js';
2
+ import { Money } from './chunk-BVI5XDDA.js';
3
+ import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from './chunk-OWCGEEAZ.js';
4
+ import { useFormatters, useCopy } from './chunk-TJSNVTVB.js';
5
+ import { cn } from './chunk-77QBZC7J.js';
6
+ import Link from 'next/link';
7
+ import { ChevronRight } from 'lucide-react';
8
+ import { jsx, jsxs } from 'react/jsx-runtime';
9
+
10
+ function ViolationHistoryTable({
11
+ tickets,
12
+ currentTicketNo,
13
+ lastName
14
+ }) {
15
+ const { formatDate } = useFormatters();
16
+ const copy = useCopy();
17
+ const t = copy.citizen.ticket.history;
18
+ if (tickets.length === 0) {
19
+ return /* @__PURE__ */ jsx("div", { className: "px-4 py-10 text-center text-sm text-muted-foreground", children: t.empty });
20
+ }
21
+ return /* @__PURE__ */ jsxs(Table, { children: [
22
+ /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
23
+ /* @__PURE__ */ jsx(TableHead, { children: t.colTicketNo }),
24
+ /* @__PURE__ */ jsx(TableHead, { className: "hidden sm:table-cell", children: t.colDate }),
25
+ /* @__PURE__ */ jsx(TableHead, { children: t.colViolations }),
26
+ /* @__PURE__ */ jsx(TableHead, { children: t.colStatus }),
27
+ /* @__PURE__ */ jsx(TableHead, { className: "text-right", children: t.colAmount }),
28
+ /* @__PURE__ */ jsx(TableHead, { className: "w-8" })
29
+ ] }) }),
30
+ /* @__PURE__ */ jsx(TableBody, { children: tickets.map((ticket) => {
31
+ const isCurrent = ticket.ovrTicketNo === currentTicketNo;
32
+ const href = `/citizen/ticket/${encodeURIComponent(
33
+ ticket.ovrTicketNo
34
+ )}?ln=${encodeURIComponent(lastName)}`;
35
+ const amount = ticket.status === "PAID" ? ticket.payment?.amount ?? 0 : ticket.totalAmountDue;
36
+ const first = ticket.violations[0];
37
+ const extra = ticket.violations.length - 1;
38
+ return /* @__PURE__ */ jsxs(
39
+ TableRow,
40
+ {
41
+ className: cn(isCurrent && "bg-muted/40"),
42
+ children: [
43
+ /* @__PURE__ */ jsx(TableCell, { className: "font-mono text-xs", children: /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
44
+ /* @__PURE__ */ jsx(Link, { href, className: "hover:underline", children: ticket.ovrTicketNo }),
45
+ isCurrent ? /* @__PURE__ */ jsx("span", { className: "rounded bg-brand/12 px-1.5 py-0.5 text-[0.65rem] font-medium uppercase tracking-wide text-brand", children: t.current }) : null
46
+ ] }) }),
47
+ /* @__PURE__ */ jsx(TableCell, { className: "hidden text-muted-foreground sm:table-cell", children: formatDate(ticket.apprehendedAt) }),
48
+ /* @__PURE__ */ jsxs(TableCell, { className: "max-w-[16rem] truncate text-muted-foreground", children: [
49
+ first?.title ?? "\u2014",
50
+ extra > 0 ? /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
51
+ " +",
52
+ extra
53
+ ] }) : null
54
+ ] }),
55
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(StatusBadge, { status: ticket.status }) }),
56
+ /* @__PURE__ */ jsx(TableCell, { className: "text-right", children: /* @__PURE__ */ jsx(Money, { value: amount }) }),
57
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(
58
+ Link,
59
+ {
60
+ href,
61
+ className: "text-muted-foreground hover:text-foreground",
62
+ "aria-label": `Open ${ticket.ovrTicketNo}`,
63
+ children: /* @__PURE__ */ jsx(ChevronRight, { className: "size-4" })
64
+ }
65
+ ) })
66
+ ]
67
+ },
68
+ ticket.ovrTicketNo
69
+ );
70
+ }) })
71
+ ] });
72
+ }
73
+
74
+ export { ViolationHistoryTable };