@gelabs/ovr 0.2.1 → 0.2.2

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/offline.d.ts CHANGED
@@ -76,24 +76,6 @@ declare function sync(): Promise<void>;
76
76
  /** Issue a ticket from the device. Returns the enriched ticket (shown inline). */
77
77
  declare function issueTicketOffline(input: NewTicketInput, rules: RulesConfig): Promise<Ticket>;
78
78
 
79
- /** All locally-stored tickets, newest first. `undefined` while loading. */
80
- declare function useTickets(): Ticket[] | undefined;
81
- declare function useCatalog(): ViolationCatalogItem[] | undefined;
82
- declare function useOfficers(): Officer[] | undefined;
83
- /** Cached dashboard stats (as-of last sync). `undefined` until first sync. */
84
- declare function useStats(): TicketStats | undefined;
85
- /** A single ticket by number: `undefined` while loading, `null` if not stored. */
86
- declare function useTicket(ovrTicketNo: string): Ticket | undefined | null;
87
- /** ovrTicketNos still waiting to sync (present in the outbox). Reactive. */
88
- declare function usePendingSync(): Set<string>;
89
- interface SyncState {
90
- syncing: boolean;
91
- online: boolean;
92
- error: string | null;
93
- }
94
- /** Pull from the API on mount (if online) and whenever connectivity returns. */
95
- declare function useSync(): SyncState;
96
-
97
79
  interface AdminIdentity {
98
80
  userId: string;
99
81
  username: string;
@@ -123,4 +105,25 @@ declare function cacheCredential(username: string, password: string, identity: A
123
105
  * sets the active identity and returns it; otherwise null. */
124
106
  declare function verifyOffline(username: string, password: string): Promise<AdminIdentity | null>;
125
107
 
126
- export { type AdminIdentity, type AuthState, type MetaRow, type OutboxRow, type PushResult, SessionExpired, type SyncState, cacheCredential, clearIdentity, db, ensureLease, getIdentity, getMeta, isOnline, issueTicketOffline, offlineApiBase, pullAll, pushOutbox, setMeta, setOfflineApiBase, sync, useAdminAuth, useCatalog, useOfficers, usePendingSync, useStats, useSync, useTicket, useTickets, verifyOffline };
108
+ /** All locally-stored tickets, newest first. `undefined` while loading. */
109
+ declare function useTickets(): Ticket[] | undefined;
110
+ declare function useCatalog(): ViolationCatalogItem[] | undefined;
111
+ declare function useOfficers(): Officer[] | undefined;
112
+ /** The signed-in admin identity (cached locally), reactive. `undefined` while
113
+ * loading, `null` if signed out. Used to tailor the admin UI to the role. */
114
+ declare function useIdentity(): AdminIdentity | null | undefined;
115
+ /** Cached dashboard stats (as-of last sync). `undefined` until first sync. */
116
+ declare function useStats(): TicketStats | undefined;
117
+ /** A single ticket by number: `undefined` while loading, `null` if not stored. */
118
+ declare function useTicket(ovrTicketNo: string): Ticket | undefined | null;
119
+ /** ovrTicketNos still waiting to sync (present in the outbox). Reactive. */
120
+ declare function usePendingSync(): Set<string>;
121
+ interface SyncState {
122
+ syncing: boolean;
123
+ online: boolean;
124
+ error: string | null;
125
+ }
126
+ /** Pull from the API on mount (if online) and whenever connectivity returns. */
127
+ declare function useSync(): SyncState;
128
+
129
+ export { type AdminIdentity, type AuthState, type MetaRow, type OutboxRow, type PushResult, SessionExpired, type SyncState, cacheCredential, clearIdentity, db, ensureLease, getIdentity, getMeta, isOnline, issueTicketOffline, offlineApiBase, pullAll, pushOutbox, setMeta, setOfflineApiBase, sync, useAdminAuth, useCatalog, useIdentity, useOfficers, usePendingSync, useStats, useSync, useTicket, useTickets, verifyOffline };
package/dist/offline.js CHANGED
@@ -1,376 +1,3 @@
1
1
  "use client";
2
- import { makeOvrTicketNo, addDays, makeBillNo, makeOrderOfPaymentNo, enrich } from './chunk-B634JHKZ.js';
3
- import Dexie from 'dexie';
4
- import { useLiveQuery } from 'dexie-react-hooks';
5
- import { useState, useEffect } from 'react';
6
-
7
- var db = new Dexie("eovr-offline");
8
- db.version(1).stores({
9
- tickets: "ovrTicketNo, paymentStatus, createdAt",
10
- catalog: "code, category",
11
- officers: "id",
12
- meta: "key"
13
- });
14
- db.version(2).stores({
15
- outbox: "ovrTicketNo, status, createdAt"
16
- });
17
- async function getMeta(key) {
18
- const row = await db.meta.get(key);
19
- return row?.value;
20
- }
21
- async function setMeta(key, value) {
22
- await db.meta.put({ key, value });
23
- }
24
-
25
- // ../ovr-offline/src/sync.ts
26
- var API = "/api";
27
- var LEASE_MIN = 10;
28
- var LEASE_BATCH = 50;
29
- function setOfflineApiBase(base) {
30
- API = base.replace(/\/$/, "");
31
- }
32
- function offlineApiBase() {
33
- return API;
34
- }
35
- var SessionExpired = class extends Error {
36
- constructor() {
37
- super("session_expired");
38
- this.name = "SessionExpired";
39
- }
40
- };
41
- function isOnline() {
42
- return typeof navigator !== "undefined" ? navigator.onLine : true;
43
- }
44
- async function call(path, init) {
45
- let res;
46
- try {
47
- res = await fetch(`${API}${path}`, { credentials: "include", ...init });
48
- } catch {
49
- return null;
50
- }
51
- if (res.status === 401) throw new SessionExpired();
52
- return res;
53
- }
54
- var postInit = (body) => ({
55
- method: "POST",
56
- headers: { "content-type": "application/json" },
57
- body: JSON.stringify(body)
58
- });
59
- async function ensureLease(min = LEASE_MIN) {
60
- if (!isOnline()) return;
61
- const seqs = await getMeta("leaseSeqs") ?? [];
62
- if (seqs.length >= min) return;
63
- const res = await call("/sync/lease", postInit({ count: LEASE_BATCH }));
64
- if (!res || !res.ok) return;
65
- const { seqs: fresh } = await res.json();
66
- await setMeta("leaseSeqs", [...seqs, ...fresh]);
67
- }
68
- async function pushOutbox() {
69
- const pending = await db.outbox.toArray();
70
- if (!pending.length) return;
71
- const res = await call(
72
- "/sync/push",
73
- postInit({ tickets: pending.map((p) => p.payload) })
74
- );
75
- if (!res || !res.ok) return;
76
- const { results } = await res.json();
77
- await db.transaction("rw", db.outbox, db.tickets, async () => {
78
- for (const r of results) {
79
- if (r.ok) {
80
- await db.outbox.delete(r.ovrTicketNo);
81
- if (r.ticket) await db.tickets.put(r.ticket);
82
- } else {
83
- await db.outbox.update(r.ovrTicketNo, { status: "error", error: r.error });
84
- }
85
- }
86
- });
87
- }
88
- async function pullAll() {
89
- const [t, c, o, s] = await Promise.all([
90
- call("/tickets"),
91
- call("/violations"),
92
- call("/officers"),
93
- call("/tickets/stats")
94
- ]);
95
- if (!t || !c || !o || !s) return;
96
- if (!t.ok || !c.ok || !o.ok || !s.ok) {
97
- throw new Error(
98
- `sync.pullAll failed: tickets=${t.status} catalog=${c.status} officers=${o.status} stats=${s.status}`
99
- );
100
- }
101
- const tickets = await t.json();
102
- const catalog = await c.json();
103
- const officers = await o.json();
104
- const stats = await s.json();
105
- await db.transaction(
106
- "rw",
107
- db.tickets,
108
- db.catalog,
109
- db.officers,
110
- db.meta,
111
- async () => {
112
- await db.tickets.bulkPut(tickets);
113
- await db.catalog.clear();
114
- await db.catalog.bulkPut(catalog);
115
- await db.officers.clear();
116
- await db.officers.bulkPut(officers);
117
- await db.meta.put({ key: "stats", value: stats });
118
- }
119
- );
120
- await setMeta("lastSyncedAt", (/* @__PURE__ */ new Date()).toISOString());
121
- }
122
- async function sync() {
123
- if (!isOnline()) return;
124
- await pushOutbox();
125
- await ensureLease();
126
- await pullAll();
127
- }
128
-
129
- // ../ovr-offline/src/issue.ts
130
- async function popSeq() {
131
- return db.transaction("rw", db.meta, async () => {
132
- const row = await db.meta.get("leaseSeqs");
133
- const seqs = row?.value ?? [];
134
- if (seqs.length === 0) return null;
135
- await db.meta.put({ key: "leaseSeqs", value: seqs.slice(1) });
136
- return seqs[0];
137
- });
138
- }
139
- async function issueTicketOffline(input, rules) {
140
- let seq = await popSeq();
141
- if (seq === null && isOnline()) {
142
- await ensureLease(50);
143
- seq = await popSeq();
144
- }
145
- if (seq === null) {
146
- throw new Error(
147
- isOnline() ? "Couldn't reserve ticket numbers \u2014 please try again." : "No offline ticket numbers left. Reconnect to the internet to reserve more."
148
- );
149
- }
150
- const now = /* @__PURE__ */ new Date();
151
- const ovrTicketNo = makeOvrTicketNo(rules.idPrefix, now.getFullYear(), seq);
152
- const officer = await db.officers.get(input.officerId);
153
- if (!officer) {
154
- const cur = await getMeta("leaseSeqs") ?? [];
155
- await setMeta("leaseSeqs", [seq, ...cur]);
156
- throw new Error("That officer isn't available offline. Sync and try again.");
157
- }
158
- const catalog = await db.catalog.toArray();
159
- const byCode = new Map(catalog.map((c) => [c.code, c]));
160
- const violations = input.violations.map((x) => {
161
- const c = byCode.get(x.catalogCode);
162
- if (!c) throw new Error(`Unknown violation: ${x.catalogCode}`);
163
- const v = {
164
- catalogCode: c.code,
165
- title: c.title,
166
- basicFine: c.basicFine
167
- };
168
- if (x.details?.trim()) v.details = x.details.trim();
169
- return v;
170
- });
171
- const basicFinesTotal = violations.reduce((s, v) => s + v.basicFine, 0);
172
- const record = {
173
- ovrTicketNo,
174
- orderOfPaymentNo: makeOrderOfPaymentNo(ovrTicketNo),
175
- billNo: makeBillNo(now, officer.office, officer.badgeNo ?? "X000", seq),
176
- violator: input.violator,
177
- apprehendedAt: input.apprehendedAt,
178
- officer,
179
- violations,
180
- assessedAt: now.toISOString(),
181
- dueDate: addDays(now, rules.dueWindowDays).toISOString(),
182
- basicFinesTotal,
183
- paymentStatus: "UNPAID",
184
- createdAt: now.toISOString()
185
- };
186
- if (input.placeOfViolation?.trim())
187
- record.placeOfViolation = input.placeOfViolation.trim();
188
- if (input.remarks?.trim()) record.remarks = input.remarks.trim();
189
- const ticket = enrich(record, now, rules.surchargeRatePerMonth);
190
- await db.transaction("rw", db.tickets, db.outbox, async () => {
191
- await db.tickets.put(ticket);
192
- await db.outbox.put({
193
- ovrTicketNo,
194
- createdAt: record.createdAt,
195
- status: "pending",
196
- payload: {
197
- ovrTicketNo,
198
- createdAt: record.createdAt,
199
- violator: input.violator,
200
- apprehendedAt: input.apprehendedAt,
201
- ...record.placeOfViolation ? { placeOfViolation: record.placeOfViolation } : {},
202
- officerId: input.officerId,
203
- violations: input.violations,
204
- ...record.remarks ? { remarks: record.remarks } : {}
205
- }
206
- });
207
- });
208
- if (isOnline()) void pushOutbox().catch(() => {
209
- });
210
- return ticket;
211
- }
212
- async function getIdentity() {
213
- return await getMeta("identity") ?? void 0;
214
- }
215
- async function clearIdentity() {
216
- await setMeta("identity", null);
217
- }
218
- function useAdminAuth() {
219
- const [state, setState] = useState({ status: "loading" });
220
- useEffect(() => {
221
- let cancelled = false;
222
- (async () => {
223
- const cached = await getMeta("identity");
224
- if (isOnline()) {
225
- try {
226
- const res = await fetch(`${offlineApiBase()}/auth/me`, {
227
- credentials: "include"
228
- });
229
- if (res.ok) {
230
- const { user } = await res.json();
231
- await setMeta("identity", user);
232
- if (!cancelled) setState({ status: "authed", user });
233
- return;
234
- }
235
- await setMeta("identity", null);
236
- if (!cancelled) setState({ status: "unauthed" });
237
- return;
238
- } catch {
239
- }
240
- }
241
- if (!cancelled) {
242
- setState(
243
- cached ? { status: "authed", user: cached } : { status: "unauthed" }
244
- );
245
- }
246
- })();
247
- return () => {
248
- cancelled = true;
249
- };
250
- }, []);
251
- return state;
252
- }
253
- var PBKDF2_ITERATIONS = 21e4;
254
- function subtle() {
255
- return typeof crypto !== "undefined" && crypto.subtle ? crypto.subtle : null;
256
- }
257
- var toHex = (buf) => Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, "0")).join(
258
- ""
259
- );
260
- function fromHex(hex) {
261
- const out = new Uint8Array(hex.length / 2);
262
- for (let i = 0; i < out.length; i++)
263
- out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
264
- return out;
265
- }
266
- async function deriveHashHex(password, salt, iterations) {
267
- const s = subtle();
268
- if (!s) return null;
269
- const keyMaterial = await s.importKey(
270
- "raw",
271
- new TextEncoder().encode(password),
272
- "PBKDF2",
273
- false,
274
- ["deriveBits"]
275
- );
276
- const bits = await s.deriveBits(
277
- { name: "PBKDF2", salt, iterations, hash: "SHA-256" },
278
- keyMaterial,
279
- 256
280
- );
281
- return toHex(bits);
282
- }
283
- async function cacheCredential(username, password, identity) {
284
- if (!subtle()) return;
285
- const salt = crypto.getRandomValues(new Uint8Array(16));
286
- const hashHex = await deriveHashHex(password, salt, PBKDF2_ITERATIONS);
287
- if (!hashHex) return;
288
- const cred = {
289
- username: username.trim(),
290
- saltHex: toHex(salt.buffer),
291
- hashHex,
292
- iterations: PBKDF2_ITERATIONS,
293
- identity
294
- };
295
- await setMeta("credential", cred);
296
- }
297
- async function verifyOffline(username, password) {
298
- const cred = await getMeta("credential");
299
- if (!cred || cred.username !== username.trim()) return null;
300
- const hashHex = await deriveHashHex(
301
- password,
302
- fromHex(cred.saltHex),
303
- cred.iterations
304
- );
305
- if (!hashHex || hashHex !== cred.hashHex) return null;
306
- await setMeta("identity", cred.identity);
307
- return cred.identity;
308
- }
309
-
310
- // ../ovr-offline/src/hooks.ts
311
- function useTickets() {
312
- return useLiveQuery(() => db.tickets.orderBy("createdAt").reverse().toArray());
313
- }
314
- function useCatalog() {
315
- return useLiveQuery(() => db.catalog.orderBy("category").toArray());
316
- }
317
- function useOfficers() {
318
- return useLiveQuery(() => db.officers.toArray());
319
- }
320
- function useStats() {
321
- return useLiveQuery(
322
- async () => (await db.meta.get("stats"))?.value
323
- );
324
- }
325
- function useTicket(ovrTicketNo) {
326
- return useLiveQuery(
327
- async () => await db.tickets.get(ovrTicketNo) ?? null,
328
- [ovrTicketNo]
329
- );
330
- }
331
- function usePendingSync() {
332
- const rows = useLiveQuery(() => db.outbox.toArray());
333
- return new Set((rows ?? []).map((r) => r.ovrTicketNo));
334
- }
335
- function useSync() {
336
- const [syncing, setSyncing] = useState(false);
337
- const [online, setOnline] = useState(true);
338
- const [error, setError] = useState(null);
339
- useEffect(() => {
340
- let cancelled = false;
341
- setOnline(isOnline());
342
- async function runSyncCycle() {
343
- if (!isOnline()) return;
344
- setSyncing(true);
345
- setError(null);
346
- try {
347
- await sync();
348
- } catch (e) {
349
- if (e instanceof SessionExpired) {
350
- await clearIdentity();
351
- window.location.assign("/admin/login");
352
- return;
353
- }
354
- if (!cancelled) setError(e instanceof Error ? e.message : "sync failed");
355
- } finally {
356
- if (!cancelled) setSyncing(false);
357
- }
358
- }
359
- const onOnline = () => {
360
- setOnline(true);
361
- void runSyncCycle();
362
- };
363
- const onOffline = () => setOnline(false);
364
- void runSyncCycle();
365
- window.addEventListener("online", onOnline);
366
- window.addEventListener("offline", onOffline);
367
- return () => {
368
- cancelled = true;
369
- window.removeEventListener("online", onOnline);
370
- window.removeEventListener("offline", onOffline);
371
- };
372
- }, []);
373
- return { syncing, online, error };
374
- }
375
-
376
- export { SessionExpired, cacheCredential, clearIdentity, db, ensureLease, getIdentity, getMeta, isOnline, issueTicketOffline, offlineApiBase, pullAll, pushOutbox, setMeta, setOfflineApiBase, sync, useAdminAuth, useCatalog, useOfficers, usePendingSync, useStats, useSync, useTicket, useTickets, verifyOffline };
2
+ export { SessionExpired, cacheCredential, clearIdentity, db, ensureLease, getIdentity, getMeta, isOnline, issueTicketOffline, offlineApiBase, pullAll, pushOutbox, setMeta, setOfflineApiBase, sync, useAdminAuth, useCatalog, useIdentity, useOfficers, usePendingSync, useStats, useSync, useTicket, useTickets, verifyOffline } from './chunk-IB4JVGKJ.js';
3
+ import './chunk-B634JHKZ.js';
package/dist/types.d.ts CHANGED
@@ -64,6 +64,7 @@ interface TicketRecord {
64
64
  officer: Officer;
65
65
  violations: IssuedViolation[];
66
66
  remarks?: string;
67
+ issuedBy?: string;
67
68
  assessedAt: string;
68
69
  dueDate: string;
69
70
  basicFinesTotal: number;
@@ -3,6 +3,7 @@ import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, Dialog
3
3
  import { Button } from '../chunk-I4WDVYHX.js';
4
4
  import { useCopy } from '../chunk-E2D7QT6N.js';
5
5
  import { cn } from '../chunk-77QBZC7J.js';
6
+ import { useIdentity } from '../chunk-IB4JVGKJ.js';
6
7
  import '../chunk-B634JHKZ.js';
7
8
  import Link from 'next/link';
8
9
  import { usePathname } from 'next/navigation';
@@ -21,8 +22,10 @@ function AdminNav({
21
22
  }) {
22
23
  const pathname = usePathname();
23
24
  const copy = useCopy();
25
+ const identity = useIdentity();
26
+ const limited = identity?.role === "ENFORCER";
24
27
  const nav = [
25
- { href: "/admin", label: copy.admin.dashboard, icon: LayoutDashboard },
28
+ ...limited ? [] : [{ href: "/admin", label: copy.admin.dashboard, icon: LayoutDashboard }],
26
29
  { href: "/admin/tickets", label: copy.admin.tickets, icon: ListChecks },
27
30
  { href: "/admin/tickets/new", label: copy.admin.issueTicket, icon: FilePlus2 }
28
31
  ];
@@ -1,9 +1,9 @@
1
1
  "use client";
2
+ import { Skeleton } from '../chunk-EGKFELO3.js';
2
3
  import { Textarea } from '../chunk-QCRVT2SS.js';
4
+ import { Checkbox } from '../chunk-BBQBKQA4.js';
3
5
  import { Input } from '../chunk-K3KIBHJF.js';
4
6
  import { Label } from '../chunk-XQTVSNHC.js';
5
- import { Skeleton } from '../chunk-EGKFELO3.js';
6
- import { Checkbox } from '../chunk-BBQBKQA4.js';
7
7
  import { TicketPreview } from '../chunk-GDOCD7LT.js';
8
8
  import '../chunk-JEYT63LE.js';
9
9
  import '../chunk-BIQ2J75Y.js';
@@ -124,6 +124,7 @@ function IssuanceForm({
124
124
  const [officerId, setOfficerId] = React.useState(officers[0]?.id ?? "");
125
125
  const [selected, setSelected] = React.useState({});
126
126
  const [remarks, setRemarks] = React.useState("");
127
+ const [issuedBy, setIssuedBy] = React.useState("");
127
128
  const [mounted, setMounted] = React.useState(false);
128
129
  const [open, setOpen] = React.useState(false);
129
130
  const [error, setError] = React.useState(null);
@@ -175,7 +176,8 @@ function IssuanceForm({
175
176
  catalogCode,
176
177
  details: details || void 0
177
178
  })),
178
- remarks: remarks || void 0
179
+ remarks: remarks || void 0,
180
+ issuedBy: issuedBy || void 0
179
181
  };
180
182
  startTransition(async () => {
181
183
  const res = await createAction(input);
@@ -284,15 +286,27 @@ function IssuanceForm({
284
286
  ] }),
285
287
  /* @__PURE__ */ jsxs(Card, { children: [
286
288
  /* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, { className: "text-base", children: "Remarks" }) }),
287
- /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx(
288
- Textarea,
289
- {
290
- value: remarks,
291
- onChange: (e) => setRemarks(e.target.value),
292
- placeholder: "Optional notes for this ticket.",
293
- rows: 3
294
- }
295
- ) })
289
+ /* @__PURE__ */ jsxs(CardContent, { className: "space-y-3", children: [
290
+ /* @__PURE__ */ jsx(
291
+ Textarea,
292
+ {
293
+ value: remarks,
294
+ onChange: (e) => setRemarks(e.target.value),
295
+ placeholder: "Optional notes for this ticket.",
296
+ rows: 3
297
+ }
298
+ ),
299
+ /* @__PURE__ */ jsx(
300
+ TextField,
301
+ {
302
+ id: "issuedBy",
303
+ label: "Issued by",
304
+ value: issuedBy,
305
+ onChange: setIssuedBy,
306
+ placeholder: "Optional \u2014 name shown on the receipt"
307
+ }
308
+ )
309
+ ] })
296
310
  ] })
297
311
  ] }),
298
312
  /* @__PURE__ */ jsx("div", { className: "lg:col-span-2", children: /* @__PURE__ */ jsxs("div", { className: "space-y-4 lg:sticky lg:top-6", children: [
@@ -63,7 +63,8 @@ function TicketReceipt({ ticket }) {
63
63
  label: "Apprehending Officer",
64
64
  value: `${ticket.officer.name}${ticket.officer.badgeNo ? ` (${ticket.officer.badgeNo})` : ""}`
65
65
  }
66
- )
66
+ ),
67
+ ticket.issuedBy ? /* @__PURE__ */ jsx(Field, { label: "Issued By", value: ticket.issuedBy }) : null
67
68
  ] }),
68
69
  /* @__PURE__ */ jsxs("div", { className: "mt-6 print:mt-3", children: [
69
70
  /* @__PURE__ */ jsx("p", { className: "mb-2 text-sm font-medium print:mb-1", children: "Violations" }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gelabs/ovr",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "The @gelabs/ovr SDK — one self-contained package over the OVR (Online Ordinance Violation Receipt) platform. A standalone per-LGU app installs ONLY this package and imports from @gelabs/ovr/{config,core,data,runtime,auth,ui,...}; the seven @gelabs/ovr-* implementation packages are bundled in at build time.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -161,10 +161,10 @@
161
161
  "@gelabs/ovr-types": "0.0.0",
162
162
  "@gelabs/ovr-config": "0.0.0",
163
163
  "@gelabs/ovr-data": "0.0.0",
164
- "@gelabs/ovr-core": "0.0.0",
165
- "@gelabs/ovr-auth": "0.0.0",
166
164
  "@gelabs/ovr-ui": "0.0.0",
167
165
  "@gelabs/ovr-offline": "0.0.0",
166
+ "@gelabs/ovr-core": "0.0.0",
167
+ "@gelabs/ovr-auth": "0.0.0",
168
168
  "@gelabs/ovr-runtime": "0.0.0"
169
169
  },
170
170
  "scripts": {
@@ -0,0 +1,2 @@
1
+ -- Optional "Issued by" name shown on the receipt (enforcer-entered).
2
+ ALTER TABLE "Ticket" ADD COLUMN "issuedBy" TEXT;
@@ -92,6 +92,7 @@ model Ticket {
92
92
  apprehendedAt DateTime
93
93
  placeOfViolation String?
94
94
  remarks String?
95
+ issuedBy String?
95
96
 
96
97
  officerId String
97
98
  officer Officer @relation(fields: [officerId], references: [id])