@gelabs/ovr 0.3.0 → 0.4.1

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 (43) hide show
  1. package/dist/{chunk-HGWPA7FU.js → chunk-6AXIWTMW.js} +36 -0
  2. package/dist/{chunk-IS3THKTE.js → chunk-QZRRFE6E.js} +13 -2
  3. package/dist/{chunk-ZUMEOZ22.js → chunk-VH4J2SIV.js} +2 -2
  4. package/dist/core-i18n.d.ts +2 -2
  5. package/dist/core-i18n.js +1 -1
  6. package/dist/data-mock-store.js +93 -7
  7. package/dist/data-prisma-store.js +87 -9
  8. package/dist/data.d.ts +16 -1
  9. package/dist/generated/client/edge.js +6 -4
  10. package/dist/generated/client/index-browser.js +3 -1
  11. package/dist/generated/client/index.d.ts +97 -39
  12. package/dist/generated/client/index.js +6 -4
  13. package/dist/generated/client/package.json +1 -1
  14. package/dist/generated/client/schema.prisma +2 -0
  15. package/dist/generated/client/wasm.js +6 -4
  16. package/dist/index.d.ts +1 -1
  17. package/dist/{types-BOgdk0Jw.d.ts → types-DNEO6wrO.d.ts} +36 -0
  18. package/dist/types.d.ts +14 -3
  19. package/dist/types.js +1 -1
  20. package/dist/ui-components-admin/accounts-manager.js +5 -5
  21. package/dist/ui-components-admin/admin-nav.js +6 -4
  22. package/dist/ui-components-admin/issuance-form.js +10 -10
  23. package/dist/ui-components-admin/logs-viewer.js +4 -4
  24. package/dist/ui-components-admin/notifications-list.js +1 -1
  25. package/dist/ui-components-admin/officers-manager.js +3 -3
  26. package/dist/ui-components-admin/roles-manager.js +4 -4
  27. package/dist/ui-components-admin/ticket-preview.js +6 -6
  28. package/dist/ui-components-admin/tickets-table.js +3 -3
  29. package/dist/ui-components-admin/violations-manager.d.ts +36 -0
  30. package/dist/ui-components-admin/violations-manager.js +454 -0
  31. package/dist/ui-components-citizen/payment-form.js +3 -3
  32. package/dist/ui-components-citizen/payment-qr-dialog.js +2 -2
  33. package/dist/ui-components-citizen/violation-history-table.js +2 -2
  34. package/dist/ui-components-shared/ticket-receipt.js +4 -4
  35. package/dist/ui-components-shared/violations-table.js +2 -2
  36. package/dist/ui-config.d.ts +1 -1
  37. package/dist/ui-server.d.ts +1 -1
  38. package/dist/ui-server.js +1 -1
  39. package/package.json +6 -6
  40. package/prisma/migrations/20260622050000_violation_catalog_management/migration.sql +5 -0
  41. package/prisma/schema.prisma +2 -0
  42. package/dist/{chunk-IBZVIUNI.js → chunk-6BH4EFP3.js} +1 -1
  43. package/dist/{chunk-TLG4C2XI.js → chunk-QCAURREW.js} +1 -1
@@ -98,6 +98,7 @@ var baseCopy = {
98
98
  accounts: "Accounts",
99
99
  roles: "Roles",
100
100
  officers: "Officers",
101
+ violations: "Violations",
101
102
  logs: "Activity log",
102
103
  more: "More",
103
104
  signOut: "Sign out",
@@ -195,6 +196,41 @@ var baseCopy = {
195
196
  empty: "No officers yet \u2014 add the first one.",
196
197
  actions: "Actions"
197
198
  },
199
+ violationsPage: {
200
+ title: "Violation catalog",
201
+ subtitle: "The ordinance / violation schedule enforcers issue against. Archived entries are hidden from the Issue-Ticket form but kept for old tickets.",
202
+ newViolation: "New violation",
203
+ code: "Code",
204
+ codePlaceholder: "e.g. ORD 2021-05 S.4",
205
+ titleLabel: "Title",
206
+ titlePlaceholder: "e.g. No helmet (driver)",
207
+ category: "Category",
208
+ categoryTraffic: "Traffic",
209
+ categoryOrdinance: "Ordinance",
210
+ fine: "Basic fine (\u20B1)",
211
+ finePlaceholder: "e.g. 500",
212
+ legalText: "Legal basis (optional)",
213
+ legalTextPlaceholder: "e.g. Sec. 4, Municipal Ordinance No. 2021-05",
214
+ status: "Status",
215
+ active: "Active",
216
+ archived: "Archived",
217
+ edit: "Edit",
218
+ editTitle: "Edit violation",
219
+ create: "Add violation",
220
+ creating: "Adding\u2026",
221
+ save: "Save",
222
+ saving: "Saving\u2026",
223
+ cancel: "Cancel",
224
+ archive: "Archive",
225
+ restore: "Restore",
226
+ archiveConfirmTitle: "Archive this violation?",
227
+ archiveConfirmBody: "It will be hidden from the Issue-Ticket form. Already-issued tickets keep their copy, and you can restore it anytime.",
228
+ delete: "Delete",
229
+ deleteConfirmTitle: "Delete this violation permanently?",
230
+ deleteConfirmBody: "This can't be undone. Only possible because no ticket has used it \u2014 used violations can only be archived.",
231
+ empty: "No violations yet \u2014 add the first one.",
232
+ actions: "Actions"
233
+ },
198
234
  notifications: {
199
235
  title: "Notifications",
200
236
  subtitle: "Tickets that need attention \u2014 overdue and outstanding.",
@@ -6,6 +6,7 @@ var PERMISSION_CATALOG = [
6
6
  { key: "accounts:manage", label: "Manage accounts", description: "Create and manage user accounts." },
7
7
  { key: "roles:manage", label: "Manage roles", description: "Create roles and set their permissions." },
8
8
  { key: "officers:manage", label: "Manage officers", description: "Add, edit, and remove apprehending officers." },
9
+ { key: "violations:manage", label: "Manage violations", description: "Add, edit, and archive the violation / ordinance catalog." },
9
10
  { key: "logs:view", label: "View activity logs", description: "Browse the audit trail of account actions." },
10
11
  { key: "notifications:view", label: "View notifications", description: "See the alert bell for overdue / pending tickets." }
11
12
  ];
@@ -22,6 +23,7 @@ var SYSTEM_ROLES = [
22
23
  "accounts:manage",
23
24
  "roles:manage",
24
25
  "officers:manage",
26
+ "violations:manage",
25
27
  "logs:view",
26
28
  "notifications:view"
27
29
  ]
@@ -35,6 +37,7 @@ var SYSTEM_ROLES = [
35
37
  "tickets:view",
36
38
  "tickets:create",
37
39
  "officers:manage",
40
+ "violations:manage",
38
41
  "logs:view",
39
42
  "notifications:view"
40
43
  ]
@@ -66,7 +69,11 @@ var ACTIVITY_ACTIONS = [
66
69
  "role.delete",
67
70
  "officer.create",
68
71
  "officer.update",
69
- "officer.delete"
72
+ "officer.delete",
73
+ "violation.create",
74
+ "violation.update",
75
+ "violation.delete",
76
+ "violation.purge"
70
77
  ];
71
78
  var ACTIVITY_ACTION_LABELS = {
72
79
  "auth.login": "Signed in",
@@ -83,7 +90,11 @@ var ACTIVITY_ACTION_LABELS = {
83
90
  "role.delete": "Deleted role",
84
91
  "officer.create": "Added officer",
85
92
  "officer.update": "Updated officer",
86
- "officer.delete": "Removed officer"
93
+ "officer.delete": "Removed officer",
94
+ "violation.create": "Added violation",
95
+ "violation.update": "Updated violation",
96
+ "violation.delete": "Archived violation",
97
+ "violation.purge": "Deleted violation"
87
98
  };
88
99
 
89
100
  export { ACTIVITY_ACTIONS, ACTIVITY_ACTION_LABELS, ALL_PERMISSIONS, DEFAULT_ROLE_NAME, PERMISSION_CATALOG, SUPER_ADMIN_LOCKED_PERMISSIONS, SYSTEM_ROLES, hasPermission };
@@ -1,6 +1,6 @@
1
- import { ViolationsTable } from './chunk-IBZVIUNI.js';
2
- import { AmountSummary } from './chunk-GLIK5BHP.js';
1
+ import { ViolationsTable } from './chunk-6BH4EFP3.js';
3
2
  import { StatusBadge } from './chunk-OE525ZER.js';
3
+ import { AmountSummary } from './chunk-GLIK5BHP.js';
4
4
  import { Card, CardHeader, CardTitle, CardContent } from './chunk-SETIN6XP.js';
5
5
  import { useFormatters } from './chunk-TJSNVTVB.js';
6
6
  import { formalName, formatAddress } from './chunk-BI4EGLPG.js';
@@ -1,5 +1,5 @@
1
- import { C as CopyOverrides, D as Dictionary } from './types-BOgdk0Jw.js';
2
- export { b as baseCopy } from './types-BOgdk0Jw.js';
1
+ import { C as CopyOverrides, D as Dictionary } from './types-DNEO6wrO.js';
2
+ export { b as baseCopy } from './types-DNEO6wrO.js';
3
3
  import { M as MunicipalityConfig } from './schema-CdsFQxIg.js';
4
4
  import 'zod';
5
5
 
package/dist/core-i18n.js CHANGED
@@ -1 +1 @@
1
- export { baseCopy, mergeCopy } from './chunk-HGWPA7FU.js';
1
+ export { baseCopy, mergeCopy } from './chunk-6AXIWTMW.js';
@@ -1,4 +1,4 @@
1
- import { SUPER_ADMIN_LOCKED_PERMISSIONS, ALL_PERMISSIONS } from './chunk-IS3THKTE.js';
1
+ import { SUPER_ADMIN_LOCKED_PERMISSIONS, ALL_PERMISSIONS } from './chunk-QZRRFE6E.js';
2
2
  import { round2, computeCharges, makePaymentRef, fullName, addDays, makeBillNo, makeOrderOfPaymentNo, makeOvrTicketNo, enrich } from './chunk-BI4EGLPG.js';
3
3
  import 'server-only';
4
4
  import { promises } from 'fs';
@@ -6,7 +6,7 @@ import { randomUUID } from 'crypto';
6
6
  import os from 'os';
7
7
  import path from 'path';
8
8
 
9
- var SEED_VERSION = 5;
9
+ var SEED_VERSION = 6;
10
10
  var norm = (s) => s.trim().toLowerCase();
11
11
  function slugRole(label) {
12
12
  return label.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -27,7 +27,11 @@ function createMockStore(rules, seed) {
27
27
  users: structuredClone(USERS),
28
28
  roles: structuredClone(ROLES),
29
29
  activities: [],
30
- officers: structuredClone(OFFICERS)
30
+ officers: structuredClone(OFFICERS),
31
+ catalog: structuredClone(CATALOG).map((c) => ({
32
+ ...c,
33
+ active: c.active ?? true
34
+ }))
31
35
  };
32
36
  }
33
37
  async function writeStore(data) {
@@ -37,7 +41,7 @@ function createMockStore(rules, seed) {
37
41
  try {
38
42
  const raw = await promises.readFile(STORE_PATH, "utf8");
39
43
  const data = JSON.parse(raw);
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") {
44
+ 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) || !Array.isArray(data.catalog) || typeof data.counter !== "number") {
41
45
  const fresh = seeded();
42
46
  await writeStore(fresh);
43
47
  return fresh;
@@ -51,9 +55,91 @@ function createMockStore(rules, seed) {
51
55
  }
52
56
  const store = {
53
57
  async listViolationCatalog(category) {
54
- const items = category ? CATALOG.filter((c) => c.category === category) : CATALOG;
58
+ const data = await readStore();
59
+ const items = data.catalog.filter(
60
+ (c) => c.active !== false && (!category || c.category === category)
61
+ );
55
62
  return structuredClone(items);
56
63
  },
64
+ // ── Violation catalog management (GE-031) ──────────────────────────────
65
+ async listAllViolations() {
66
+ const data = await readStore();
67
+ return structuredClone(data.catalog).sort(
68
+ (a, b) => a.category === b.category ? a.code.localeCompare(b.code) : a.category.localeCompare(b.category)
69
+ );
70
+ },
71
+ async createViolation(input) {
72
+ const data = await readStore();
73
+ const code = input.code.trim();
74
+ if (!code) throw new Error("Violation code is required.");
75
+ const title = input.title.trim();
76
+ if (!title) throw new Error("Title is required.");
77
+ if (!(input.basicFine >= 0)) throw new Error("Basic fine must be \u2265 0.");
78
+ if (data.catalog.some((c) => norm(c.code) === norm(code))) {
79
+ throw new Error(`A violation with code "${code}" already exists.`);
80
+ }
81
+ const item = {
82
+ code,
83
+ title,
84
+ category: input.category,
85
+ basicFine: round2(input.basicFine),
86
+ legalText: input.legalText?.trim() || void 0,
87
+ active: true
88
+ };
89
+ data.catalog.push(item);
90
+ await writeStore(data);
91
+ return structuredClone(item);
92
+ },
93
+ async updateViolation(code, patch) {
94
+ const data = await readStore();
95
+ const c = data.catalog.find((x) => x.code === code);
96
+ if (!c) throw new Error("Violation not found.");
97
+ if (patch.title !== void 0) {
98
+ const title = patch.title.trim();
99
+ if (!title) throw new Error("Title is required.");
100
+ c.title = title;
101
+ }
102
+ if (patch.category !== void 0) c.category = patch.category;
103
+ if (patch.basicFine !== void 0) {
104
+ if (!(patch.basicFine >= 0)) throw new Error("Basic fine must be \u2265 0.");
105
+ c.basicFine = round2(patch.basicFine);
106
+ }
107
+ if (patch.legalText !== void 0) {
108
+ c.legalText = patch.legalText?.trim() || void 0;
109
+ }
110
+ if (patch.active !== void 0) c.active = patch.active;
111
+ await writeStore(data);
112
+ return structuredClone(c);
113
+ },
114
+ async deleteViolation(code) {
115
+ const data = await readStore();
116
+ const c = data.catalog.find((x) => x.code === code);
117
+ if (!c) throw new Error("Violation not found.");
118
+ c.active = false;
119
+ await writeStore(data);
120
+ },
121
+ async usedViolationCodes() {
122
+ const data = await readStore();
123
+ const used = /* @__PURE__ */ new Set();
124
+ for (const t of data.tickets)
125
+ for (const v of t.violations) used.add(v.catalogCode);
126
+ return [...used];
127
+ },
128
+ async purgeViolation(code) {
129
+ const data = await readStore();
130
+ const c = data.catalog.find((x) => x.code === code);
131
+ if (!c) throw new Error("Violation not found.");
132
+ const uses = data.tickets.filter(
133
+ (t) => t.violations.some((v) => v.catalogCode === code)
134
+ ).length;
135
+ if (uses > 0) {
136
+ throw new Error(
137
+ `Can't delete \u2014 used by ${uses} ticket(s). Archive it instead.`
138
+ );
139
+ }
140
+ data.catalog = data.catalog.filter((x) => x.code !== code);
141
+ await writeStore(data);
142
+ },
57
143
  async listOfficers() {
58
144
  const data = await readStore();
59
145
  return structuredClone(data.officers);
@@ -125,7 +211,7 @@ function createMockStore(rules, seed) {
125
211
  const seq = store2.counter;
126
212
  const officer = store2.officers.find((o) => o.id === input.officerId) ?? store2.officers[0];
127
213
  const violations = input.violations.map((v) => {
128
- const c = CATALOG.find((x) => x.code === v.catalogCode);
214
+ const c = store2.catalog.find((x) => x.code === v.catalogCode);
129
215
  if (!c) throw new Error(`Unknown violation code: ${v.catalogCode}`);
130
216
  return {
131
217
  catalogCode: c.code,
@@ -186,7 +272,7 @@ function createMockStore(rules, seed) {
186
272
  const created = new Date(input.createdAt);
187
273
  const officer = data.officers.find((o) => o.id === input.officerId) ?? data.officers[0];
188
274
  const violations = input.violations.map((v) => {
189
- const c = CATALOG.find((x) => x.code === v.catalogCode);
275
+ const c = data.catalog.find((x) => x.code === v.catalogCode);
190
276
  if (!c) throw new Error(`Unknown violation code: ${v.catalogCode}`);
191
277
  return {
192
278
  catalogCode: c.code,
@@ -1,5 +1,5 @@
1
1
  import { prisma } from './chunk-MKALJTAU.js';
2
- import { SUPER_ADMIN_LOCKED_PERMISSIONS, ALL_PERMISSIONS } from './chunk-IS3THKTE.js';
2
+ import { SUPER_ADMIN_LOCKED_PERMISSIONS, ALL_PERMISSIONS } from './chunk-QZRRFE6E.js';
3
3
  import { round2, computeCharges, makePaymentRef, fullName, addDays, makeBillNo, makeOrderOfPaymentNo, makeOvrTicketNo, enrich } from './chunk-BI4EGLPG.js';
4
4
  import 'server-only';
5
5
 
@@ -25,6 +25,16 @@ function toOfficer(o) {
25
25
  office: o.office
26
26
  };
27
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
+ }
28
38
  function toUserAccount(u) {
29
39
  return {
30
40
  id: u.id,
@@ -95,16 +105,84 @@ function createPrismaStore(rules) {
95
105
  const store = {
96
106
  async listViolationCatalog(category) {
97
107
  const items = await prisma.violationCatalog.findMany({
98
- where: category ? { category } : void 0,
108
+ where: { active: true, ...category ? { category } : {} },
99
109
  orderBy: [{ category: "asc" }, { code: "asc" }]
100
110
  });
101
- return items.map((c) => ({
102
- code: c.code,
103
- title: c.title,
104
- category: c.category,
105
- basicFine: c.basicFine.toNumber(),
106
- legalText: c.legalText ?? void 0
107
- }));
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
+ });
168
+ },
169
+ async usedViolationCodes() {
170
+ const rows = await prisma.ticketViolation.findMany({
171
+ distinct: ["catalogCode"],
172
+ select: { catalogCode: true }
173
+ });
174
+ return rows.map((r) => r.catalogCode);
175
+ },
176
+ async purgeViolation(code) {
177
+ const uses = await prisma.ticketViolation.count({
178
+ where: { catalogCode: code }
179
+ });
180
+ if (uses > 0) {
181
+ throw new Error(
182
+ `Can't delete \u2014 used by ${uses} ticket(s). Archive it instead.`
183
+ );
184
+ }
185
+ await prisma.violationCatalog.delete({ where: { code } });
108
186
  },
109
187
  async listOfficers() {
110
188
  const officers = await prisma.officer.findMany({
package/dist/data.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ViolationCatalogItem, Officer, TicketRecord, UserAccount, RoleDef, ViolationCategory, NewOfficerInput, Violator, Ticket, TicketStatus, PaymentMethod, NewUserInput, Permission, NewRoleInput, NewActivityInput, ActivityFilter, ActivityLog } 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
@@ -44,7 +44,22 @@ interface TicketStats {
44
44
  issuedToday: number;
45
45
  }
46
46
  interface DataStore {
47
+ /** ACTIVE catalog entries only (what enforcers may issue against). */
47
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>;
59
+ /** Distinct catalog codes referenced by at least one issued ticket. */
60
+ usedViolationCodes(): Promise<string[]>;
61
+ /** Permanently remove an entry. Throws if any ticket has used it (archive instead). */
62
+ purgeViolation(code: string): Promise<void>;
48
63
  listOfficers(): Promise<Officer[]>;
49
64
  getOfficer(id: string): Promise<Officer | null>;
50
65
  /** Add an apprehending officer (id is slugified from the name; must be unique). */