@gelabs/ovr 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-DJMUW5T2.js +298 -0
- package/dist/{chunk-BIQ2J75Y.js → chunk-GLIK5BHP.js} +2 -2
- package/dist/{chunk-JEYT63LE.js → chunk-IBZVIUNI.js} +1 -1
- 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-WUNTHINH.js +98 -0
- package/dist/{chunk-IF5UAVIE.js → chunk-YC7G2IOZ.js} +1 -1
- package/dist/{chunk-IB4JVGKJ.js → chunk-YGYA7KEG.js} +47 -3
- 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 +330 -12
- package/dist/data-prisma-store.js +319 -9
- package/dist/data-seed-runner.js +18 -15
- package/dist/data.d.ts +64 -3
- package/dist/generated/client/edge.js +31 -10
- package/dist/generated/client/index-browser.js +28 -7
- package/dist/generated/client/index.d.ts +3583 -577
- package/dist/generated/client/index.js +31 -10
- package/dist/generated/client/package.json +1 -1
- package/dist/generated/client/schema.prisma +48 -9
- package/dist/generated/client/wasm.js +31 -10
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/offline.d.ts +34 -1
- package/dist/offline.js +2 -2
- package/dist/types-B8MopM4b.d.ts +281 -0
- package/dist/types.d.ts +104 -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 +388 -60
- package/dist/ui-components-admin/issuance-form.js +72 -13
- 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-admin/violations-manager.d.ts +32 -0
- package/dist/ui-components-admin/violations-manager.js +385 -0
- 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 +13 -6
- 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 +6 -6
- 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/migrations/20260622050000_violation_catalog_management/migration.sql +5 -0
- package/prisma/schema.prisma +48 -9
- package/dist/chunk-5YYR37CF.js +0 -146
- package/dist/chunk-B634JHKZ.js +0 -181
- package/dist/types-CtBC5-TW.d.ts +0 -129
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { prisma } from './chunk-MKALJTAU.js';
|
|
2
|
-
import {
|
|
2
|
+
import { SUPER_ADMIN_LOCKED_PERMISSIONS, ALL_PERMISSIONS } from './chunk-WUNTHINH.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,36 @@ function toOfficer(o) {
|
|
|
16
25
|
office: o.office
|
|
17
26
|
};
|
|
18
27
|
}
|
|
28
|
+
function toViolation(c) {
|
|
29
|
+
return {
|
|
30
|
+
code: c.code,
|
|
31
|
+
title: c.title,
|
|
32
|
+
category: c.category,
|
|
33
|
+
basicFine: c.basicFine.toNumber(),
|
|
34
|
+
legalText: c.legalText ?? void 0,
|
|
35
|
+
active: c.active
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function toUserAccount(u) {
|
|
39
|
+
return {
|
|
40
|
+
id: u.id,
|
|
41
|
+
username: u.username,
|
|
42
|
+
role: u.role,
|
|
43
|
+
roleLabel: u.roleRef?.label,
|
|
44
|
+
active: u.active,
|
|
45
|
+
officerId: u.officerId ?? null,
|
|
46
|
+
officerName: u.officer?.name,
|
|
47
|
+
createdAt: u.createdAt.toISOString()
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function toRoleDef(r) {
|
|
51
|
+
return {
|
|
52
|
+
name: r.name,
|
|
53
|
+
label: r.label,
|
|
54
|
+
isSystem: r.isSystem,
|
|
55
|
+
permissions: normalizePerms(r.permissions)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
19
58
|
function toRecord(row) {
|
|
20
59
|
const violator = {
|
|
21
60
|
firstName: row.violatorFirstName,
|
|
@@ -46,6 +85,8 @@ function toRecord(row) {
|
|
|
46
85
|
violations,
|
|
47
86
|
remarks: row.remarks ?? void 0,
|
|
48
87
|
issuedBy: row.issuedBy ?? void 0,
|
|
88
|
+
apprehendingEnforcerId: row.apprehendingEnforcerId ?? void 0,
|
|
89
|
+
apprehendingEnforcerName: row.apprehendingEnforcerName ?? void 0,
|
|
49
90
|
assessedAt: row.assessedAt.toISOString(),
|
|
50
91
|
dueDate: row.dueDate.toISOString(),
|
|
51
92
|
basicFinesTotal: row.basicFinesTotal.toNumber(),
|
|
@@ -64,16 +105,66 @@ function createPrismaStore(rules) {
|
|
|
64
105
|
const store = {
|
|
65
106
|
async listViolationCatalog(category) {
|
|
66
107
|
const items = await prisma.violationCatalog.findMany({
|
|
67
|
-
where: category ? { category } :
|
|
108
|
+
where: { active: true, ...category ? { category } : {} },
|
|
68
109
|
orderBy: [{ category: "asc" }, { code: "asc" }]
|
|
69
110
|
});
|
|
70
|
-
return items.map(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
})
|
|
111
|
+
return items.map(toViolation);
|
|
112
|
+
},
|
|
113
|
+
// ── Violation catalog management (GE-031) ──────────────────────────────
|
|
114
|
+
async listAllViolations() {
|
|
115
|
+
const items = await prisma.violationCatalog.findMany({
|
|
116
|
+
orderBy: [{ category: "asc" }, { code: "asc" }]
|
|
117
|
+
});
|
|
118
|
+
return items.map(toViolation);
|
|
119
|
+
},
|
|
120
|
+
async createViolation(input) {
|
|
121
|
+
const code = input.code.trim();
|
|
122
|
+
if (!code) throw new Error("Violation code is required.");
|
|
123
|
+
const title = input.title.trim();
|
|
124
|
+
if (!title) throw new Error("Title is required.");
|
|
125
|
+
if (!(input.basicFine >= 0)) throw new Error("Basic fine must be \u2265 0.");
|
|
126
|
+
const existing = await prisma.violationCatalog.findUnique({
|
|
127
|
+
where: { code }
|
|
128
|
+
});
|
|
129
|
+
if (existing) {
|
|
130
|
+
throw new Error(`A violation with code "${code}" already exists.`);
|
|
131
|
+
}
|
|
132
|
+
const c = await prisma.violationCatalog.create({
|
|
133
|
+
data: {
|
|
134
|
+
code,
|
|
135
|
+
title,
|
|
136
|
+
category: input.category,
|
|
137
|
+
basicFine: round2(input.basicFine),
|
|
138
|
+
legalText: input.legalText?.trim() || null,
|
|
139
|
+
active: true
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
return toViolation(c);
|
|
143
|
+
},
|
|
144
|
+
async updateViolation(code, patch) {
|
|
145
|
+
const data = {};
|
|
146
|
+
if (patch.title !== void 0) {
|
|
147
|
+
const title = patch.title.trim();
|
|
148
|
+
if (!title) throw new Error("Title is required.");
|
|
149
|
+
data.title = title;
|
|
150
|
+
}
|
|
151
|
+
if (patch.category !== void 0) data.category = patch.category;
|
|
152
|
+
if (patch.basicFine !== void 0) {
|
|
153
|
+
if (!(patch.basicFine >= 0)) throw new Error("Basic fine must be \u2265 0.");
|
|
154
|
+
data.basicFine = round2(patch.basicFine);
|
|
155
|
+
}
|
|
156
|
+
if (patch.legalText !== void 0) {
|
|
157
|
+
data.legalText = patch.legalText?.trim() || null;
|
|
158
|
+
}
|
|
159
|
+
if (patch.active !== void 0) data.active = patch.active;
|
|
160
|
+
const c = await prisma.violationCatalog.update({ where: { code }, data });
|
|
161
|
+
return toViolation(c);
|
|
162
|
+
},
|
|
163
|
+
async deleteViolation(code) {
|
|
164
|
+
await prisma.violationCatalog.update({
|
|
165
|
+
where: { code },
|
|
166
|
+
data: { active: false }
|
|
167
|
+
});
|
|
77
168
|
},
|
|
78
169
|
async listOfficers() {
|
|
79
170
|
const officers = await prisma.officer.findMany({
|
|
@@ -85,6 +176,49 @@ function createPrismaStore(rules) {
|
|
|
85
176
|
const o = await prisma.officer.findUnique({ where: { id } });
|
|
86
177
|
return o ? toOfficer(o) : null;
|
|
87
178
|
},
|
|
179
|
+
async createOfficer(input) {
|
|
180
|
+
const name = input.name.trim();
|
|
181
|
+
if (!name) throw new Error("Officer name is required.");
|
|
182
|
+
const office = input.office.trim();
|
|
183
|
+
if (!office) throw new Error("Office is required.");
|
|
184
|
+
const id = `off-${slugRole(name)}`;
|
|
185
|
+
if (id === "off-") throw new Error("Officer name must include letters or numbers.");
|
|
186
|
+
const existing = await prisma.officer.findUnique({ where: { id } });
|
|
187
|
+
if (existing) throw new Error(`An officer "${name}" already exists.`);
|
|
188
|
+
const o = await prisma.officer.create({
|
|
189
|
+
data: { id, name, badgeNo: input.badgeNo?.trim() || null, office }
|
|
190
|
+
});
|
|
191
|
+
return toOfficer(o);
|
|
192
|
+
},
|
|
193
|
+
async updateOfficer(id, patch) {
|
|
194
|
+
const data = {};
|
|
195
|
+
if (patch.name !== void 0) {
|
|
196
|
+
const name = patch.name.trim();
|
|
197
|
+
if (!name) throw new Error("Officer name is required.");
|
|
198
|
+
data.name = name;
|
|
199
|
+
}
|
|
200
|
+
if (patch.office !== void 0) {
|
|
201
|
+
const office = patch.office.trim();
|
|
202
|
+
if (!office) throw new Error("Office is required.");
|
|
203
|
+
data.office = office;
|
|
204
|
+
}
|
|
205
|
+
if (patch.badgeNo !== void 0) data.badgeNo = patch.badgeNo?.trim() || null;
|
|
206
|
+
const o = await prisma.officer.update({ where: { id }, data });
|
|
207
|
+
return toOfficer(o);
|
|
208
|
+
},
|
|
209
|
+
async deleteOfficer(id) {
|
|
210
|
+
const tickets = await prisma.ticket.count({ where: { officerId: id } });
|
|
211
|
+
if (tickets > 0) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Can't remove \u2014 this officer has ${tickets} issued ticket(s).`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const linked = await prisma.user.count({ where: { officerId: id } });
|
|
217
|
+
if (linked > 0) {
|
|
218
|
+
throw new Error("Can't remove \u2014 this officer is linked to an account.");
|
|
219
|
+
}
|
|
220
|
+
await prisma.officer.delete({ where: { id } });
|
|
221
|
+
},
|
|
88
222
|
async createTicket(input) {
|
|
89
223
|
const now = /* @__PURE__ */ new Date();
|
|
90
224
|
const v = input.violator;
|
|
@@ -147,6 +281,8 @@ function createPrismaStore(rules) {
|
|
|
147
281
|
placeOfViolation: input.placeOfViolation?.trim() || null,
|
|
148
282
|
remarks: input.remarks?.trim() || null,
|
|
149
283
|
issuedBy: input.issuedBy?.trim() || null,
|
|
284
|
+
apprehendingEnforcerId: input.apprehendingEnforcerId || null,
|
|
285
|
+
apprehendingEnforcerName: input.apprehendingEnforcerName?.trim() || null,
|
|
150
286
|
officerId: officer.id,
|
|
151
287
|
assessedAt: now,
|
|
152
288
|
dueDate: addDays(now, rules.dueWindowDays),
|
|
@@ -231,6 +367,8 @@ function createPrismaStore(rules) {
|
|
|
231
367
|
placeOfViolation: input.placeOfViolation?.trim() || null,
|
|
232
368
|
remarks: input.remarks?.trim() || null,
|
|
233
369
|
issuedBy: input.issuedBy?.trim() || null,
|
|
370
|
+
apprehendingEnforcerId: input.apprehendingEnforcerId || null,
|
|
371
|
+
apprehendingEnforcerName: input.apprehendingEnforcerName?.trim() || null,
|
|
234
372
|
officerId: officer.id,
|
|
235
373
|
assessedAt: created,
|
|
236
374
|
dueDate: addDays(created, rules.dueWindowDays),
|
|
@@ -342,6 +480,178 @@ function createPrismaStore(rules) {
|
|
|
342
480
|
}) === todayKey
|
|
343
481
|
).length
|
|
344
482
|
};
|
|
483
|
+
},
|
|
484
|
+
// ── Accounts (GE-013 RBAC) ──────────────────────────────────────────────
|
|
485
|
+
async listUsers() {
|
|
486
|
+
const rows = await prisma.user.findMany({
|
|
487
|
+
include: userInclude,
|
|
488
|
+
orderBy: { createdAt: "desc" }
|
|
489
|
+
});
|
|
490
|
+
return rows.map(toUserAccount);
|
|
491
|
+
},
|
|
492
|
+
async createUser(input) {
|
|
493
|
+
const username = input.username.trim();
|
|
494
|
+
if (!username) throw new Error("Username is required.");
|
|
495
|
+
const role = await prisma.role.findUnique({ where: { name: input.role } });
|
|
496
|
+
if (!role) throw new Error(`Unknown role: ${input.role}`);
|
|
497
|
+
const existing = await prisma.user.findUnique({ where: { username } });
|
|
498
|
+
if (existing) throw new Error(`Username "${username}" is already taken.`);
|
|
499
|
+
const row = await prisma.user.create({
|
|
500
|
+
data: {
|
|
501
|
+
username,
|
|
502
|
+
passwordHash: input.passwordHash,
|
|
503
|
+
role: input.role,
|
|
504
|
+
active: true,
|
|
505
|
+
officerId: input.officerId ?? null
|
|
506
|
+
},
|
|
507
|
+
include: userInclude
|
|
508
|
+
});
|
|
509
|
+
return toUserAccount(row);
|
|
510
|
+
},
|
|
511
|
+
async setUserActive(id, active) {
|
|
512
|
+
const row = await prisma.user.update({
|
|
513
|
+
where: { id },
|
|
514
|
+
data: { active },
|
|
515
|
+
include: userInclude
|
|
516
|
+
});
|
|
517
|
+
return toUserAccount(row);
|
|
518
|
+
},
|
|
519
|
+
async setUserRole(id, role) {
|
|
520
|
+
const exists = await prisma.role.findUnique({ where: { name: role } });
|
|
521
|
+
if (!exists) throw new Error(`Unknown role: ${role}`);
|
|
522
|
+
const row = await prisma.user.update({
|
|
523
|
+
where: { id },
|
|
524
|
+
data: { role },
|
|
525
|
+
include: userInclude
|
|
526
|
+
});
|
|
527
|
+
return toUserAccount(row);
|
|
528
|
+
},
|
|
529
|
+
async updateUser(id, patch) {
|
|
530
|
+
const data = {};
|
|
531
|
+
if (patch.username !== void 0) {
|
|
532
|
+
const username = patch.username.trim();
|
|
533
|
+
if (!username) throw new Error("Username is required.");
|
|
534
|
+
const existing = await prisma.user.findUnique({ where: { username } });
|
|
535
|
+
if (existing && existing.id !== id) {
|
|
536
|
+
throw new Error(`Username "${username}" is already taken.`);
|
|
537
|
+
}
|
|
538
|
+
data.username = username;
|
|
539
|
+
}
|
|
540
|
+
if (patch.role !== void 0) {
|
|
541
|
+
const role = await prisma.role.findUnique({ where: { name: patch.role } });
|
|
542
|
+
if (!role) throw new Error(`Unknown role: ${patch.role}`);
|
|
543
|
+
data.roleRef = { connect: { name: patch.role } };
|
|
544
|
+
}
|
|
545
|
+
if (patch.officerId !== void 0) {
|
|
546
|
+
data.officer = patch.officerId ? { connect: { id: patch.officerId } } : { disconnect: true };
|
|
547
|
+
}
|
|
548
|
+
const row = await prisma.user.update({
|
|
549
|
+
where: { id },
|
|
550
|
+
data,
|
|
551
|
+
include: userInclude
|
|
552
|
+
});
|
|
553
|
+
return toUserAccount(row);
|
|
554
|
+
},
|
|
555
|
+
async resetUserPassword(id, passwordHash) {
|
|
556
|
+
await prisma.user.update({ where: { id }, data: { passwordHash } });
|
|
557
|
+
},
|
|
558
|
+
// ── Roles (GE-013 RBAC) ─────────────────────────────────────────────────
|
|
559
|
+
async listRoles() {
|
|
560
|
+
const rows = await prisma.role.findMany({
|
|
561
|
+
orderBy: [{ isSystem: "desc" }, { label: "asc" }]
|
|
562
|
+
});
|
|
563
|
+
return rows.map(toRoleDef);
|
|
564
|
+
},
|
|
565
|
+
async getRolePermissions(name) {
|
|
566
|
+
const r = await prisma.role.findUnique({ where: { name } });
|
|
567
|
+
return r ? normalizePerms(r.permissions) : [];
|
|
568
|
+
},
|
|
569
|
+
async createRole(input) {
|
|
570
|
+
const label = input.label.trim();
|
|
571
|
+
if (!label) throw new Error("Role name is required.");
|
|
572
|
+
const name = slugRole(label);
|
|
573
|
+
if (!name) throw new Error("Role name must include letters or numbers.");
|
|
574
|
+
const existing = await prisma.role.findUnique({ where: { name } });
|
|
575
|
+
if (existing) throw new Error(`A role named "${label}" already exists.`);
|
|
576
|
+
const row = await prisma.role.create({
|
|
577
|
+
data: {
|
|
578
|
+
name,
|
|
579
|
+
label,
|
|
580
|
+
isSystem: false,
|
|
581
|
+
permissions: normalizePerms(input.permissions)
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
return toRoleDef(row);
|
|
585
|
+
},
|
|
586
|
+
async updateRole(name, patch) {
|
|
587
|
+
const role = await prisma.role.findUnique({ where: { name } });
|
|
588
|
+
if (!role) throw new Error("Role not found.");
|
|
589
|
+
const data = {};
|
|
590
|
+
if (patch.label !== void 0 && !role.isSystem) {
|
|
591
|
+
const label = patch.label.trim();
|
|
592
|
+
if (!label) throw new Error("Role name is required.");
|
|
593
|
+
data.label = label;
|
|
594
|
+
}
|
|
595
|
+
if (patch.permissions !== void 0) {
|
|
596
|
+
let perms = normalizePerms(patch.permissions);
|
|
597
|
+
if (name === "SUPER_ADMIN") {
|
|
598
|
+
perms = normalizePerms([...perms, ...SUPER_ADMIN_LOCKED_PERMISSIONS]);
|
|
599
|
+
}
|
|
600
|
+
data.permissions = perms;
|
|
601
|
+
}
|
|
602
|
+
const row = await prisma.role.update({ where: { name }, data });
|
|
603
|
+
return toRoleDef(row);
|
|
604
|
+
},
|
|
605
|
+
async deleteRole(name) {
|
|
606
|
+
const role = await prisma.role.findUnique({ where: { name } });
|
|
607
|
+
if (!role) throw new Error("Role not found.");
|
|
608
|
+
if (role.isSystem) throw new Error("System roles can't be deleted.");
|
|
609
|
+
const inUse = await prisma.user.count({ where: { role: name } });
|
|
610
|
+
if (inUse > 0) {
|
|
611
|
+
throw new Error(
|
|
612
|
+
`Can't delete "${role.label}" \u2014 it's assigned to ${inUse} account(s).`
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
await prisma.role.delete({ where: { name } });
|
|
616
|
+
},
|
|
617
|
+
// ── Activity log (GE-022) ───────────────────────────────────────────────
|
|
618
|
+
async logActivity(entry) {
|
|
619
|
+
await prisma.activityLog.create({
|
|
620
|
+
data: {
|
|
621
|
+
actorId: entry.actorId ?? null,
|
|
622
|
+
actorUsername: entry.actorUsername ?? null,
|
|
623
|
+
action: entry.action,
|
|
624
|
+
summary: entry.summary,
|
|
625
|
+
targetType: entry.targetType ?? null,
|
|
626
|
+
targetId: entry.targetId ?? null
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
},
|
|
630
|
+
async listActivity(filter) {
|
|
631
|
+
const where = {};
|
|
632
|
+
if (filter?.action) where.action = filter.action;
|
|
633
|
+
if (filter?.actorId) where.actorId = filter.actorId;
|
|
634
|
+
if (filter?.fromISO || filter?.toISO) {
|
|
635
|
+
const range = {};
|
|
636
|
+
if (filter.fromISO) range.gte = new Date(filter.fromISO);
|
|
637
|
+
if (filter.toISO) range.lte = new Date(filter.toISO);
|
|
638
|
+
where.createdAt = range;
|
|
639
|
+
}
|
|
640
|
+
const rows = await prisma.activityLog.findMany({
|
|
641
|
+
where,
|
|
642
|
+
orderBy: { createdAt: "desc" },
|
|
643
|
+
take: Math.min(Math.max(filter?.limit ?? 200, 1), 1e3)
|
|
644
|
+
});
|
|
645
|
+
return rows.map((r) => ({
|
|
646
|
+
id: r.id,
|
|
647
|
+
actorId: r.actorId ?? null,
|
|
648
|
+
actorUsername: r.actorUsername ?? null,
|
|
649
|
+
action: r.action,
|
|
650
|
+
summary: r.summary,
|
|
651
|
+
targetType: r.targetType ?? void 0,
|
|
652
|
+
targetId: r.targetId ?? void 0,
|
|
653
|
+
createdAt: r.createdAt.toISOString()
|
|
654
|
+
}));
|
|
345
655
|
}
|
|
346
656
|
};
|
|
347
657
|
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
|
|
package/dist/data.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ViolationCatalogItem, Officer, TicketRecord, ViolationCategory, Violator, Ticket, TicketStatus, PaymentMethod } from './types.js';
|
|
1
|
+
import { ViolationCatalogItem, Officer, TicketRecord, UserAccount, RoleDef, ViolationCategory, NewViolationInput, NewOfficerInput, Violator, Ticket, TicketStatus, PaymentMethod, NewUserInput, Permission, NewRoleInput, NewActivityInput, ActivityFilter, ActivityLog } from './types.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The data-access contract. UI code depends ONLY on this interface — never on a
|
|
@@ -19,6 +19,8 @@ interface NewTicketInput {
|
|
|
19
19
|
}[];
|
|
20
20
|
remarks?: string;
|
|
21
21
|
issuedBy?: string;
|
|
22
|
+
apprehendingEnforcerId?: string;
|
|
23
|
+
apprehendingEnforcerName?: string;
|
|
22
24
|
}
|
|
23
25
|
/** An offline-issued ticket pushed to the server (client-assigned number + time). */
|
|
24
26
|
interface PushTicketInput extends NewTicketInput {
|
|
@@ -42,9 +44,26 @@ interface TicketStats {
|
|
|
42
44
|
issuedToday: number;
|
|
43
45
|
}
|
|
44
46
|
interface DataStore {
|
|
47
|
+
/** ACTIVE catalog entries only (what enforcers may issue against). */
|
|
45
48
|
listViolationCatalog(category?: ViolationCategory): Promise<ViolationCatalogItem[]>;
|
|
49
|
+
/** ALL catalog entries incl. archived — for the admin Violations manager. */
|
|
50
|
+
listAllViolations(): Promise<ViolationCatalogItem[]>;
|
|
51
|
+
/** Add a catalog entry (code is the id; must be unique). */
|
|
52
|
+
createViolation(input: NewViolationInput): Promise<ViolationCatalogItem>;
|
|
53
|
+
/** Edit an entry's title/category/fine/legalText, or (un)archive it. */
|
|
54
|
+
updateViolation(code: string, patch: Partial<Omit<NewViolationInput, "code">> & {
|
|
55
|
+
active?: boolean;
|
|
56
|
+
}): Promise<ViolationCatalogItem>;
|
|
57
|
+
/** Archive an entry (soft-delete: active=false) so old tickets stay resolvable. */
|
|
58
|
+
deleteViolation(code: string): Promise<void>;
|
|
46
59
|
listOfficers(): Promise<Officer[]>;
|
|
47
60
|
getOfficer(id: string): Promise<Officer | null>;
|
|
61
|
+
/** Add an apprehending officer (id is slugified from the name; must be unique). */
|
|
62
|
+
createOfficer(input: NewOfficerInput): Promise<Officer>;
|
|
63
|
+
/** Edit an officer's name / badge / office. */
|
|
64
|
+
updateOfficer(id: string, patch: Partial<NewOfficerInput>): Promise<Officer>;
|
|
65
|
+
/** Remove an officer. Throws if they have issued tickets or are linked to an account. */
|
|
66
|
+
deleteOfficer(id: string): Promise<void>;
|
|
48
67
|
createTicket(input: NewTicketInput): Promise<Ticket>;
|
|
49
68
|
/** Reserve a block of global ticket-sequence numbers (offline ID leasing). */
|
|
50
69
|
leaseSeqs(count: number): Promise<{
|
|
@@ -58,6 +77,40 @@ interface DataStore {
|
|
|
58
77
|
listTickets(filter?: TicketFilter): Promise<Ticket[]>;
|
|
59
78
|
payTicket(ovrTicketNo: string, payment: NewPaymentInput): Promise<Ticket>;
|
|
60
79
|
stats(): Promise<TicketStats>;
|
|
80
|
+
/** All accounts, newest first (never includes the password hash). */
|
|
81
|
+
listUsers(): Promise<UserAccount[]>;
|
|
82
|
+
/** Create an account. Throws if the username is already taken. */
|
|
83
|
+
createUser(input: NewUserInput): Promise<UserAccount>;
|
|
84
|
+
/** Activate / deactivate an account (soft enable/disable of login). */
|
|
85
|
+
setUserActive(id: string, active: boolean): Promise<UserAccount>;
|
|
86
|
+
/** Change a role (must be an existing role name). */
|
|
87
|
+
setUserRole(id: string, role: string): Promise<UserAccount>;
|
|
88
|
+
/** Edit an account's username / role / linked officer (any subset). */
|
|
89
|
+
updateUser(id: string, patch: {
|
|
90
|
+
username?: string;
|
|
91
|
+
role?: string;
|
|
92
|
+
officerId?: string | null;
|
|
93
|
+
}): Promise<UserAccount>;
|
|
94
|
+
/** Replace the password hash (admin-initiated reset). */
|
|
95
|
+
resetUserPassword(id: string, passwordHash: string): Promise<void>;
|
|
96
|
+
/** All roles (system first, then custom), for the Roles editor + account dropdown. */
|
|
97
|
+
listRoles(): Promise<RoleDef[]>;
|
|
98
|
+
/** Effective permissions for a role name ([] if the role is unknown). */
|
|
99
|
+
getRolePermissions(name: string): Promise<Permission[]>;
|
|
100
|
+
/** Create a custom role (name is slugified from the label; must be unique). */
|
|
101
|
+
createRole(input: NewRoleInput): Promise<RoleDef>;
|
|
102
|
+
/** Update a role's label and/or permissions. SUPER_ADMIN keeps its locked perms. */
|
|
103
|
+
updateRole(name: string, patch: {
|
|
104
|
+
label?: string;
|
|
105
|
+
permissions?: Permission[];
|
|
106
|
+
}): Promise<RoleDef>;
|
|
107
|
+
/** Delete a custom role. Throws if it's a system role or still assigned to a user. */
|
|
108
|
+
deleteRole(name: string): Promise<void>;
|
|
109
|
+
/** Record an audit-trail entry (append-only; never throws on a logging failure
|
|
110
|
+
* in callers — treat as best-effort). */
|
|
111
|
+
logActivity(entry: NewActivityInput): Promise<void>;
|
|
112
|
+
/** List activity newest-first, optionally filtered (action / actor / date / limit). */
|
|
113
|
+
listActivity(filter?: ActivityFilter): Promise<ActivityLog[]>;
|
|
61
114
|
}
|
|
62
115
|
/** Seed data injected into the mock store factory (per-LGU, app-owned). */
|
|
63
116
|
interface MockSeed {
|
|
@@ -65,11 +118,17 @@ interface MockSeed {
|
|
|
65
118
|
OFFICERS: Officer[];
|
|
66
119
|
SEED_TICKETS: TicketRecord[];
|
|
67
120
|
SEED_NEXT_SEQ: number;
|
|
121
|
+
/** Accounts shown in the mock accounts UI (mock login still uses the demo cookie). */
|
|
122
|
+
USERS: UserAccount[];
|
|
123
|
+
/** Roles available in the mock Roles editor + account dropdown. */
|
|
124
|
+
ROLES: RoleDef[];
|
|
68
125
|
}
|
|
69
|
-
/** One
|
|
126
|
+
/** One seeded login. Defaults to an ENFORCER linked to an officer; a SUPER_ADMIN
|
|
127
|
+
* or ADMIN may omit `officerId`. `role` is a role name. */
|
|
70
128
|
interface SeedUser {
|
|
71
129
|
username: string;
|
|
72
|
-
officerId
|
|
130
|
+
officerId?: string | null;
|
|
131
|
+
role?: string;
|
|
73
132
|
}
|
|
74
133
|
/** Everything the Postgres seed runner needs (argon2 hash pre-computed). */
|
|
75
134
|
interface SeedData {
|
|
@@ -77,6 +136,8 @@ interface SeedData {
|
|
|
77
136
|
officers: Officer[];
|
|
78
137
|
tickets: TicketRecord[];
|
|
79
138
|
users: SeedUser[];
|
|
139
|
+
/** Roles to upsert (system roles seeded from SYSTEM_ROLES). */
|
|
140
|
+
roles: RoleDef[];
|
|
80
141
|
/** argon2 hash of the demo password — computed by the app (keeps argon2 out of this package). */
|
|
81
142
|
passwordHash: string;
|
|
82
143
|
}
|