@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.
- package/dist/{chunk-HGWPA7FU.js → chunk-6AXIWTMW.js} +36 -0
- package/dist/{chunk-IS3THKTE.js → chunk-QZRRFE6E.js} +13 -2
- package/dist/{chunk-ZUMEOZ22.js → chunk-VH4J2SIV.js} +2 -2
- package/dist/core-i18n.d.ts +2 -2
- package/dist/core-i18n.js +1 -1
- package/dist/data-mock-store.js +93 -7
- package/dist/data-prisma-store.js +87 -9
- package/dist/data.d.ts +16 -1
- package/dist/generated/client/edge.js +6 -4
- package/dist/generated/client/index-browser.js +3 -1
- package/dist/generated/client/index.d.ts +97 -39
- package/dist/generated/client/index.js +6 -4
- package/dist/generated/client/package.json +1 -1
- package/dist/generated/client/schema.prisma +2 -0
- package/dist/generated/client/wasm.js +6 -4
- package/dist/index.d.ts +1 -1
- package/dist/{types-BOgdk0Jw.d.ts → types-DNEO6wrO.d.ts} +36 -0
- package/dist/types.d.ts +14 -3
- package/dist/types.js +1 -1
- package/dist/ui-components-admin/accounts-manager.js +5 -5
- package/dist/ui-components-admin/admin-nav.js +6 -4
- package/dist/ui-components-admin/issuance-form.js +10 -10
- package/dist/ui-components-admin/logs-viewer.js +4 -4
- package/dist/ui-components-admin/notifications-list.js +1 -1
- package/dist/ui-components-admin/officers-manager.js +3 -3
- package/dist/ui-components-admin/roles-manager.js +4 -4
- package/dist/ui-components-admin/ticket-preview.js +6 -6
- package/dist/ui-components-admin/tickets-table.js +3 -3
- package/dist/ui-components-admin/violations-manager.d.ts +36 -0
- package/dist/ui-components-admin/violations-manager.js +454 -0
- package/dist/ui-components-citizen/payment-form.js +3 -3
- package/dist/ui-components-citizen/payment-qr-dialog.js +2 -2
- package/dist/ui-components-citizen/violation-history-table.js +2 -2
- package/dist/ui-components-shared/ticket-receipt.js +4 -4
- package/dist/ui-components-shared/violations-table.js +2 -2
- package/dist/ui-config.d.ts +1 -1
- package/dist/ui-server.d.ts +1 -1
- package/dist/ui-server.js +1 -1
- package/package.json +6 -6
- package/prisma/migrations/20260622050000_violation_catalog_management/migration.sql +5 -0
- package/prisma/schema.prisma +2 -0
- package/dist/{chunk-IBZVIUNI.js → chunk-6BH4EFP3.js} +1 -1
- 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-
|
|
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';
|
package/dist/core-i18n.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { C as CopyOverrides, D as Dictionary } from './types-
|
|
2
|
-
export { b as baseCopy } from './types-
|
|
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-
|
|
1
|
+
export { baseCopy, mergeCopy } from './chunk-6AXIWTMW.js';
|
package/dist/data-mock-store.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SUPER_ADMIN_LOCKED_PERMISSIONS, ALL_PERMISSIONS } from './chunk-
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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-
|
|
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 } :
|
|
108
|
+
where: { active: true, ...category ? { category } : {} },
|
|
99
109
|
orderBy: [{ category: "asc" }, { code: "asc" }]
|
|
100
110
|
});
|
|
101
|
-
return items.map(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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). */
|