@gelabs/ovr 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/auth-auth.js +1 -1
  2. package/dist/auth.js +1 -1
  3. package/dist/{chunk-MDTRBOPQ.js → chunk-2C3VCTYJ.js} +1 -1
  4. package/dist/chunk-3YKVH4Y7.js +126 -0
  5. package/dist/chunk-6YFZLXFP.js +84 -0
  6. package/dist/{chunk-3NZ2XUBO.js → chunk-AJ2RZTVX.js} +9 -2
  7. package/dist/chunk-BI4EGLPG.js +298 -0
  8. package/dist/{chunk-3KIDW4LT.js → chunk-BVI5XDDA.js} +1 -1
  9. package/dist/chunk-DJMUW5T2.js +298 -0
  10. package/dist/{chunk-BIQ2J75Y.js → chunk-GLIK5BHP.js} +2 -2
  11. package/dist/{chunk-JEYT63LE.js → chunk-IBZVIUNI.js} +1 -1
  12. package/dist/{chunk-4SZXBT56.js → chunk-NT72CQAI.js} +2 -2
  13. package/dist/{chunk-E2D7QT6N.js → chunk-TJSNVTVB.js} +1 -1
  14. package/dist/{chunk-5Z2IAD5I.js → chunk-TLG4C2XI.js} +2 -2
  15. package/dist/chunk-V7VQVDWS.js +237 -0
  16. package/dist/chunk-WUNTHINH.js +98 -0
  17. package/dist/{chunk-IF5UAVIE.js → chunk-YC7G2IOZ.js} +1 -1
  18. package/dist/{chunk-IB4JVGKJ.js → chunk-YGYA7KEG.js} +47 -3
  19. package/dist/{chunk-GDOCD7LT.js → chunk-ZUMEOZ22.js} +5 -5
  20. package/dist/core-i18n.d.ts +2 -2
  21. package/dist/core-i18n.js +1 -1
  22. package/dist/core.d.ts +61 -1
  23. package/dist/core.js +1 -1
  24. package/dist/data-mock-store.js +330 -12
  25. package/dist/data-prisma-store.js +319 -9
  26. package/dist/data-seed-runner.js +18 -15
  27. package/dist/data.d.ts +64 -3
  28. package/dist/generated/client/edge.js +31 -10
  29. package/dist/generated/client/index-browser.js +28 -7
  30. package/dist/generated/client/index.d.ts +3583 -577
  31. package/dist/generated/client/index.js +31 -10
  32. package/dist/generated/client/package.json +1 -1
  33. package/dist/generated/client/schema.prisma +48 -9
  34. package/dist/generated/client/wasm.js +31 -10
  35. package/dist/index.d.ts +2 -2
  36. package/dist/index.js +1 -1
  37. package/dist/offline.d.ts +34 -1
  38. package/dist/offline.js +2 -2
  39. package/dist/types-B8MopM4b.d.ts +281 -0
  40. package/dist/types.d.ts +104 -1
  41. package/dist/types.js +1 -1
  42. package/dist/ui-components-admin/accounts-manager.d.ts +52 -0
  43. package/dist/ui-components-admin/accounts-manager.js +471 -0
  44. package/dist/ui-components-admin/admin-nav.d.ts +15 -1
  45. package/dist/ui-components-admin/admin-nav.js +388 -60
  46. package/dist/ui-components-admin/issuance-form.js +72 -13
  47. package/dist/ui-components-admin/logs-viewer.d.ts +13 -0
  48. package/dist/ui-components-admin/logs-viewer.js +102 -0
  49. package/dist/ui-components-admin/notifications-list.d.ts +5 -0
  50. package/dist/ui-components-admin/notifications-list.js +70 -0
  51. package/dist/ui-components-admin/officers-manager.d.ts +27 -0
  52. package/dist/ui-components-admin/officers-manager.js +271 -0
  53. package/dist/ui-components-admin/roles-manager.d.ts +37 -0
  54. package/dist/ui-components-admin/roles-manager.js +406 -0
  55. package/dist/ui-components-admin/ticket-preview.js +7 -7
  56. package/dist/ui-components-admin/tickets-table.js +56 -33
  57. package/dist/ui-components-admin/violations-manager.d.ts +32 -0
  58. package/dist/ui-components-admin/violations-manager.js +385 -0
  59. package/dist/ui-components-citizen/citizen-nav.js +2 -2
  60. package/dist/ui-components-citizen/payment-form.js +5 -5
  61. package/dist/ui-components-citizen/payment-qr-dialog.js +4 -4
  62. package/dist/ui-components-citizen/ticket-not-found.js +2 -2
  63. package/dist/ui-components-citizen/violation-history-table.js +3 -3
  64. package/dist/ui-components-shared/amount-summary.js +4 -4
  65. package/dist/ui-components-shared/money.js +3 -3
  66. package/dist/ui-components-shared/municipal-seal.js +3 -3
  67. package/dist/ui-components-shared/official-header.js +4 -4
  68. package/dist/ui-components-shared/site-header.js +4 -4
  69. package/dist/ui-components-shared/sonner.js +2 -2
  70. package/dist/ui-components-shared/theme-toggle.js +3 -3
  71. package/dist/ui-components-shared/ticket-receipt.js +13 -6
  72. package/dist/ui-components-shared/violations-table.js +4 -4
  73. package/dist/ui-components-ui/badge.d.ts +1 -1
  74. package/dist/ui-components-ui/button.d.ts +1 -1
  75. package/dist/ui-components-ui/dropdown-menu.js +2 -237
  76. package/dist/ui-components-ui/sheet.js +3 -126
  77. package/dist/ui-config.d.ts +1 -1
  78. package/dist/ui-config.js +2 -2
  79. package/dist/ui-server.d.ts +1 -1
  80. package/dist/ui-server.js +2 -2
  81. package/package.json +6 -6
  82. package/prisma/migrations/20260622010000_add_super_admin_role/migration.sql +3 -0
  83. package/prisma/migrations/20260622020000_add_apprehending_enforcer/migration.sql +4 -0
  84. package/prisma/migrations/20260622030000_custom_roles/migration.sql +30 -0
  85. package/prisma/migrations/20260622040000_add_activity_log/migration.sql +18 -0
  86. package/prisma/migrations/20260622050000_violation_catalog_management/migration.sql +5 -0
  87. package/prisma/schema.prisma +48 -9
  88. package/dist/chunk-5YYR37CF.js +0 -146
  89. package/dist/chunk-B634JHKZ.js +0 -181
  90. package/dist/types-CtBC5-TW.d.ts +0 -129
@@ -1,13 +1,22 @@
1
1
  import { prisma } from './chunk-MKALJTAU.js';
2
- import { round2, computeCharges, makePaymentRef, fullName, addDays, makeBillNo, makeOrderOfPaymentNo, makeOvrTicketNo, enrich } from './chunk-B634JHKZ.js';
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 } : void 0,
108
+ where: { active: true, ...category ? { category } : {} },
68
109
  orderBy: [{ category: "asc" }, { code: "asc" }]
69
110
  });
70
- return items.map((c) => ({
71
- code: c.code,
72
- title: c.title,
73
- category: c.category,
74
- basicFine: c.basicFine.toNumber(),
75
- legalText: c.legalText ?? void 0
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;
@@ -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
- passwordHash,
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 enforcer login (links a username to an officer). */
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: string;
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
  }