@gelabs/ovr 0.3.0 → 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.
@@ -0,0 +1,385 @@
1
+ "use client";
2
+ import { Textarea } from '../chunk-QCRVT2SS.js';
3
+ import { Money } from '../chunk-BVI5XDDA.js';
4
+ import { Card, CardContent } from '../chunk-SETIN6XP.js';
5
+ import { usePagination, Pagination } from '../chunk-6YFZLXFP.js';
6
+ import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from '../chunk-OWCGEEAZ.js';
7
+ import { Badge } from '../chunk-55FQP2DO.js';
8
+ import { Label } from '../chunk-XQTVSNHC.js';
9
+ import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from '../chunk-M35R6JLA.js';
10
+ import { Input } from '../chunk-K3KIBHJF.js';
11
+ import { Button } from '../chunk-I4WDVYHX.js';
12
+ import { useCopy } from '../chunk-TJSNVTVB.js';
13
+ import { cn } from '../chunk-77QBZC7J.js';
14
+ import '../chunk-BI4EGLPG.js';
15
+ import * as React from 'react';
16
+ import { toast } from 'sonner';
17
+ import { useRouter } from 'next/navigation';
18
+ import { Plus, Pencil, Loader2, Archive, ArchiveRestore } from 'lucide-react';
19
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
20
+
21
+ var fieldClass = "h-9 w-full rounded-lg border border-input bg-transparent px-2.5 text-sm shadow-xs outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:opacity-50 dark:bg-input/30";
22
+ function ViolationsManager({
23
+ violations,
24
+ createAction,
25
+ updateAction,
26
+ deleteAction
27
+ }) {
28
+ const t = useCopy().admin.violationsPage;
29
+ const router = useRouter();
30
+ const { pageItems, page, setPage, totalPages, from, to, total } = usePagination(violations, 12);
31
+ const categoryLabel = (c) => c === "TRAFFIC" ? t.categoryTraffic : t.categoryOrdinance;
32
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-5xl p-4 sm:p-6 lg:p-8", children: [
33
+ /* @__PURE__ */ jsxs("div", { className: "mb-6 flex items-center justify-between gap-3", children: [
34
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
35
+ /* @__PURE__ */ jsx("h1", { className: "font-heading text-2xl font-semibold tracking-tight", children: t.title }),
36
+ /* @__PURE__ */ jsx("p", { className: "max-w-2xl text-sm text-muted-foreground", children: t.subtitle })
37
+ ] }),
38
+ /* @__PURE__ */ jsx(
39
+ ViolationDialog,
40
+ {
41
+ mode: "create",
42
+ createAction,
43
+ onDone: () => router.refresh()
44
+ }
45
+ )
46
+ ] }),
47
+ /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { className: "p-0", children: violations.length === 0 ? /* @__PURE__ */ jsx("p", { className: "p-8 text-center text-sm text-muted-foreground", children: t.empty }) : /* @__PURE__ */ jsxs(Fragment, { children: [
48
+ /* @__PURE__ */ jsxs(Table, { children: [
49
+ /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
50
+ /* @__PURE__ */ jsx(TableHead, { children: t.code }),
51
+ /* @__PURE__ */ jsx(TableHead, { children: t.titleLabel }),
52
+ /* @__PURE__ */ jsx(TableHead, { className: "hidden sm:table-cell", children: t.category }),
53
+ /* @__PURE__ */ jsx(TableHead, { className: "text-right", children: t.fine }),
54
+ /* @__PURE__ */ jsx(TableHead, { children: t.status }),
55
+ /* @__PURE__ */ jsx(TableHead, { className: "text-right", children: t.actions })
56
+ ] }) }),
57
+ /* @__PURE__ */ jsx(TableBody, { children: pageItems.map((v) => {
58
+ const archived = v.active === false;
59
+ return /* @__PURE__ */ jsxs(TableRow, { className: cn(archived && "opacity-60"), children: [
60
+ /* @__PURE__ */ jsx(TableCell, { className: "font-mono text-xs", children: v.code }),
61
+ /* @__PURE__ */ jsx(TableCell, { className: "font-medium", children: v.title }),
62
+ /* @__PURE__ */ jsx(TableCell, { className: "hidden text-muted-foreground sm:table-cell", children: categoryLabel(v.category) }),
63
+ /* @__PURE__ */ jsx(TableCell, { className: "text-right", children: /* @__PURE__ */ jsx(Money, { value: v.basicFine }) }),
64
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(Badge, { variant: archived ? "outline" : "secondary", children: archived ? t.archived : t.active }) }),
65
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-1.5", children: [
66
+ /* @__PURE__ */ jsx(
67
+ ViolationDialog,
68
+ {
69
+ mode: "edit",
70
+ violation: v,
71
+ updateAction,
72
+ onDone: () => router.refresh()
73
+ }
74
+ ),
75
+ archived ? /* @__PURE__ */ jsx(
76
+ RestoreButton,
77
+ {
78
+ violation: v,
79
+ updateAction,
80
+ onDone: () => router.refresh()
81
+ }
82
+ ) : /* @__PURE__ */ jsx(
83
+ ArchiveButton,
84
+ {
85
+ violation: v,
86
+ deleteAction,
87
+ onDone: () => router.refresh()
88
+ }
89
+ )
90
+ ] }) })
91
+ ] }, v.code);
92
+ }) })
93
+ ] }),
94
+ /* @__PURE__ */ jsx(
95
+ Pagination,
96
+ {
97
+ page,
98
+ totalPages,
99
+ from,
100
+ to,
101
+ total,
102
+ onPage: setPage
103
+ }
104
+ )
105
+ ] }) }) })
106
+ ] });
107
+ }
108
+ function ViolationDialog({
109
+ mode,
110
+ violation,
111
+ createAction,
112
+ updateAction,
113
+ onDone
114
+ }) {
115
+ const t = useCopy().admin.violationsPage;
116
+ const [open, setOpen] = React.useState(false);
117
+ const [code, setCode] = React.useState(violation?.code ?? "");
118
+ const [title, setTitle] = React.useState(violation?.title ?? "");
119
+ const [category, setCategory] = React.useState(
120
+ violation?.category ?? "TRAFFIC"
121
+ );
122
+ const [fine, setFine] = React.useState(
123
+ violation ? String(violation.basicFine) : ""
124
+ );
125
+ const [legalText, setLegalText] = React.useState(violation?.legalText ?? "");
126
+ const [submitting, setSubmitting] = React.useState(false);
127
+ function resetTo(v) {
128
+ setCode(v?.code ?? "");
129
+ setTitle(v?.title ?? "");
130
+ setCategory(v?.category ?? "TRAFFIC");
131
+ setFine(v ? String(v.basicFine) : "");
132
+ setLegalText(v?.legalText ?? "");
133
+ }
134
+ async function submit(e) {
135
+ e.preventDefault();
136
+ if (mode === "create" && !code.trim())
137
+ return toast.error(`${t.code} is required.`);
138
+ if (!title.trim()) return toast.error(`${t.titleLabel} is required.`);
139
+ const fineNum = Number(fine);
140
+ if (!Number.isFinite(fineNum) || fineNum < 0)
141
+ return toast.error(`${t.fine} must be a number \u2265 0.`);
142
+ setSubmitting(true);
143
+ try {
144
+ let res;
145
+ if (mode === "create") {
146
+ const input = {
147
+ code: code.trim(),
148
+ title: title.trim(),
149
+ category,
150
+ basicFine: fineNum,
151
+ legalText: legalText.trim() || void 0
152
+ };
153
+ res = await createAction?.(input);
154
+ } else {
155
+ res = await updateAction?.(violation.code, {
156
+ title: title.trim(),
157
+ category,
158
+ basicFine: fineNum,
159
+ legalText: legalText.trim() || void 0
160
+ });
161
+ }
162
+ if (res?.error) {
163
+ toast.error(res.error);
164
+ return;
165
+ }
166
+ toast.success(
167
+ `${mode === "create" ? t.create : t.editTitle}: ${title.trim()}`
168
+ );
169
+ setOpen(false);
170
+ onDone();
171
+ } finally {
172
+ setSubmitting(false);
173
+ }
174
+ }
175
+ return /* @__PURE__ */ jsxs(
176
+ Dialog,
177
+ {
178
+ open,
179
+ onOpenChange: (o) => {
180
+ if (o) resetTo(violation);
181
+ setOpen(o);
182
+ },
183
+ children: [
184
+ /* @__PURE__ */ jsxs(
185
+ DialogTrigger,
186
+ {
187
+ render: mode === "create" ? /* @__PURE__ */ jsx(Button, { className: "gap-1.5" }) : /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", className: "gap-1.5" }),
188
+ children: [
189
+ mode === "create" ? /* @__PURE__ */ jsx(Plus, { className: "size-4" }) : /* @__PURE__ */ jsx(Pencil, { className: "size-3.5" }),
190
+ /* @__PURE__ */ jsx("span", { className: mode === "create" ? "" : "hidden sm:inline", children: mode === "create" ? t.newViolation : t.edit })
191
+ ]
192
+ }
193
+ ),
194
+ /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-md", children: [
195
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
196
+ /* @__PURE__ */ jsx(DialogTitle, { children: mode === "create" ? t.newViolation : t.editTitle }),
197
+ /* @__PURE__ */ jsx(DialogDescription, { children: t.subtitle })
198
+ ] }),
199
+ /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: "space-y-4", children: [
200
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
201
+ /* @__PURE__ */ jsx(Label, { htmlFor: "vio-code", children: t.code }),
202
+ /* @__PURE__ */ jsx(
203
+ Input,
204
+ {
205
+ id: "vio-code",
206
+ value: code,
207
+ onChange: (e) => setCode(e.target.value),
208
+ placeholder: t.codePlaceholder,
209
+ autoComplete: "off",
210
+ disabled: mode === "edit",
211
+ autoFocus: mode === "create"
212
+ }
213
+ )
214
+ ] }),
215
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
216
+ /* @__PURE__ */ jsx(Label, { htmlFor: "vio-title", children: t.titleLabel }),
217
+ /* @__PURE__ */ jsx(
218
+ Input,
219
+ {
220
+ id: "vio-title",
221
+ value: title,
222
+ onChange: (e) => setTitle(e.target.value),
223
+ placeholder: t.titlePlaceholder,
224
+ autoComplete: "off",
225
+ autoFocus: mode === "edit"
226
+ }
227
+ )
228
+ ] }),
229
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [
230
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
231
+ /* @__PURE__ */ jsx(Label, { htmlFor: "vio-category", children: t.category }),
232
+ /* @__PURE__ */ jsxs(
233
+ "select",
234
+ {
235
+ id: "vio-category",
236
+ value: category,
237
+ onChange: (e) => setCategory(e.target.value),
238
+ className: fieldClass,
239
+ children: [
240
+ /* @__PURE__ */ jsx("option", { value: "TRAFFIC", children: t.categoryTraffic }),
241
+ /* @__PURE__ */ jsx("option", { value: "ORDINANCE", children: t.categoryOrdinance })
242
+ ]
243
+ }
244
+ )
245
+ ] }),
246
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
247
+ /* @__PURE__ */ jsx(Label, { htmlFor: "vio-fine", children: t.fine }),
248
+ /* @__PURE__ */ jsx(
249
+ Input,
250
+ {
251
+ id: "vio-fine",
252
+ type: "number",
253
+ min: "0",
254
+ step: "1",
255
+ inputMode: "numeric",
256
+ value: fine,
257
+ onChange: (e) => setFine(e.target.value),
258
+ placeholder: t.finePlaceholder,
259
+ autoComplete: "off"
260
+ }
261
+ )
262
+ ] })
263
+ ] }),
264
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
265
+ /* @__PURE__ */ jsx(Label, { htmlFor: "vio-legal", children: t.legalText }),
266
+ /* @__PURE__ */ jsx(
267
+ Textarea,
268
+ {
269
+ id: "vio-legal",
270
+ value: legalText,
271
+ onChange: (e) => setLegalText(e.target.value),
272
+ placeholder: t.legalTextPlaceholder,
273
+ rows: 2
274
+ }
275
+ )
276
+ ] }),
277
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
278
+ /* @__PURE__ */ jsx(DialogClose, { render: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline" }), children: t.cancel }),
279
+ /* @__PURE__ */ jsxs(Button, { type: "submit", disabled: submitting, className: "gap-1.5", children: [
280
+ submitting ? /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin" }) : null,
281
+ submitting ? mode === "create" ? t.creating : t.saving : mode === "create" ? t.create : t.save
282
+ ] })
283
+ ] })
284
+ ] })
285
+ ] })
286
+ ]
287
+ }
288
+ );
289
+ }
290
+ function ArchiveButton({
291
+ violation,
292
+ deleteAction,
293
+ onDone
294
+ }) {
295
+ const t = useCopy().admin.violationsPage;
296
+ const [busy, setBusy] = React.useState(false);
297
+ async function archive() {
298
+ setBusy(true);
299
+ try {
300
+ const res = await deleteAction(violation.code);
301
+ if (res?.error) {
302
+ toast.error(res.error);
303
+ return;
304
+ }
305
+ toast.success(`${t.archive}: ${violation.title}`);
306
+ onDone();
307
+ } finally {
308
+ setBusy(false);
309
+ }
310
+ }
311
+ return /* @__PURE__ */ jsxs(Dialog, { children: [
312
+ /* @__PURE__ */ jsxs(
313
+ DialogTrigger,
314
+ {
315
+ render: /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", className: "gap-1.5" }),
316
+ children: [
317
+ /* @__PURE__ */ jsx(Archive, { className: "size-3.5" }),
318
+ /* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: t.archive })
319
+ ]
320
+ }
321
+ ),
322
+ /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-sm", children: [
323
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
324
+ /* @__PURE__ */ jsx(DialogTitle, { children: t.archiveConfirmTitle }),
325
+ /* @__PURE__ */ jsxs(DialogDescription, { children: [
326
+ violation.title,
327
+ " \u2014 ",
328
+ t.archiveConfirmBody
329
+ ] })
330
+ ] }),
331
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
332
+ /* @__PURE__ */ jsx(DialogClose, { render: /* @__PURE__ */ jsx(Button, { variant: "outline" }), children: t.cancel }),
333
+ /* @__PURE__ */ jsxs(
334
+ DialogClose,
335
+ {
336
+ render: /* @__PURE__ */ jsx(Button, { className: "gap-2", disabled: busy }),
337
+ onClick: archive,
338
+ children: [
339
+ /* @__PURE__ */ jsx(Archive, { className: "size-4" }),
340
+ t.archive
341
+ ]
342
+ }
343
+ )
344
+ ] })
345
+ ] })
346
+ ] });
347
+ }
348
+ function RestoreButton({
349
+ violation,
350
+ updateAction,
351
+ onDone
352
+ }) {
353
+ const t = useCopy().admin.violationsPage;
354
+ const [busy, setBusy] = React.useState(false);
355
+ async function restore() {
356
+ setBusy(true);
357
+ try {
358
+ const res = await updateAction(violation.code, { active: true });
359
+ if (res?.error) {
360
+ toast.error(res.error);
361
+ return;
362
+ }
363
+ toast.success(`${t.restore}: ${violation.title}`);
364
+ onDone();
365
+ } finally {
366
+ setBusy(false);
367
+ }
368
+ }
369
+ return /* @__PURE__ */ jsxs(
370
+ Button,
371
+ {
372
+ variant: "outline",
373
+ size: "sm",
374
+ className: "gap-1.5",
375
+ disabled: busy,
376
+ onClick: restore,
377
+ children: [
378
+ busy ? /* @__PURE__ */ jsx(Loader2, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ jsx(ArchiveRestore, { className: "size-3.5" }),
379
+ /* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: t.restore })
380
+ ]
381
+ }
382
+ );
383
+ }
384
+
385
+ export { ViolationsManager };
@@ -1,7 +1,7 @@
1
1
  "use client";
2
- import { ThemeToggle } from '../chunk-2C3VCTYJ.js';
3
2
  import { MunicipalSeal } from '../chunk-YC7G2IOZ.js';
4
3
  import '../chunk-WOPU6DI7.js';
4
+ import { ThemeToggle } from '../chunk-2C3VCTYJ.js';
5
5
  import '../chunk-I4WDVYHX.js';
6
6
  import { useOvrConfig, useCopy } from '../chunk-TJSNVTVB.js';
7
7
  import '../chunk-77QBZC7J.js';
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { b as OvrConfig } from './schema-CdsFQxIg.js';
3
3
  import { a as Formatters } from './format-C7MSwUHK.js';
4
- import { D as Dictionary } from './types-BOgdk0Jw.js';
4
+ import { D as Dictionary } from './types-B8MopM4b.js';
5
5
  import 'zod';
6
6
  import './types.js';
7
7
 
@@ -1,6 +1,6 @@
1
1
  import { b as OvrConfig } from './schema-CdsFQxIg.js';
2
2
  import { a as Formatters } from './format-C7MSwUHK.js';
3
- import { D as Dictionary, C as CopyOverrides } from './types-BOgdk0Jw.js';
3
+ import { D as Dictionary, C as CopyOverrides } from './types-B8MopM4b.js';
4
4
  import 'zod';
5
5
  import './types.js';
6
6
 
package/dist/ui-server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { mergeCopy } from './chunk-HGWPA7FU.js';
1
+ import { mergeCopy } from './chunk-DJMUW5T2.js';
2
2
  import { createFormatters } from './chunk-BI4EGLPG.js';
3
3
  import 'server-only';
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gelabs/ovr",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
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",
@@ -158,14 +158,14 @@
158
158
  "react-dom": "19.2.4",
159
159
  "tsup": "^8.4.0",
160
160
  "typescript": "^5",
161
- "@gelabs/ovr-types": "0.0.0",
162
161
  "@gelabs/ovr-config": "0.0.0",
163
- "@gelabs/ovr-data": "0.0.0",
162
+ "@gelabs/ovr-offline": "0.0.0",
164
163
  "@gelabs/ovr-core": "0.0.0",
165
- "@gelabs/ovr-auth": "0.0.0",
166
164
  "@gelabs/ovr-ui": "0.0.0",
165
+ "@gelabs/ovr-types": "0.0.0",
166
+ "@gelabs/ovr-data": "0.0.0",
167
167
  "@gelabs/ovr-runtime": "0.0.0",
168
- "@gelabs/ovr-offline": "0.0.0"
168
+ "@gelabs/ovr-auth": "0.0.0"
169
169
  },
170
170
  "scripts": {
171
171
  "build": "tsup && node scripts/copy-assets.mjs",
@@ -0,0 +1,5 @@
1
+ -- GE-031 (violation catalog management): soft-delete + edit tracking.
2
+ -- `active` lets admins archive a violation (hidden from issuance) while keeping
3
+ -- it resolvable for already-issued tickets. Existing rows default to active.
4
+ ALTER TABLE "ViolationCatalog" ADD COLUMN "active" BOOLEAN NOT NULL DEFAULT true;
5
+ ALTER TABLE "ViolationCatalog" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
@@ -61,8 +61,10 @@ model ViolationCatalog {
61
61
  category ViolationCategory
62
62
  basicFine Decimal @db.Decimal(12, 2)
63
63
  legalText String?
64
+ active Boolean @default(true) // GE-031: false = archived (hidden from issuance)
64
65
 
65
66
  createdAt DateTime @default(now())
67
+ updatedAt DateTime @default(now()) @updatedAt
66
68
  }
67
69
 
68
70
  model Ticket {