@gelabs/ovr 0.2.1 → 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 (86) 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-YGYA7KEG.js +423 -0
  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 +265 -9
  25. package/dist/data-prisma-store.js +254 -1
  26. package/dist/data-seed-runner.js +18 -15
  27. package/dist/data.d.ts +54 -3
  28. package/dist/generated/client/edge.js +29 -9
  29. package/dist/generated/client/index-browser.js +26 -6
  30. package/dist/generated/client/index.d.ts +3544 -552
  31. package/dist/generated/client/index.js +29 -9
  32. package/dist/generated/client/package.json +1 -1
  33. package/dist/generated/client/schema.prisma +47 -9
  34. package/dist/generated/client/wasm.js +29 -9
  35. package/dist/index.d.ts +2 -2
  36. package/dist/index.js +1 -1
  37. package/dist/offline.d.ts +55 -19
  38. package/dist/offline.js +2 -375
  39. package/dist/{types-CtBC5-TW.d.ts → types-BOgdk0Jw.d.ts} +119 -0
  40. package/dist/types.d.ts +94 -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 +387 -58
  46. package/dist/ui-components-admin/issuance-form.js +96 -23
  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 +15 -7
  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 +3 -3
  80. package/prisma/migrations/20260622000000_add_issued_by/migration.sql +2 -0
  81. package/prisma/migrations/20260622010000_add_super_admin_role/migration.sql +3 -0
  82. package/prisma/migrations/20260622020000_add_apprehending_enforcer/migration.sql +4 -0
  83. package/prisma/migrations/20260622030000_custom_roles/migration.sql +30 -0
  84. package/prisma/migrations/20260622040000_add_activity_log/migration.sql +18 -0
  85. package/prisma/schema.prisma +47 -9
  86. package/dist/chunk-B634JHKZ.js +0 -181
package/dist/offline.js CHANGED
@@ -1,376 +1,3 @@
1
1
  "use client";
2
- import { makeOvrTicketNo, addDays, makeBillNo, makeOrderOfPaymentNo, enrich } from './chunk-B634JHKZ.js';
3
- import Dexie from 'dexie';
4
- import { useLiveQuery } from 'dexie-react-hooks';
5
- import { useState, useEffect } from 'react';
6
-
7
- var db = new Dexie("eovr-offline");
8
- db.version(1).stores({
9
- tickets: "ovrTicketNo, paymentStatus, createdAt",
10
- catalog: "code, category",
11
- officers: "id",
12
- meta: "key"
13
- });
14
- db.version(2).stores({
15
- outbox: "ovrTicketNo, status, createdAt"
16
- });
17
- async function getMeta(key) {
18
- const row = await db.meta.get(key);
19
- return row?.value;
20
- }
21
- async function setMeta(key, value) {
22
- await db.meta.put({ key, value });
23
- }
24
-
25
- // ../ovr-offline/src/sync.ts
26
- var API = "/api";
27
- var LEASE_MIN = 10;
28
- var LEASE_BATCH = 50;
29
- function setOfflineApiBase(base) {
30
- API = base.replace(/\/$/, "");
31
- }
32
- function offlineApiBase() {
33
- return API;
34
- }
35
- var SessionExpired = class extends Error {
36
- constructor() {
37
- super("session_expired");
38
- this.name = "SessionExpired";
39
- }
40
- };
41
- function isOnline() {
42
- return typeof navigator !== "undefined" ? navigator.onLine : true;
43
- }
44
- async function call(path, init) {
45
- let res;
46
- try {
47
- res = await fetch(`${API}${path}`, { credentials: "include", ...init });
48
- } catch {
49
- return null;
50
- }
51
- if (res.status === 401) throw new SessionExpired();
52
- return res;
53
- }
54
- var postInit = (body) => ({
55
- method: "POST",
56
- headers: { "content-type": "application/json" },
57
- body: JSON.stringify(body)
58
- });
59
- async function ensureLease(min = LEASE_MIN) {
60
- if (!isOnline()) return;
61
- const seqs = await getMeta("leaseSeqs") ?? [];
62
- if (seqs.length >= min) return;
63
- const res = await call("/sync/lease", postInit({ count: LEASE_BATCH }));
64
- if (!res || !res.ok) return;
65
- const { seqs: fresh } = await res.json();
66
- await setMeta("leaseSeqs", [...seqs, ...fresh]);
67
- }
68
- async function pushOutbox() {
69
- const pending = await db.outbox.toArray();
70
- if (!pending.length) return;
71
- const res = await call(
72
- "/sync/push",
73
- postInit({ tickets: pending.map((p) => p.payload) })
74
- );
75
- if (!res || !res.ok) return;
76
- const { results } = await res.json();
77
- await db.transaction("rw", db.outbox, db.tickets, async () => {
78
- for (const r of results) {
79
- if (r.ok) {
80
- await db.outbox.delete(r.ovrTicketNo);
81
- if (r.ticket) await db.tickets.put(r.ticket);
82
- } else {
83
- await db.outbox.update(r.ovrTicketNo, { status: "error", error: r.error });
84
- }
85
- }
86
- });
87
- }
88
- async function pullAll() {
89
- const [t, c, o, s] = await Promise.all([
90
- call("/tickets"),
91
- call("/violations"),
92
- call("/officers"),
93
- call("/tickets/stats")
94
- ]);
95
- if (!t || !c || !o || !s) return;
96
- if (!t.ok || !c.ok || !o.ok || !s.ok) {
97
- throw new Error(
98
- `sync.pullAll failed: tickets=${t.status} catalog=${c.status} officers=${o.status} stats=${s.status}`
99
- );
100
- }
101
- const tickets = await t.json();
102
- const catalog = await c.json();
103
- const officers = await o.json();
104
- const stats = await s.json();
105
- await db.transaction(
106
- "rw",
107
- db.tickets,
108
- db.catalog,
109
- db.officers,
110
- db.meta,
111
- async () => {
112
- await db.tickets.bulkPut(tickets);
113
- await db.catalog.clear();
114
- await db.catalog.bulkPut(catalog);
115
- await db.officers.clear();
116
- await db.officers.bulkPut(officers);
117
- await db.meta.put({ key: "stats", value: stats });
118
- }
119
- );
120
- await setMeta("lastSyncedAt", (/* @__PURE__ */ new Date()).toISOString());
121
- }
122
- async function sync() {
123
- if (!isOnline()) return;
124
- await pushOutbox();
125
- await ensureLease();
126
- await pullAll();
127
- }
128
-
129
- // ../ovr-offline/src/issue.ts
130
- async function popSeq() {
131
- return db.transaction("rw", db.meta, async () => {
132
- const row = await db.meta.get("leaseSeqs");
133
- const seqs = row?.value ?? [];
134
- if (seqs.length === 0) return null;
135
- await db.meta.put({ key: "leaseSeqs", value: seqs.slice(1) });
136
- return seqs[0];
137
- });
138
- }
139
- async function issueTicketOffline(input, rules) {
140
- let seq = await popSeq();
141
- if (seq === null && isOnline()) {
142
- await ensureLease(50);
143
- seq = await popSeq();
144
- }
145
- if (seq === null) {
146
- throw new Error(
147
- isOnline() ? "Couldn't reserve ticket numbers \u2014 please try again." : "No offline ticket numbers left. Reconnect to the internet to reserve more."
148
- );
149
- }
150
- const now = /* @__PURE__ */ new Date();
151
- const ovrTicketNo = makeOvrTicketNo(rules.idPrefix, now.getFullYear(), seq);
152
- const officer = await db.officers.get(input.officerId);
153
- if (!officer) {
154
- const cur = await getMeta("leaseSeqs") ?? [];
155
- await setMeta("leaseSeqs", [seq, ...cur]);
156
- throw new Error("That officer isn't available offline. Sync and try again.");
157
- }
158
- const catalog = await db.catalog.toArray();
159
- const byCode = new Map(catalog.map((c) => [c.code, c]));
160
- const violations = input.violations.map((x) => {
161
- const c = byCode.get(x.catalogCode);
162
- if (!c) throw new Error(`Unknown violation: ${x.catalogCode}`);
163
- const v = {
164
- catalogCode: c.code,
165
- title: c.title,
166
- basicFine: c.basicFine
167
- };
168
- if (x.details?.trim()) v.details = x.details.trim();
169
- return v;
170
- });
171
- const basicFinesTotal = violations.reduce((s, v) => s + v.basicFine, 0);
172
- const record = {
173
- ovrTicketNo,
174
- orderOfPaymentNo: makeOrderOfPaymentNo(ovrTicketNo),
175
- billNo: makeBillNo(now, officer.office, officer.badgeNo ?? "X000", seq),
176
- violator: input.violator,
177
- apprehendedAt: input.apprehendedAt,
178
- officer,
179
- violations,
180
- assessedAt: now.toISOString(),
181
- dueDate: addDays(now, rules.dueWindowDays).toISOString(),
182
- basicFinesTotal,
183
- paymentStatus: "UNPAID",
184
- createdAt: now.toISOString()
185
- };
186
- if (input.placeOfViolation?.trim())
187
- record.placeOfViolation = input.placeOfViolation.trim();
188
- if (input.remarks?.trim()) record.remarks = input.remarks.trim();
189
- const ticket = enrich(record, now, rules.surchargeRatePerMonth);
190
- await db.transaction("rw", db.tickets, db.outbox, async () => {
191
- await db.tickets.put(ticket);
192
- await db.outbox.put({
193
- ovrTicketNo,
194
- createdAt: record.createdAt,
195
- status: "pending",
196
- payload: {
197
- ovrTicketNo,
198
- createdAt: record.createdAt,
199
- violator: input.violator,
200
- apprehendedAt: input.apprehendedAt,
201
- ...record.placeOfViolation ? { placeOfViolation: record.placeOfViolation } : {},
202
- officerId: input.officerId,
203
- violations: input.violations,
204
- ...record.remarks ? { remarks: record.remarks } : {}
205
- }
206
- });
207
- });
208
- if (isOnline()) void pushOutbox().catch(() => {
209
- });
210
- return ticket;
211
- }
212
- async function getIdentity() {
213
- return await getMeta("identity") ?? void 0;
214
- }
215
- async function clearIdentity() {
216
- await setMeta("identity", null);
217
- }
218
- function useAdminAuth() {
219
- const [state, setState] = useState({ status: "loading" });
220
- useEffect(() => {
221
- let cancelled = false;
222
- (async () => {
223
- const cached = await getMeta("identity");
224
- if (isOnline()) {
225
- try {
226
- const res = await fetch(`${offlineApiBase()}/auth/me`, {
227
- credentials: "include"
228
- });
229
- if (res.ok) {
230
- const { user } = await res.json();
231
- await setMeta("identity", user);
232
- if (!cancelled) setState({ status: "authed", user });
233
- return;
234
- }
235
- await setMeta("identity", null);
236
- if (!cancelled) setState({ status: "unauthed" });
237
- return;
238
- } catch {
239
- }
240
- }
241
- if (!cancelled) {
242
- setState(
243
- cached ? { status: "authed", user: cached } : { status: "unauthed" }
244
- );
245
- }
246
- })();
247
- return () => {
248
- cancelled = true;
249
- };
250
- }, []);
251
- return state;
252
- }
253
- var PBKDF2_ITERATIONS = 21e4;
254
- function subtle() {
255
- return typeof crypto !== "undefined" && crypto.subtle ? crypto.subtle : null;
256
- }
257
- var toHex = (buf) => Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, "0")).join(
258
- ""
259
- );
260
- function fromHex(hex) {
261
- const out = new Uint8Array(hex.length / 2);
262
- for (let i = 0; i < out.length; i++)
263
- out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
264
- return out;
265
- }
266
- async function deriveHashHex(password, salt, iterations) {
267
- const s = subtle();
268
- if (!s) return null;
269
- const keyMaterial = await s.importKey(
270
- "raw",
271
- new TextEncoder().encode(password),
272
- "PBKDF2",
273
- false,
274
- ["deriveBits"]
275
- );
276
- const bits = await s.deriveBits(
277
- { name: "PBKDF2", salt, iterations, hash: "SHA-256" },
278
- keyMaterial,
279
- 256
280
- );
281
- return toHex(bits);
282
- }
283
- async function cacheCredential(username, password, identity) {
284
- if (!subtle()) return;
285
- const salt = crypto.getRandomValues(new Uint8Array(16));
286
- const hashHex = await deriveHashHex(password, salt, PBKDF2_ITERATIONS);
287
- if (!hashHex) return;
288
- const cred = {
289
- username: username.trim(),
290
- saltHex: toHex(salt.buffer),
291
- hashHex,
292
- iterations: PBKDF2_ITERATIONS,
293
- identity
294
- };
295
- await setMeta("credential", cred);
296
- }
297
- async function verifyOffline(username, password) {
298
- const cred = await getMeta("credential");
299
- if (!cred || cred.username !== username.trim()) return null;
300
- const hashHex = await deriveHashHex(
301
- password,
302
- fromHex(cred.saltHex),
303
- cred.iterations
304
- );
305
- if (!hashHex || hashHex !== cred.hashHex) return null;
306
- await setMeta("identity", cred.identity);
307
- return cred.identity;
308
- }
309
-
310
- // ../ovr-offline/src/hooks.ts
311
- function useTickets() {
312
- return useLiveQuery(() => db.tickets.orderBy("createdAt").reverse().toArray());
313
- }
314
- function useCatalog() {
315
- return useLiveQuery(() => db.catalog.orderBy("category").toArray());
316
- }
317
- function useOfficers() {
318
- return useLiveQuery(() => db.officers.toArray());
319
- }
320
- function useStats() {
321
- return useLiveQuery(
322
- async () => (await db.meta.get("stats"))?.value
323
- );
324
- }
325
- function useTicket(ovrTicketNo) {
326
- return useLiveQuery(
327
- async () => await db.tickets.get(ovrTicketNo) ?? null,
328
- [ovrTicketNo]
329
- );
330
- }
331
- function usePendingSync() {
332
- const rows = useLiveQuery(() => db.outbox.toArray());
333
- return new Set((rows ?? []).map((r) => r.ovrTicketNo));
334
- }
335
- function useSync() {
336
- const [syncing, setSyncing] = useState(false);
337
- const [online, setOnline] = useState(true);
338
- const [error, setError] = useState(null);
339
- useEffect(() => {
340
- let cancelled = false;
341
- setOnline(isOnline());
342
- async function runSyncCycle() {
343
- if (!isOnline()) return;
344
- setSyncing(true);
345
- setError(null);
346
- try {
347
- await sync();
348
- } catch (e) {
349
- if (e instanceof SessionExpired) {
350
- await clearIdentity();
351
- window.location.assign("/admin/login");
352
- return;
353
- }
354
- if (!cancelled) setError(e instanceof Error ? e.message : "sync failed");
355
- } finally {
356
- if (!cancelled) setSyncing(false);
357
- }
358
- }
359
- const onOnline = () => {
360
- setOnline(true);
361
- void runSyncCycle();
362
- };
363
- const onOffline = () => setOnline(false);
364
- void runSyncCycle();
365
- window.addEventListener("online", onOnline);
366
- window.addEventListener("offline", onOffline);
367
- return () => {
368
- cancelled = true;
369
- window.removeEventListener("online", onOnline);
370
- window.removeEventListener("offline", onOffline);
371
- };
372
- }, []);
373
- return { syncing, online, error };
374
- }
375
-
376
- export { SessionExpired, cacheCredential, clearIdentity, db, ensureLease, getIdentity, getMeta, isOnline, issueTicketOffline, offlineApiBase, pullAll, pushOutbox, setMeta, setOfflineApiBase, sync, useAdminAuth, useCatalog, useOfficers, usePendingSync, useStats, useSync, useTicket, useTickets, verifyOffline };
2
+ export { SessionExpired, cacheCredential, clearIdentity, db, ensureLease, getIdentity, getMeta, isOnline, issueTicketOffline, offlineApiBase, pullAll, pushOutbox, setMeta, setOfflineApiBase, sync, useAdminAuth, useCatalog, useIdentity, useNotifications, useNotificationsState, useOfficers, usePendingSync, useStats, useSync, useTicket, useTickets, verifyOffline } from './chunk-YGYA7KEG.js';
3
+ import './chunk-BI4EGLPG.js';
@@ -101,7 +101,126 @@ declare const baseCopy: {
101
101
  readonly dashboard: "Dashboard";
102
102
  readonly tickets: "Tickets";
103
103
  readonly issueTicket: "Issue ticket";
104
+ readonly accounts: "Accounts";
105
+ readonly roles: "Roles";
106
+ readonly officers: "Officers";
107
+ readonly logs: "Activity log";
108
+ readonly more: "More";
109
+ readonly signOut: "Sign out";
110
+ readonly signOutConfirmTitle: "Sign out?";
111
+ readonly signOutConfirmBody: "You'll need to sign in again to issue or manage tickets.";
112
+ readonly cancel: "Cancel";
104
113
  readonly newTicketTitle: "Issue Violation Ticket";
114
+ readonly accountsPage: {
115
+ readonly title: "Accounts";
116
+ readonly subtitle: "Create and manage who can sign in to the enforcer portal. Each account's role decides what it can see and do.";
117
+ readonly newAccount: "New account";
118
+ readonly edit: "Edit";
119
+ readonly editTitle: "Edit account";
120
+ readonly saveChanges: "Save changes";
121
+ readonly username: "Username";
122
+ readonly password: "Password";
123
+ readonly role: "Role";
124
+ readonly officer: "Linked officer";
125
+ readonly officerNone: "— None —";
126
+ readonly officerHint: "Link an enforcer to an officer record so issued tickets are attributed.";
127
+ readonly status: "Status";
128
+ readonly active: "Active";
129
+ readonly inactive: "Inactive";
130
+ readonly created: "Created";
131
+ readonly actions: "Actions";
132
+ readonly create: "Create account";
133
+ readonly creating: "Creating…";
134
+ readonly deactivate: "Deactivate";
135
+ readonly activate: "Activate";
136
+ readonly resetPassword: "Reset password";
137
+ readonly newPassword: "New password";
138
+ readonly save: "Save";
139
+ readonly saving: "Saving…";
140
+ readonly cancel: "Cancel";
141
+ readonly empty: "No accounts yet — create the first one.";
142
+ readonly you: "You";
143
+ };
144
+ readonly rolesPage: {
145
+ readonly title: "Roles & permissions";
146
+ readonly subtitle: "Define what each role can do. Create custom roles, set their permissions, then assign them to accounts.";
147
+ readonly newRole: "New role";
148
+ readonly roleName: "Role name";
149
+ readonly roleNamePlaceholder: "e.g. Cashier, Supervisor";
150
+ readonly permissions: "Permissions";
151
+ readonly editPermissions: "Edit permissions";
152
+ readonly noPermissions: "No permissions";
153
+ readonly permissionsCount: "permissions";
154
+ readonly system: "System";
155
+ readonly custom: "Custom";
156
+ readonly create: "Create role";
157
+ readonly creating: "Creating…";
158
+ readonly save: "Save";
159
+ readonly saving: "Saving…";
160
+ readonly saved: "Saved";
161
+ readonly cancel: "Cancel";
162
+ readonly delete: "Delete";
163
+ readonly deleteConfirmTitle: "Delete this role?";
164
+ readonly deleteConfirmBody: "Move any accounts off this role first. This can't be undone.";
165
+ readonly lockedHint: "Super Admin always keeps account & role management.";
166
+ readonly empty: "No roles yet — create the first one.";
167
+ readonly inUseSuffix: "in use";
168
+ };
169
+ readonly logsPage: {
170
+ readonly title: "Activity log";
171
+ readonly subtitle: "Audit trail of what accounts did — sign-ins, ticket issuance, and account & role changes.";
172
+ readonly allActions: "All actions";
173
+ readonly search: "Search actor or detail…";
174
+ readonly actor: "Actor";
175
+ readonly action: "Action";
176
+ readonly detail: "Detail";
177
+ readonly when: "When";
178
+ readonly empty: "No activity recorded yet.";
179
+ readonly system: "system";
180
+ };
181
+ readonly officersPage: {
182
+ readonly title: "Apprehending officers";
183
+ readonly subtitle: "Officers who apprehend violations — shown in the Issue-Ticket form and linked to enforcer accounts.";
184
+ readonly newOfficer: "New officer";
185
+ readonly name: "Full name";
186
+ readonly namePlaceholder: "e.g. DELA CRUZ, JUAN P.";
187
+ readonly badge: "Badge no.";
188
+ readonly badgePlaceholder: "e.g. A176";
189
+ readonly office: "Office";
190
+ readonly officePlaceholder: "e.g. POSO";
191
+ readonly edit: "Edit";
192
+ readonly editTitle: "Edit officer";
193
+ readonly create: "Add officer";
194
+ readonly creating: "Adding…";
195
+ readonly save: "Save";
196
+ readonly saving: "Saving…";
197
+ readonly cancel: "Cancel";
198
+ readonly delete: "Remove";
199
+ readonly deleteConfirmTitle: "Remove this officer?";
200
+ readonly deleteConfirmBody: "This can't be undone. Officers with issued tickets or a linked account can't be removed.";
201
+ readonly empty: "No officers yet — add the first one.";
202
+ readonly actions: "Actions";
203
+ };
204
+ readonly notifications: {
205
+ readonly title: "Notifications";
206
+ readonly subtitle: "Tickets that need attention — overdue and outstanding.";
207
+ readonly empty: "You're all caught up.";
208
+ readonly overdue: "Overdue";
209
+ readonly outstanding: "Outstanding";
210
+ readonly viewAll: "View all";
211
+ };
212
+ readonly changePassword: {
213
+ readonly title: "Change password";
214
+ readonly subtitle: "Enter your current password, then your new one.";
215
+ readonly current: "Current password";
216
+ readonly new: "New password";
217
+ readonly confirm: "Confirm new password";
218
+ readonly submit: "Update password";
219
+ readonly saving: "Updating…";
220
+ readonly cancel: "Cancel";
221
+ readonly mismatch: "New passwords don't match.";
222
+ readonly success: "Password updated.";
223
+ };
105
224
  readonly sync: {
106
225
  readonly offline: "Offline";
107
226
  readonly syncing: "Syncing…";
package/dist/types.d.ts CHANGED
@@ -42,6 +42,12 @@ interface Officer {
42
42
  badgeNo?: string;
43
43
  office: string;
44
44
  }
45
+ /** What an admin submits to add/edit an apprehending officer (GE-025). */
46
+ interface NewOfficerInput {
47
+ name: string;
48
+ badgeNo?: string;
49
+ office: string;
50
+ }
45
51
  type PaymentMethod = "GCASH" | "MAYA" | "LANDBANK" | "OVER_THE_COUNTER";
46
52
  interface Payment {
47
53
  method: PaymentMethod;
@@ -64,6 +70,9 @@ interface TicketRecord {
64
70
  officer: Officer;
65
71
  violations: IssuedViolation[];
66
72
  remarks?: string;
73
+ issuedBy?: string;
74
+ apprehendingEnforcerId?: string;
75
+ apprehendingEnforcerName?: string;
67
76
  assessedAt: string;
68
77
  dueDate: string;
69
78
  basicFinesTotal: number;
@@ -77,5 +86,89 @@ interface Ticket extends TicketRecord {
77
86
  penaltyAmount: number;
78
87
  totalAmountDue: number;
79
88
  }
89
+ /** Every capability the app gates on. Add a gate by extending this + the catalog. */
90
+ type Permission = "dashboard:view" | "tickets:view" | "tickets:create" | "accounts:manage" | "roles:manage" | "officers:manage" | "logs:view" | "notifications:view";
91
+ interface PermissionDef {
92
+ key: Permission;
93
+ label: string;
94
+ description: string;
95
+ }
96
+ /** The catalog shown in the Roles editor (array order = display order). */
97
+ declare const PERMISSION_CATALOG: PermissionDef[];
98
+ declare const ALL_PERMISSIONS: Permission[];
99
+ /** A role + its permission set. `isSystem` roles can't be renamed or deleted. */
100
+ interface RoleDef {
101
+ name: string;
102
+ label: string;
103
+ isSystem: boolean;
104
+ permissions: Permission[];
105
+ }
106
+ /** What an admin submits to create a custom role. */
107
+ interface NewRoleInput {
108
+ label: string;
109
+ permissions: Permission[];
110
+ }
111
+ /** The built-in roles, seeded into every deployment. */
112
+ declare const SYSTEM_ROLES: RoleDef[];
113
+ /** Default role assigned to a new account / seeded login. */
114
+ declare const DEFAULT_ROLE_NAME = "ENFORCER";
115
+ /** SUPER_ADMIN ALWAYS keeps these (anti-lockout): locked-on in the Roles editor
116
+ * and re-asserted server-side, so no one can revoke their own ability to manage
117
+ * accounts/roles and lock everyone out. */
118
+ declare const SUPER_ADMIN_LOCKED_PERMISSIONS: Permission[];
119
+ /** Whether a permission set grants a capability. Safe with null/undefined. */
120
+ declare function hasPermission(permissions: readonly string[] | null | undefined, permission: Permission): boolean;
121
+ /** A user account as shown to admins (NEVER carries the password hash). */
122
+ interface UserAccount {
123
+ id: string;
124
+ username: string;
125
+ role: string;
126
+ roleLabel?: string;
127
+ active: boolean;
128
+ officerId: string | null;
129
+ /** Convenience: the linked officer's display name, if any. */
130
+ officerName?: string;
131
+ createdAt: string;
132
+ }
133
+ /** What an admin submits to create an account. The password hash is computed by
134
+ * the app (argon2 stays out of the data layer), mirroring the seed path. */
135
+ interface NewUserInput {
136
+ username: string;
137
+ passwordHash: string;
138
+ role: string;
139
+ officerId?: string | null;
140
+ }
141
+ /** The recorded action kinds (extensible; stored as a string for forward-compat). */
142
+ declare const ACTIVITY_ACTIONS: readonly ["auth.login", "auth.logout", "ticket.issued", "account.create", "account.update", "account.activate", "account.deactivate", "account.password_reset", "account.password_change", "role.create", "role.update", "role.delete", "officer.create", "officer.update", "officer.delete"];
143
+ type ActivityAction = (typeof ACTIVITY_ACTIONS)[number];
144
+ declare const ACTIVITY_ACTION_LABELS: Record<ActivityAction, string>;
145
+ /** One audit-trail entry: who did what, when. */
146
+ interface ActivityLog {
147
+ id: string;
148
+ actorId: string | null;
149
+ actorUsername: string | null;
150
+ action: string;
151
+ summary: string;
152
+ targetType?: string;
153
+ targetId?: string;
154
+ createdAt: string;
155
+ }
156
+ /** What the app records for an event (id + createdAt are assigned by the store). */
157
+ interface NewActivityInput {
158
+ actorId?: string | null;
159
+ actorUsername?: string | null;
160
+ action: ActivityAction;
161
+ summary: string;
162
+ targetType?: string;
163
+ targetId?: string;
164
+ }
165
+ /** Filter for an activity-log query. */
166
+ interface ActivityFilter {
167
+ action?: string;
168
+ actorId?: string;
169
+ fromISO?: string;
170
+ toISO?: string;
171
+ limit?: number;
172
+ }
80
173
 
81
- export type { IssuedViolation, Officer, Payment, PaymentMethod, PaymentStatus, Ticket, TicketRecord, TicketStatus, ViolationCatalogItem, ViolationCategory, Violator };
174
+ export { ACTIVITY_ACTIONS, ACTIVITY_ACTION_LABELS, ALL_PERMISSIONS, type ActivityAction, type ActivityFilter, type ActivityLog, DEFAULT_ROLE_NAME, type IssuedViolation, type NewActivityInput, type NewOfficerInput, type NewRoleInput, type NewUserInput, type Officer, PERMISSION_CATALOG, type Payment, type PaymentMethod, type PaymentStatus, type Permission, type PermissionDef, type RoleDef, SUPER_ADMIN_LOCKED_PERMISSIONS, SYSTEM_ROLES, type Ticket, type TicketRecord, type TicketStatus, type UserAccount, type ViolationCatalogItem, type ViolationCategory, type Violator, hasPermission };
package/dist/types.js CHANGED
@@ -1 +1 @@
1
-
1
+ export { ACTIVITY_ACTIONS, ACTIVITY_ACTION_LABELS, ALL_PERMISSIONS, DEFAULT_ROLE_NAME, PERMISSION_CATALOG, SUPER_ADMIN_LOCKED_PERMISSIONS, SYSTEM_ROLES, hasPermission } from './chunk-IS3THKTE.js';
@@ -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 };