@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.
- package/dist/auth-auth.js +1 -1
- package/dist/auth.js +1 -1
- package/dist/{chunk-MDTRBOPQ.js → chunk-2C3VCTYJ.js} +1 -1
- package/dist/chunk-3YKVH4Y7.js +126 -0
- package/dist/chunk-6YFZLXFP.js +84 -0
- package/dist/{chunk-3NZ2XUBO.js → chunk-AJ2RZTVX.js} +9 -2
- package/dist/chunk-BI4EGLPG.js +298 -0
- package/dist/{chunk-3KIDW4LT.js → chunk-BVI5XDDA.js} +1 -1
- package/dist/{chunk-BIQ2J75Y.js → chunk-GLIK5BHP.js} +2 -2
- package/dist/{chunk-5YYR37CF.js → chunk-HGWPA7FU.js} +119 -0
- package/dist/{chunk-JEYT63LE.js → chunk-IBZVIUNI.js} +1 -1
- package/dist/chunk-IS3THKTE.js +89 -0
- package/dist/{chunk-4SZXBT56.js → chunk-NT72CQAI.js} +2 -2
- package/dist/{chunk-E2D7QT6N.js → chunk-TJSNVTVB.js} +1 -1
- package/dist/{chunk-5Z2IAD5I.js → chunk-TLG4C2XI.js} +2 -2
- package/dist/chunk-V7VQVDWS.js +237 -0
- package/dist/{chunk-IF5UAVIE.js → chunk-YC7G2IOZ.js} +1 -1
- package/dist/chunk-YGYA7KEG.js +423 -0
- package/dist/{chunk-GDOCD7LT.js → chunk-ZUMEOZ22.js} +5 -5
- package/dist/core-i18n.d.ts +2 -2
- package/dist/core-i18n.js +1 -1
- package/dist/core.d.ts +61 -1
- package/dist/core.js +1 -1
- package/dist/data-mock-store.js +265 -9
- package/dist/data-prisma-store.js +254 -1
- package/dist/data-seed-runner.js +18 -15
- package/dist/data.d.ts +54 -3
- package/dist/generated/client/edge.js +29 -9
- package/dist/generated/client/index-browser.js +26 -6
- package/dist/generated/client/index.d.ts +3544 -552
- package/dist/generated/client/index.js +29 -9
- package/dist/generated/client/package.json +1 -1
- package/dist/generated/client/schema.prisma +47 -9
- package/dist/generated/client/wasm.js +29 -9
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/offline.d.ts +55 -19
- package/dist/offline.js +2 -375
- package/dist/{types-CtBC5-TW.d.ts → types-BOgdk0Jw.d.ts} +119 -0
- package/dist/types.d.ts +94 -1
- package/dist/types.js +1 -1
- package/dist/ui-components-admin/accounts-manager.d.ts +52 -0
- package/dist/ui-components-admin/accounts-manager.js +471 -0
- package/dist/ui-components-admin/admin-nav.d.ts +15 -1
- package/dist/ui-components-admin/admin-nav.js +387 -58
- package/dist/ui-components-admin/issuance-form.js +96 -23
- package/dist/ui-components-admin/logs-viewer.d.ts +13 -0
- package/dist/ui-components-admin/logs-viewer.js +102 -0
- package/dist/ui-components-admin/notifications-list.d.ts +5 -0
- package/dist/ui-components-admin/notifications-list.js +70 -0
- package/dist/ui-components-admin/officers-manager.d.ts +27 -0
- package/dist/ui-components-admin/officers-manager.js +271 -0
- package/dist/ui-components-admin/roles-manager.d.ts +37 -0
- package/dist/ui-components-admin/roles-manager.js +406 -0
- package/dist/ui-components-admin/ticket-preview.js +7 -7
- package/dist/ui-components-admin/tickets-table.js +56 -33
- package/dist/ui-components-citizen/citizen-nav.js +2 -2
- package/dist/ui-components-citizen/payment-form.js +5 -5
- package/dist/ui-components-citizen/payment-qr-dialog.js +4 -4
- package/dist/ui-components-citizen/ticket-not-found.js +2 -2
- package/dist/ui-components-citizen/violation-history-table.js +3 -3
- package/dist/ui-components-shared/amount-summary.js +4 -4
- package/dist/ui-components-shared/money.js +3 -3
- package/dist/ui-components-shared/municipal-seal.js +3 -3
- package/dist/ui-components-shared/official-header.js +4 -4
- package/dist/ui-components-shared/site-header.js +4 -4
- package/dist/ui-components-shared/sonner.js +2 -2
- package/dist/ui-components-shared/theme-toggle.js +3 -3
- package/dist/ui-components-shared/ticket-receipt.js +15 -7
- package/dist/ui-components-shared/violations-table.js +4 -4
- package/dist/ui-components-ui/badge.d.ts +1 -1
- package/dist/ui-components-ui/button.d.ts +1 -1
- package/dist/ui-components-ui/dropdown-menu.js +2 -237
- package/dist/ui-components-ui/sheet.js +3 -126
- package/dist/ui-config.d.ts +1 -1
- package/dist/ui-config.js +2 -2
- package/dist/ui-server.d.ts +1 -1
- package/dist/ui-server.js +2 -2
- package/package.json +3 -3
- package/prisma/migrations/20260622000000_add_issued_by/migration.sql +2 -0
- package/prisma/migrations/20260622010000_add_super_admin_role/migration.sql +3 -0
- package/prisma/migrations/20260622020000_add_apprehending_enforcer/migration.sql +4 -0
- package/prisma/migrations/20260622030000_custom_roles/migration.sql +30 -0
- package/prisma/migrations/20260622040000_add_activity_log/migration.sql +18 -0
- package/prisma/schema.prisma +47 -9
- package/dist/chunk-B634JHKZ.js +0 -181
package/dist/data-mock-store.js
CHANGED
|
@@ -1,20 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SUPER_ADMIN_LOCKED_PERMISSIONS, ALL_PERMISSIONS } from './chunk-IS3THKTE.js';
|
|
2
|
+
import { round2, computeCharges, makePaymentRef, fullName, addDays, makeBillNo, makeOrderOfPaymentNo, makeOvrTicketNo, enrich } from './chunk-BI4EGLPG.js';
|
|
2
3
|
import 'server-only';
|
|
3
4
|
import { promises } from 'fs';
|
|
5
|
+
import { randomUUID } from 'crypto';
|
|
4
6
|
import os from 'os';
|
|
5
7
|
import path from 'path';
|
|
6
8
|
|
|
7
|
-
var SEED_VERSION =
|
|
9
|
+
var SEED_VERSION = 5;
|
|
8
10
|
var norm = (s) => s.trim().toLowerCase();
|
|
11
|
+
function slugRole(label) {
|
|
12
|
+
return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
13
|
+
}
|
|
14
|
+
function normalizePerms(perms) {
|
|
15
|
+
const set = new Set(perms);
|
|
16
|
+
return ALL_PERMISSIONS.filter((p) => set.has(p));
|
|
17
|
+
}
|
|
9
18
|
function createMockStore(rules, seed) {
|
|
10
|
-
const { CATALOG, OFFICERS, SEED_TICKETS, SEED_NEXT_SEQ } = seed;
|
|
19
|
+
const { CATALOG, OFFICERS, SEED_TICKETS, SEED_NEXT_SEQ, USERS, ROLES } = seed;
|
|
11
20
|
const STORE_PATH = process.env.EOVR_STORE_FILE || path.join(os.tmpdir(), "eovr-store.json");
|
|
12
21
|
const enrichRec = (rec, asOf) => enrich(rec, asOf, rules.surchargeRatePerMonth);
|
|
13
22
|
function seeded() {
|
|
14
23
|
return {
|
|
15
24
|
version: SEED_VERSION,
|
|
16
25
|
counter: SEED_NEXT_SEQ,
|
|
17
|
-
tickets: structuredClone(SEED_TICKETS)
|
|
26
|
+
tickets: structuredClone(SEED_TICKETS),
|
|
27
|
+
users: structuredClone(USERS),
|
|
28
|
+
roles: structuredClone(ROLES),
|
|
29
|
+
activities: [],
|
|
30
|
+
officers: structuredClone(OFFICERS)
|
|
18
31
|
};
|
|
19
32
|
}
|
|
20
33
|
async function writeStore(data) {
|
|
@@ -24,7 +37,7 @@ function createMockStore(rules, seed) {
|
|
|
24
37
|
try {
|
|
25
38
|
const raw = await promises.readFile(STORE_PATH, "utf8");
|
|
26
39
|
const data = JSON.parse(raw);
|
|
27
|
-
if (!data || data.version !== SEED_VERSION || !Array.isArray(data.tickets) || typeof data.counter !== "number") {
|
|
40
|
+
if (!data || data.version !== SEED_VERSION || !Array.isArray(data.tickets) || !Array.isArray(data.users) || !Array.isArray(data.roles) || !Array.isArray(data.activities) || !Array.isArray(data.officers) || typeof data.counter !== "number") {
|
|
28
41
|
const fresh = seeded();
|
|
29
42
|
await writeStore(fresh);
|
|
30
43
|
return fresh;
|
|
@@ -42,16 +55,75 @@ function createMockStore(rules, seed) {
|
|
|
42
55
|
return structuredClone(items);
|
|
43
56
|
},
|
|
44
57
|
async listOfficers() {
|
|
45
|
-
|
|
58
|
+
const data = await readStore();
|
|
59
|
+
return structuredClone(data.officers);
|
|
46
60
|
},
|
|
47
61
|
async getOfficer(id) {
|
|
48
|
-
|
|
62
|
+
const data = await readStore();
|
|
63
|
+
return data.officers.find((o) => o.id === id) ?? null;
|
|
64
|
+
},
|
|
65
|
+
async createOfficer(input) {
|
|
66
|
+
const data = await readStore();
|
|
67
|
+
const name = input.name.trim();
|
|
68
|
+
if (!name) throw new Error("Officer name is required.");
|
|
69
|
+
const office = input.office.trim();
|
|
70
|
+
if (!office) throw new Error("Office is required.");
|
|
71
|
+
const id = `off-${slugRole(name)}`;
|
|
72
|
+
if (id === "off-") {
|
|
73
|
+
throw new Error("Officer name must include letters or numbers.");
|
|
74
|
+
}
|
|
75
|
+
if (data.officers.some((o) => o.id === id)) {
|
|
76
|
+
throw new Error(`An officer "${name}" already exists.`);
|
|
77
|
+
}
|
|
78
|
+
const officer = {
|
|
79
|
+
id,
|
|
80
|
+
name,
|
|
81
|
+
badgeNo: input.badgeNo?.trim() || void 0,
|
|
82
|
+
office
|
|
83
|
+
};
|
|
84
|
+
data.officers.push(officer);
|
|
85
|
+
await writeStore(data);
|
|
86
|
+
return structuredClone(officer);
|
|
87
|
+
},
|
|
88
|
+
async updateOfficer(id, patch) {
|
|
89
|
+
const data = await readStore();
|
|
90
|
+
const o = data.officers.find((x) => x.id === id);
|
|
91
|
+
if (!o) throw new Error("Officer not found.");
|
|
92
|
+
if (patch.name !== void 0) {
|
|
93
|
+
const name = patch.name.trim();
|
|
94
|
+
if (!name) throw new Error("Officer name is required.");
|
|
95
|
+
o.name = name;
|
|
96
|
+
}
|
|
97
|
+
if (patch.office !== void 0) {
|
|
98
|
+
const office = patch.office.trim();
|
|
99
|
+
if (!office) throw new Error("Office is required.");
|
|
100
|
+
o.office = office;
|
|
101
|
+
}
|
|
102
|
+
if (patch.badgeNo !== void 0) o.badgeNo = patch.badgeNo?.trim() || void 0;
|
|
103
|
+
await writeStore(data);
|
|
104
|
+
return structuredClone(o);
|
|
105
|
+
},
|
|
106
|
+
async deleteOfficer(id) {
|
|
107
|
+
const data = await readStore();
|
|
108
|
+
const o = data.officers.find((x) => x.id === id);
|
|
109
|
+
if (!o) throw new Error("Officer not found.");
|
|
110
|
+
const ticketCount = data.tickets.filter((t) => t.officer.id === id).length;
|
|
111
|
+
if (ticketCount > 0) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Can't remove \u2014 this officer has ${ticketCount} issued ticket(s).`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
if (data.users.some((u) => u.officerId === id)) {
|
|
117
|
+
throw new Error("Can't remove \u2014 this officer is linked to an account.");
|
|
118
|
+
}
|
|
119
|
+
data.officers = data.officers.filter((x) => x.id !== id);
|
|
120
|
+
await writeStore(data);
|
|
49
121
|
},
|
|
50
122
|
async createTicket(input) {
|
|
51
123
|
const store2 = await readStore();
|
|
52
124
|
const now = /* @__PURE__ */ new Date();
|
|
53
125
|
const seq = store2.counter;
|
|
54
|
-
const officer =
|
|
126
|
+
const officer = store2.officers.find((o) => o.id === input.officerId) ?? store2.officers[0];
|
|
55
127
|
const violations = input.violations.map((v) => {
|
|
56
128
|
const c = CATALOG.find((x) => x.code === v.catalogCode);
|
|
57
129
|
if (!c) throw new Error(`Unknown violation code: ${v.catalogCode}`);
|
|
@@ -82,6 +154,9 @@ function createMockStore(rules, seed) {
|
|
|
82
154
|
officer,
|
|
83
155
|
violations,
|
|
84
156
|
remarks: input.remarks?.trim() || void 0,
|
|
157
|
+
issuedBy: input.issuedBy?.trim() || void 0,
|
|
158
|
+
apprehendingEnforcerId: input.apprehendingEnforcerId || void 0,
|
|
159
|
+
apprehendingEnforcerName: input.apprehendingEnforcerName?.trim() || void 0,
|
|
85
160
|
assessedAt,
|
|
86
161
|
dueDate: addDays(now, rules.dueWindowDays).toISOString(),
|
|
87
162
|
basicFinesTotal,
|
|
@@ -109,7 +184,7 @@ function createMockStore(rules, seed) {
|
|
|
109
184
|
);
|
|
110
185
|
if (existing) return enrichRec(existing, /* @__PURE__ */ new Date());
|
|
111
186
|
const created = new Date(input.createdAt);
|
|
112
|
-
const officer =
|
|
187
|
+
const officer = data.officers.find((o) => o.id === input.officerId) ?? data.officers[0];
|
|
113
188
|
const violations = input.violations.map((v) => {
|
|
114
189
|
const c = CATALOG.find((x) => x.code === v.catalogCode);
|
|
115
190
|
if (!c) throw new Error(`Unknown violation code: ${v.catalogCode}`);
|
|
@@ -135,6 +210,9 @@ function createMockStore(rules, seed) {
|
|
|
135
210
|
officer,
|
|
136
211
|
violations,
|
|
137
212
|
remarks: input.remarks?.trim() || void 0,
|
|
213
|
+
issuedBy: input.issuedBy?.trim() || void 0,
|
|
214
|
+
apprehendingEnforcerId: input.apprehendingEnforcerId || void 0,
|
|
215
|
+
apprehendingEnforcerName: input.apprehendingEnforcerName?.trim() || void 0,
|
|
138
216
|
assessedAt: input.createdAt,
|
|
139
217
|
dueDate: addDays(created, rules.dueWindowDays).toISOString(),
|
|
140
218
|
basicFinesTotal,
|
|
@@ -220,6 +298,184 @@ function createMockStore(rules, seed) {
|
|
|
220
298
|
}) === todayKey
|
|
221
299
|
).length
|
|
222
300
|
};
|
|
301
|
+
},
|
|
302
|
+
// ── Accounts (GE-013 RBAC) ──────────────────────────────────────────────
|
|
303
|
+
// Mock mode has no real credential store (login is the demo cookie); these
|
|
304
|
+
// back the accounts UI so it's fully browsable without Postgres.
|
|
305
|
+
async listUsers() {
|
|
306
|
+
const data = await readStore();
|
|
307
|
+
return structuredClone(data.users).sort(
|
|
308
|
+
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
309
|
+
);
|
|
310
|
+
},
|
|
311
|
+
async createUser(input) {
|
|
312
|
+
const data = await readStore();
|
|
313
|
+
const username = input.username.trim();
|
|
314
|
+
if (!username) throw new Error("Username is required.");
|
|
315
|
+
const role = data.roles.find((r) => r.name === input.role);
|
|
316
|
+
if (!role) throw new Error(`Unknown role: ${input.role}`);
|
|
317
|
+
if (data.users.some((u) => norm(u.username) === norm(username))) {
|
|
318
|
+
throw new Error(`Username "${username}" is already taken.`);
|
|
319
|
+
}
|
|
320
|
+
const officer = input.officerId ? data.officers.find((o) => o.id === input.officerId) : void 0;
|
|
321
|
+
const account = {
|
|
322
|
+
id: `usr-${randomUUID()}`,
|
|
323
|
+
username,
|
|
324
|
+
role: role.name,
|
|
325
|
+
roleLabel: role.label,
|
|
326
|
+
active: true,
|
|
327
|
+
officerId: input.officerId ?? null,
|
|
328
|
+
officerName: officer?.name,
|
|
329
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
330
|
+
};
|
|
331
|
+
data.users.unshift(account);
|
|
332
|
+
await writeStore(data);
|
|
333
|
+
return structuredClone(account);
|
|
334
|
+
},
|
|
335
|
+
async setUserActive(id, active) {
|
|
336
|
+
const data = await readStore();
|
|
337
|
+
const u = data.users.find((x) => x.id === id);
|
|
338
|
+
if (!u) throw new Error("Account not found.");
|
|
339
|
+
u.active = active;
|
|
340
|
+
await writeStore(data);
|
|
341
|
+
return structuredClone(u);
|
|
342
|
+
},
|
|
343
|
+
async setUserRole(id, role) {
|
|
344
|
+
const data = await readStore();
|
|
345
|
+
const u = data.users.find((x) => x.id === id);
|
|
346
|
+
if (!u) throw new Error("Account not found.");
|
|
347
|
+
const roleDef = data.roles.find((r) => r.name === role);
|
|
348
|
+
if (!roleDef) throw new Error(`Unknown role: ${role}`);
|
|
349
|
+
u.role = roleDef.name;
|
|
350
|
+
u.roleLabel = roleDef.label;
|
|
351
|
+
await writeStore(data);
|
|
352
|
+
return structuredClone(u);
|
|
353
|
+
},
|
|
354
|
+
async updateUser(id, patch) {
|
|
355
|
+
const data = await readStore();
|
|
356
|
+
const u = data.users.find((x) => x.id === id);
|
|
357
|
+
if (!u) throw new Error("Account not found.");
|
|
358
|
+
if (patch.username !== void 0) {
|
|
359
|
+
const username = patch.username.trim();
|
|
360
|
+
if (!username) throw new Error("Username is required.");
|
|
361
|
+
if (data.users.some((x) => x.id !== id && norm(x.username) === norm(username))) {
|
|
362
|
+
throw new Error(`Username "${username}" is already taken.`);
|
|
363
|
+
}
|
|
364
|
+
u.username = username;
|
|
365
|
+
}
|
|
366
|
+
if (patch.role !== void 0) {
|
|
367
|
+
const roleDef = data.roles.find((r) => r.name === patch.role);
|
|
368
|
+
if (!roleDef) throw new Error(`Unknown role: ${patch.role}`);
|
|
369
|
+
u.role = roleDef.name;
|
|
370
|
+
u.roleLabel = roleDef.label;
|
|
371
|
+
}
|
|
372
|
+
if (patch.officerId !== void 0) {
|
|
373
|
+
u.officerId = patch.officerId ?? null;
|
|
374
|
+
const officer = patch.officerId ? data.officers.find((o) => o.id === patch.officerId) : void 0;
|
|
375
|
+
u.officerName = officer?.name;
|
|
376
|
+
}
|
|
377
|
+
await writeStore(data);
|
|
378
|
+
return structuredClone(u);
|
|
379
|
+
},
|
|
380
|
+
async resetUserPassword() {
|
|
381
|
+
},
|
|
382
|
+
// ── Roles (GE-013 RBAC) ─────────────────────────────────────────────────
|
|
383
|
+
async listRoles() {
|
|
384
|
+
const data = await readStore();
|
|
385
|
+
return structuredClone(data.roles).sort(
|
|
386
|
+
(a, b) => a.isSystem === b.isSystem ? a.label.localeCompare(b.label) : a.isSystem ? -1 : 1
|
|
387
|
+
);
|
|
388
|
+
},
|
|
389
|
+
async getRolePermissions(name) {
|
|
390
|
+
const data = await readStore();
|
|
391
|
+
const r = data.roles.find((x) => x.name === name);
|
|
392
|
+
return r ? normalizePerms(r.permissions) : [];
|
|
393
|
+
},
|
|
394
|
+
async createRole(input) {
|
|
395
|
+
const data = await readStore();
|
|
396
|
+
const label = input.label.trim();
|
|
397
|
+
if (!label) throw new Error("Role name is required.");
|
|
398
|
+
const name = slugRole(label);
|
|
399
|
+
if (!name) throw new Error("Role name must include letters or numbers.");
|
|
400
|
+
if (data.roles.some((r) => r.name === name)) {
|
|
401
|
+
throw new Error(`A role named "${label}" already exists.`);
|
|
402
|
+
}
|
|
403
|
+
const role = {
|
|
404
|
+
name,
|
|
405
|
+
label,
|
|
406
|
+
isSystem: false,
|
|
407
|
+
permissions: normalizePerms(input.permissions)
|
|
408
|
+
};
|
|
409
|
+
data.roles.push(role);
|
|
410
|
+
await writeStore(data);
|
|
411
|
+
return structuredClone(role);
|
|
412
|
+
},
|
|
413
|
+
async updateRole(name, patch) {
|
|
414
|
+
const data = await readStore();
|
|
415
|
+
const role = data.roles.find((r) => r.name === name);
|
|
416
|
+
if (!role) throw new Error("Role not found.");
|
|
417
|
+
if (patch.label !== void 0 && !role.isSystem) {
|
|
418
|
+
const label = patch.label.trim();
|
|
419
|
+
if (!label) throw new Error("Role name is required.");
|
|
420
|
+
role.label = label;
|
|
421
|
+
}
|
|
422
|
+
if (patch.permissions !== void 0) {
|
|
423
|
+
let perms = normalizePerms(patch.permissions);
|
|
424
|
+
if (name === "SUPER_ADMIN") {
|
|
425
|
+
perms = normalizePerms([...perms, ...SUPER_ADMIN_LOCKED_PERMISSIONS]);
|
|
426
|
+
}
|
|
427
|
+
role.permissions = perms;
|
|
428
|
+
}
|
|
429
|
+
await writeStore(data);
|
|
430
|
+
return structuredClone(role);
|
|
431
|
+
},
|
|
432
|
+
async deleteRole(name) {
|
|
433
|
+
const data = await readStore();
|
|
434
|
+
const role = data.roles.find((r) => r.name === name);
|
|
435
|
+
if (!role) throw new Error("Role not found.");
|
|
436
|
+
if (role.isSystem) throw new Error("System roles can't be deleted.");
|
|
437
|
+
const inUse = data.users.filter((u) => u.role === name).length;
|
|
438
|
+
if (inUse > 0) {
|
|
439
|
+
throw new Error(
|
|
440
|
+
`Can't delete "${role.label}" \u2014 it's assigned to ${inUse} account(s).`
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
data.roles = data.roles.filter((r) => r.name !== name);
|
|
444
|
+
await writeStore(data);
|
|
445
|
+
},
|
|
446
|
+
// ── Activity log (GE-022) ───────────────────────────────────────────────
|
|
447
|
+
async logActivity(entry) {
|
|
448
|
+
const data = await readStore();
|
|
449
|
+
data.activities.push({
|
|
450
|
+
id: `act-${randomUUID()}`,
|
|
451
|
+
actorId: entry.actorId ?? null,
|
|
452
|
+
actorUsername: entry.actorUsername ?? null,
|
|
453
|
+
action: entry.action,
|
|
454
|
+
summary: entry.summary,
|
|
455
|
+
targetType: entry.targetType,
|
|
456
|
+
targetId: entry.targetId,
|
|
457
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
458
|
+
});
|
|
459
|
+
await writeStore(data);
|
|
460
|
+
},
|
|
461
|
+
async listActivity(filter) {
|
|
462
|
+
const data = await readStore();
|
|
463
|
+
let items = [...data.activities].sort(
|
|
464
|
+
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
|
465
|
+
);
|
|
466
|
+
if (filter?.action) items = items.filter((a) => a.action === filter.action);
|
|
467
|
+
if (filter?.actorId)
|
|
468
|
+
items = items.filter((a) => a.actorId === filter.actorId);
|
|
469
|
+
if (filter?.fromISO) {
|
|
470
|
+
const lo = Date.parse(filter.fromISO);
|
|
471
|
+
items = items.filter((a) => Date.parse(a.createdAt) >= lo);
|
|
472
|
+
}
|
|
473
|
+
if (filter?.toISO) {
|
|
474
|
+
const hi = Date.parse(filter.toISO);
|
|
475
|
+
items = items.filter((a) => Date.parse(a.createdAt) <= hi);
|
|
476
|
+
}
|
|
477
|
+
const limit = Math.min(Math.max(filter?.limit ?? 200, 1), 1e3);
|
|
478
|
+
return structuredClone(items.slice(0, limit));
|
|
223
479
|
}
|
|
224
480
|
};
|
|
225
481
|
return store;
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { prisma } from './chunk-MKALJTAU.js';
|
|
2
|
-
import {
|
|
2
|
+
import { SUPER_ADMIN_LOCKED_PERMISSIONS, ALL_PERMISSIONS } from './chunk-IS3THKTE.js';
|
|
3
|
+
import { round2, computeCharges, makePaymentRef, fullName, addDays, makeBillNo, makeOrderOfPaymentNo, makeOvrTicketNo, enrich } from './chunk-BI4EGLPG.js';
|
|
3
4
|
import 'server-only';
|
|
4
5
|
|
|
5
6
|
var norm = (s) => s.trim().toLowerCase();
|
|
7
|
+
function slugRole(label) {
|
|
8
|
+
return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
9
|
+
}
|
|
10
|
+
function normalizePerms(perms) {
|
|
11
|
+
const set = new Set(perms);
|
|
12
|
+
return ALL_PERMISSIONS.filter((p) => set.has(p));
|
|
13
|
+
}
|
|
6
14
|
var ticketInclude = {
|
|
7
15
|
officer: true,
|
|
8
16
|
violations: true,
|
|
9
17
|
payment: true
|
|
10
18
|
};
|
|
19
|
+
var userInclude = { officer: true, roleRef: true };
|
|
11
20
|
function toOfficer(o) {
|
|
12
21
|
return {
|
|
13
22
|
id: o.id,
|
|
@@ -16,6 +25,26 @@ function toOfficer(o) {
|
|
|
16
25
|
office: o.office
|
|
17
26
|
};
|
|
18
27
|
}
|
|
28
|
+
function toUserAccount(u) {
|
|
29
|
+
return {
|
|
30
|
+
id: u.id,
|
|
31
|
+
username: u.username,
|
|
32
|
+
role: u.role,
|
|
33
|
+
roleLabel: u.roleRef?.label,
|
|
34
|
+
active: u.active,
|
|
35
|
+
officerId: u.officerId ?? null,
|
|
36
|
+
officerName: u.officer?.name,
|
|
37
|
+
createdAt: u.createdAt.toISOString()
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function toRoleDef(r) {
|
|
41
|
+
return {
|
|
42
|
+
name: r.name,
|
|
43
|
+
label: r.label,
|
|
44
|
+
isSystem: r.isSystem,
|
|
45
|
+
permissions: normalizePerms(r.permissions)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
19
48
|
function toRecord(row) {
|
|
20
49
|
const violator = {
|
|
21
50
|
firstName: row.violatorFirstName,
|
|
@@ -45,6 +74,9 @@ function toRecord(row) {
|
|
|
45
74
|
officer: toOfficer(row.officer),
|
|
46
75
|
violations,
|
|
47
76
|
remarks: row.remarks ?? void 0,
|
|
77
|
+
issuedBy: row.issuedBy ?? void 0,
|
|
78
|
+
apprehendingEnforcerId: row.apprehendingEnforcerId ?? void 0,
|
|
79
|
+
apprehendingEnforcerName: row.apprehendingEnforcerName ?? void 0,
|
|
48
80
|
assessedAt: row.assessedAt.toISOString(),
|
|
49
81
|
dueDate: row.dueDate.toISOString(),
|
|
50
82
|
basicFinesTotal: row.basicFinesTotal.toNumber(),
|
|
@@ -84,6 +116,49 @@ function createPrismaStore(rules) {
|
|
|
84
116
|
const o = await prisma.officer.findUnique({ where: { id } });
|
|
85
117
|
return o ? toOfficer(o) : null;
|
|
86
118
|
},
|
|
119
|
+
async createOfficer(input) {
|
|
120
|
+
const name = input.name.trim();
|
|
121
|
+
if (!name) throw new Error("Officer name is required.");
|
|
122
|
+
const office = input.office.trim();
|
|
123
|
+
if (!office) throw new Error("Office is required.");
|
|
124
|
+
const id = `off-${slugRole(name)}`;
|
|
125
|
+
if (id === "off-") throw new Error("Officer name must include letters or numbers.");
|
|
126
|
+
const existing = await prisma.officer.findUnique({ where: { id } });
|
|
127
|
+
if (existing) throw new Error(`An officer "${name}" already exists.`);
|
|
128
|
+
const o = await prisma.officer.create({
|
|
129
|
+
data: { id, name, badgeNo: input.badgeNo?.trim() || null, office }
|
|
130
|
+
});
|
|
131
|
+
return toOfficer(o);
|
|
132
|
+
},
|
|
133
|
+
async updateOfficer(id, patch) {
|
|
134
|
+
const data = {};
|
|
135
|
+
if (patch.name !== void 0) {
|
|
136
|
+
const name = patch.name.trim();
|
|
137
|
+
if (!name) throw new Error("Officer name is required.");
|
|
138
|
+
data.name = name;
|
|
139
|
+
}
|
|
140
|
+
if (patch.office !== void 0) {
|
|
141
|
+
const office = patch.office.trim();
|
|
142
|
+
if (!office) throw new Error("Office is required.");
|
|
143
|
+
data.office = office;
|
|
144
|
+
}
|
|
145
|
+
if (patch.badgeNo !== void 0) data.badgeNo = patch.badgeNo?.trim() || null;
|
|
146
|
+
const o = await prisma.officer.update({ where: { id }, data });
|
|
147
|
+
return toOfficer(o);
|
|
148
|
+
},
|
|
149
|
+
async deleteOfficer(id) {
|
|
150
|
+
const tickets = await prisma.ticket.count({ where: { officerId: id } });
|
|
151
|
+
if (tickets > 0) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Can't remove \u2014 this officer has ${tickets} issued ticket(s).`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
const linked = await prisma.user.count({ where: { officerId: id } });
|
|
157
|
+
if (linked > 0) {
|
|
158
|
+
throw new Error("Can't remove \u2014 this officer is linked to an account.");
|
|
159
|
+
}
|
|
160
|
+
await prisma.officer.delete({ where: { id } });
|
|
161
|
+
},
|
|
87
162
|
async createTicket(input) {
|
|
88
163
|
const now = /* @__PURE__ */ new Date();
|
|
89
164
|
const v = input.violator;
|
|
@@ -145,6 +220,9 @@ function createPrismaStore(rules) {
|
|
|
145
220
|
apprehendedAt: new Date(input.apprehendedAt),
|
|
146
221
|
placeOfViolation: input.placeOfViolation?.trim() || null,
|
|
147
222
|
remarks: input.remarks?.trim() || null,
|
|
223
|
+
issuedBy: input.issuedBy?.trim() || null,
|
|
224
|
+
apprehendingEnforcerId: input.apprehendingEnforcerId || null,
|
|
225
|
+
apprehendingEnforcerName: input.apprehendingEnforcerName?.trim() || null,
|
|
148
226
|
officerId: officer.id,
|
|
149
227
|
assessedAt: now,
|
|
150
228
|
dueDate: addDays(now, rules.dueWindowDays),
|
|
@@ -228,6 +306,9 @@ function createPrismaStore(rules) {
|
|
|
228
306
|
apprehendedAt: new Date(input.apprehendedAt),
|
|
229
307
|
placeOfViolation: input.placeOfViolation?.trim() || null,
|
|
230
308
|
remarks: input.remarks?.trim() || null,
|
|
309
|
+
issuedBy: input.issuedBy?.trim() || null,
|
|
310
|
+
apprehendingEnforcerId: input.apprehendingEnforcerId || null,
|
|
311
|
+
apprehendingEnforcerName: input.apprehendingEnforcerName?.trim() || null,
|
|
231
312
|
officerId: officer.id,
|
|
232
313
|
assessedAt: created,
|
|
233
314
|
dueDate: addDays(created, rules.dueWindowDays),
|
|
@@ -339,6 +420,178 @@ function createPrismaStore(rules) {
|
|
|
339
420
|
}) === todayKey
|
|
340
421
|
).length
|
|
341
422
|
};
|
|
423
|
+
},
|
|
424
|
+
// ── Accounts (GE-013 RBAC) ──────────────────────────────────────────────
|
|
425
|
+
async listUsers() {
|
|
426
|
+
const rows = await prisma.user.findMany({
|
|
427
|
+
include: userInclude,
|
|
428
|
+
orderBy: { createdAt: "desc" }
|
|
429
|
+
});
|
|
430
|
+
return rows.map(toUserAccount);
|
|
431
|
+
},
|
|
432
|
+
async createUser(input) {
|
|
433
|
+
const username = input.username.trim();
|
|
434
|
+
if (!username) throw new Error("Username is required.");
|
|
435
|
+
const role = await prisma.role.findUnique({ where: { name: input.role } });
|
|
436
|
+
if (!role) throw new Error(`Unknown role: ${input.role}`);
|
|
437
|
+
const existing = await prisma.user.findUnique({ where: { username } });
|
|
438
|
+
if (existing) throw new Error(`Username "${username}" is already taken.`);
|
|
439
|
+
const row = await prisma.user.create({
|
|
440
|
+
data: {
|
|
441
|
+
username,
|
|
442
|
+
passwordHash: input.passwordHash,
|
|
443
|
+
role: input.role,
|
|
444
|
+
active: true,
|
|
445
|
+
officerId: input.officerId ?? null
|
|
446
|
+
},
|
|
447
|
+
include: userInclude
|
|
448
|
+
});
|
|
449
|
+
return toUserAccount(row);
|
|
450
|
+
},
|
|
451
|
+
async setUserActive(id, active) {
|
|
452
|
+
const row = await prisma.user.update({
|
|
453
|
+
where: { id },
|
|
454
|
+
data: { active },
|
|
455
|
+
include: userInclude
|
|
456
|
+
});
|
|
457
|
+
return toUserAccount(row);
|
|
458
|
+
},
|
|
459
|
+
async setUserRole(id, role) {
|
|
460
|
+
const exists = await prisma.role.findUnique({ where: { name: role } });
|
|
461
|
+
if (!exists) throw new Error(`Unknown role: ${role}`);
|
|
462
|
+
const row = await prisma.user.update({
|
|
463
|
+
where: { id },
|
|
464
|
+
data: { role },
|
|
465
|
+
include: userInclude
|
|
466
|
+
});
|
|
467
|
+
return toUserAccount(row);
|
|
468
|
+
},
|
|
469
|
+
async updateUser(id, patch) {
|
|
470
|
+
const data = {};
|
|
471
|
+
if (patch.username !== void 0) {
|
|
472
|
+
const username = patch.username.trim();
|
|
473
|
+
if (!username) throw new Error("Username is required.");
|
|
474
|
+
const existing = await prisma.user.findUnique({ where: { username } });
|
|
475
|
+
if (existing && existing.id !== id) {
|
|
476
|
+
throw new Error(`Username "${username}" is already taken.`);
|
|
477
|
+
}
|
|
478
|
+
data.username = username;
|
|
479
|
+
}
|
|
480
|
+
if (patch.role !== void 0) {
|
|
481
|
+
const role = await prisma.role.findUnique({ where: { name: patch.role } });
|
|
482
|
+
if (!role) throw new Error(`Unknown role: ${patch.role}`);
|
|
483
|
+
data.roleRef = { connect: { name: patch.role } };
|
|
484
|
+
}
|
|
485
|
+
if (patch.officerId !== void 0) {
|
|
486
|
+
data.officer = patch.officerId ? { connect: { id: patch.officerId } } : { disconnect: true };
|
|
487
|
+
}
|
|
488
|
+
const row = await prisma.user.update({
|
|
489
|
+
where: { id },
|
|
490
|
+
data,
|
|
491
|
+
include: userInclude
|
|
492
|
+
});
|
|
493
|
+
return toUserAccount(row);
|
|
494
|
+
},
|
|
495
|
+
async resetUserPassword(id, passwordHash) {
|
|
496
|
+
await prisma.user.update({ where: { id }, data: { passwordHash } });
|
|
497
|
+
},
|
|
498
|
+
// ── Roles (GE-013 RBAC) ─────────────────────────────────────────────────
|
|
499
|
+
async listRoles() {
|
|
500
|
+
const rows = await prisma.role.findMany({
|
|
501
|
+
orderBy: [{ isSystem: "desc" }, { label: "asc" }]
|
|
502
|
+
});
|
|
503
|
+
return rows.map(toRoleDef);
|
|
504
|
+
},
|
|
505
|
+
async getRolePermissions(name) {
|
|
506
|
+
const r = await prisma.role.findUnique({ where: { name } });
|
|
507
|
+
return r ? normalizePerms(r.permissions) : [];
|
|
508
|
+
},
|
|
509
|
+
async createRole(input) {
|
|
510
|
+
const label = input.label.trim();
|
|
511
|
+
if (!label) throw new Error("Role name is required.");
|
|
512
|
+
const name = slugRole(label);
|
|
513
|
+
if (!name) throw new Error("Role name must include letters or numbers.");
|
|
514
|
+
const existing = await prisma.role.findUnique({ where: { name } });
|
|
515
|
+
if (existing) throw new Error(`A role named "${label}" already exists.`);
|
|
516
|
+
const row = await prisma.role.create({
|
|
517
|
+
data: {
|
|
518
|
+
name,
|
|
519
|
+
label,
|
|
520
|
+
isSystem: false,
|
|
521
|
+
permissions: normalizePerms(input.permissions)
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
return toRoleDef(row);
|
|
525
|
+
},
|
|
526
|
+
async updateRole(name, patch) {
|
|
527
|
+
const role = await prisma.role.findUnique({ where: { name } });
|
|
528
|
+
if (!role) throw new Error("Role not found.");
|
|
529
|
+
const data = {};
|
|
530
|
+
if (patch.label !== void 0 && !role.isSystem) {
|
|
531
|
+
const label = patch.label.trim();
|
|
532
|
+
if (!label) throw new Error("Role name is required.");
|
|
533
|
+
data.label = label;
|
|
534
|
+
}
|
|
535
|
+
if (patch.permissions !== void 0) {
|
|
536
|
+
let perms = normalizePerms(patch.permissions);
|
|
537
|
+
if (name === "SUPER_ADMIN") {
|
|
538
|
+
perms = normalizePerms([...perms, ...SUPER_ADMIN_LOCKED_PERMISSIONS]);
|
|
539
|
+
}
|
|
540
|
+
data.permissions = perms;
|
|
541
|
+
}
|
|
542
|
+
const row = await prisma.role.update({ where: { name }, data });
|
|
543
|
+
return toRoleDef(row);
|
|
544
|
+
},
|
|
545
|
+
async deleteRole(name) {
|
|
546
|
+
const role = await prisma.role.findUnique({ where: { name } });
|
|
547
|
+
if (!role) throw new Error("Role not found.");
|
|
548
|
+
if (role.isSystem) throw new Error("System roles can't be deleted.");
|
|
549
|
+
const inUse = await prisma.user.count({ where: { role: name } });
|
|
550
|
+
if (inUse > 0) {
|
|
551
|
+
throw new Error(
|
|
552
|
+
`Can't delete "${role.label}" \u2014 it's assigned to ${inUse} account(s).`
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
await prisma.role.delete({ where: { name } });
|
|
556
|
+
},
|
|
557
|
+
// ── Activity log (GE-022) ───────────────────────────────────────────────
|
|
558
|
+
async logActivity(entry) {
|
|
559
|
+
await prisma.activityLog.create({
|
|
560
|
+
data: {
|
|
561
|
+
actorId: entry.actorId ?? null,
|
|
562
|
+
actorUsername: entry.actorUsername ?? null,
|
|
563
|
+
action: entry.action,
|
|
564
|
+
summary: entry.summary,
|
|
565
|
+
targetType: entry.targetType ?? null,
|
|
566
|
+
targetId: entry.targetId ?? null
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
},
|
|
570
|
+
async listActivity(filter) {
|
|
571
|
+
const where = {};
|
|
572
|
+
if (filter?.action) where.action = filter.action;
|
|
573
|
+
if (filter?.actorId) where.actorId = filter.actorId;
|
|
574
|
+
if (filter?.fromISO || filter?.toISO) {
|
|
575
|
+
const range = {};
|
|
576
|
+
if (filter.fromISO) range.gte = new Date(filter.fromISO);
|
|
577
|
+
if (filter.toISO) range.lte = new Date(filter.toISO);
|
|
578
|
+
where.createdAt = range;
|
|
579
|
+
}
|
|
580
|
+
const rows = await prisma.activityLog.findMany({
|
|
581
|
+
where,
|
|
582
|
+
orderBy: { createdAt: "desc" },
|
|
583
|
+
take: Math.min(Math.max(filter?.limit ?? 200, 1), 1e3)
|
|
584
|
+
});
|
|
585
|
+
return rows.map((r) => ({
|
|
586
|
+
id: r.id,
|
|
587
|
+
actorId: r.actorId ?? null,
|
|
588
|
+
actorUsername: r.actorUsername ?? null,
|
|
589
|
+
action: r.action,
|
|
590
|
+
summary: r.summary,
|
|
591
|
+
targetType: r.targetType ?? void 0,
|
|
592
|
+
targetId: r.targetId ?? void 0,
|
|
593
|
+
createdAt: r.createdAt.toISOString()
|
|
594
|
+
}));
|
|
342
595
|
}
|
|
343
596
|
};
|
|
344
597
|
return store;
|
package/dist/data-seed-runner.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
// ../ovr-data/src/seed-runner.ts
|
|
2
2
|
async function seedRunner(prisma, data) {
|
|
3
|
-
const { catalog, officers, tickets, users, passwordHash } = data;
|
|
3
|
+
const { catalog, officers, tickets, users, roles, passwordHash } = data;
|
|
4
|
+
for (const r of roles) {
|
|
5
|
+
await prisma.role.upsert({
|
|
6
|
+
where: { name: r.name },
|
|
7
|
+
update: { label: r.label, isSystem: r.isSystem, permissions: r.permissions },
|
|
8
|
+
create: {
|
|
9
|
+
name: r.name,
|
|
10
|
+
label: r.label,
|
|
11
|
+
isSystem: r.isSystem,
|
|
12
|
+
permissions: r.permissions
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
4
16
|
for (const o of officers) {
|
|
5
17
|
await prisma.officer.upsert({
|
|
6
18
|
where: { id: o.id },
|
|
@@ -77,21 +89,12 @@ async function seedRunner(prisma, data) {
|
|
|
77
89
|
});
|
|
78
90
|
}
|
|
79
91
|
for (const u of users) {
|
|
92
|
+
const role = u.role ?? "ENFORCER";
|
|
93
|
+
const officerId = u.officerId ?? null;
|
|
80
94
|
await prisma.user.upsert({
|
|
81
95
|
where: { username: u.username },
|
|
82
|
-
update: {
|
|
83
|
-
|
|
84
|
-
role: "ENFORCER",
|
|
85
|
-
active: true,
|
|
86
|
-
officerId: u.officerId
|
|
87
|
-
},
|
|
88
|
-
create: {
|
|
89
|
-
username: u.username,
|
|
90
|
-
passwordHash,
|
|
91
|
-
role: "ENFORCER",
|
|
92
|
-
active: true,
|
|
93
|
-
officerId: u.officerId
|
|
94
|
-
}
|
|
96
|
+
update: { passwordHash, role, active: true, officerId },
|
|
97
|
+
create: { username: u.username, passwordHash, role, active: true, officerId }
|
|
95
98
|
});
|
|
96
99
|
}
|
|
97
100
|
await prisma.$executeRawUnsafe(`
|
|
@@ -102,7 +105,7 @@ async function seedRunner(prisma, data) {
|
|
|
102
105
|
)
|
|
103
106
|
`);
|
|
104
107
|
console.log(
|
|
105
|
-
`\u2713 Seed complete: ${officers.length} officers, ${catalog.length} catalog items, ${tickets.length} tickets, ${users.length} users.`
|
|
108
|
+
`\u2713 Seed complete: ${roles.length} roles, ${officers.length} officers, ${catalog.length} catalog items, ${tickets.length} tickets, ${users.length} users.`
|
|
106
109
|
);
|
|
107
110
|
}
|
|
108
111
|
|