@gelabs/ovr 0.1.1 → 0.2.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 (101) hide show
  1. package/dist/chunk-3KIDW4LT.js +13 -0
  2. package/dist/chunk-4SZXBT56.js +26 -0
  3. package/dist/chunk-55FQP2DO.js +46 -0
  4. package/dist/{chunk-JV7QQ22Q.js → chunk-5YYR37CF.js} +10 -1
  5. package/dist/chunk-5Z2IAD5I.js +72 -0
  6. package/dist/chunk-BBQBKQA4.js +31 -0
  7. package/dist/chunk-BIQ2J75Y.js +54 -0
  8. package/dist/chunk-E2D7QT6N.js +92 -0
  9. package/dist/chunk-EGKFELO3.js +15 -0
  10. package/dist/chunk-EYFZWQ4J.js +74 -0
  11. package/dist/chunk-GDOCD7LT.js +46 -0
  12. package/dist/chunk-IF5UAVIE.js +25 -0
  13. package/dist/chunk-JEYT63LE.js +25 -0
  14. package/dist/chunk-K3KIBHJF.js +20 -0
  15. package/dist/chunk-M35R6JLA.js +142 -0
  16. package/dist/chunk-MDTRBOPQ.js +22 -0
  17. package/dist/chunk-NSCIBSCW.js +24 -0
  18. package/dist/chunk-OE525ZER.js +31 -0
  19. package/dist/chunk-OWCGEEAZ.js +107 -0
  20. package/dist/chunk-QCRVT2SS.js +18 -0
  21. package/dist/chunk-SETIN6XP.js +95 -0
  22. package/dist/chunk-WOPU6DI7.js +77 -0
  23. package/dist/chunk-XQTVSNHC.js +18 -0
  24. package/dist/core-i18n.d.ts +2 -2
  25. package/dist/core-i18n.js +1 -1
  26. package/dist/data-db.d.ts +1 -4
  27. package/dist/data-mock-store.js +53 -1
  28. package/dist/data-prisma-store.d.ts +3 -1
  29. package/dist/data-prisma-store.js +84 -1
  30. package/dist/data-seed-runner.d.ts +2 -1
  31. package/dist/data.d.ts +13 -1
  32. package/dist/offline.d.ts +126 -0
  33. package/dist/offline.js +376 -0
  34. package/dist/{types-DrrNO_Ak.d.ts → types-CtBC5-TW.d.ts} +8 -0
  35. package/dist/ui-components-admin/admin-nav.d.ts +7 -0
  36. package/dist/ui-components-admin/admin-nav.js +83 -0
  37. package/dist/ui-components-admin/issuance-form.d.ts +15 -0
  38. package/dist/ui-components-admin/issuance-form.js +367 -0
  39. package/dist/ui-components-admin/stat-card.d.ts +10 -0
  40. package/dist/ui-components-admin/stat-card.js +29 -0
  41. package/dist/ui-components-admin/ticket-preview.d.ts +9 -0
  42. package/dist/ui-components-admin/ticket-preview.js +13 -0
  43. package/dist/ui-components-admin/tickets-table.d.ts +8 -0
  44. package/dist/ui-components-admin/tickets-table.js +50 -0
  45. package/dist/ui-components-citizen/citizen-nav.d.ts +5 -0
  46. package/dist/ui-components-citizen/citizen-nav.js +33 -0
  47. package/dist/ui-components-citizen/payment-form.d.ts +14 -0
  48. package/dist/ui-components-citizen/payment-form.js +168 -0
  49. package/dist/ui-components-citizen/payment-qr-dialog.d.ts +30 -0
  50. package/dist/ui-components-citizen/payment-qr-dialog.js +9 -0
  51. package/dist/ui-components-citizen/ticket-not-found.d.ts +5 -0
  52. package/dist/ui-components-citizen/ticket-not-found.js +21 -0
  53. package/dist/ui-components-citizen/violation-history-table.d.ts +16 -0
  54. package/dist/ui-components-citizen/violation-history-table.js +77 -0
  55. package/dist/ui-components-shared/amount-summary.d.ts +8 -0
  56. package/dist/ui-components-shared/amount-summary.js +7 -0
  57. package/dist/ui-components-shared/money.d.ts +9 -0
  58. package/dist/ui-components-shared/money.js +5 -0
  59. package/dist/ui-components-shared/municipal-seal.d.ts +11 -0
  60. package/dist/ui-components-shared/municipal-seal.js +6 -0
  61. package/dist/ui-components-shared/official-header.d.ts +5 -0
  62. package/dist/ui-components-shared/official-header.js +7 -0
  63. package/dist/ui-components-shared/print-button.d.ts +8 -0
  64. package/dist/ui-components-shared/print-button.js +17 -0
  65. package/dist/ui-components-shared/seal.d.ts +12 -0
  66. package/dist/ui-components-shared/seal.js +2 -0
  67. package/dist/ui-components-shared/site-header.d.ts +13 -0
  68. package/dist/ui-components-shared/site-header.js +40 -0
  69. package/dist/ui-components-shared/sonner.d.ts +6 -0
  70. package/dist/ui-components-shared/sonner.js +38 -0
  71. package/dist/ui-components-shared/status-badge.d.ts +9 -0
  72. package/dist/ui-components-shared/status-badge.js +3 -0
  73. package/dist/ui-components-shared/theme-toggle.d.ts +7 -0
  74. package/dist/ui-components-shared/theme-toggle.js +6 -0
  75. package/dist/ui-components-shared/ticket-receipt.d.ts +12 -0
  76. package/dist/ui-components-shared/ticket-receipt.js +158 -0
  77. package/dist/ui-components-shared/violations-table.d.ts +8 -0
  78. package/dist/ui-components-shared/violations-table.js +7 -0
  79. package/dist/ui-components-ui/alert.js +2 -74
  80. package/dist/ui-components-ui/badge.d.ts +1 -1
  81. package/dist/ui-components-ui/badge.js +2 -46
  82. package/dist/ui-components-ui/button.d.ts +2 -2
  83. package/dist/ui-components-ui/card.js +2 -95
  84. package/dist/ui-components-ui/checkbox.js +2 -31
  85. package/dist/ui-components-ui/dialog.js +3 -142
  86. package/dist/ui-components-ui/input.js +2 -20
  87. package/dist/ui-components-ui/label.js +2 -18
  88. package/dist/ui-components-ui/separator.js +2 -24
  89. package/dist/ui-components-ui/skeleton.js +2 -15
  90. package/dist/ui-components-ui/table.js +2 -107
  91. package/dist/ui-components-ui/textarea.js +2 -18
  92. package/dist/ui-config.d.ts +19 -2
  93. package/dist/ui-config.js +2 -36
  94. package/dist/ui-server.d.ts +1 -1
  95. package/dist/ui-server.js +1 -1
  96. package/package.json +24 -4
  97. package/prisma/migrations/20260611112111_init/migration.sql +142 -0
  98. package/prisma/migrations/20260611112121_add_ticket_sequence/migration.sql +4 -0
  99. package/prisma/migrations/20260612000000_split_violator_address/migration.sql +26 -0
  100. package/prisma/migrations/20260612000100_barangay_optional/migration.sql +2 -0
  101. package/prisma/migrations/migration_lock.toml +3 -0
@@ -0,0 +1,83 @@
1
+ "use client";
2
+ import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from '../chunk-M35R6JLA.js';
3
+ import { Button } from '../chunk-I4WDVYHX.js';
4
+ import { useCopy } from '../chunk-E2D7QT6N.js';
5
+ import { cn } from '../chunk-77QBZC7J.js';
6
+ import '../chunk-B634JHKZ.js';
7
+ import Link from 'next/link';
8
+ import { usePathname } from 'next/navigation';
9
+ import { LayoutDashboard, ListChecks, FilePlus2, LogOut } from 'lucide-react';
10
+ import { jsxs, jsx } from 'react/jsx-runtime';
11
+
12
+ function isActive(p, href) {
13
+ if (href === "/admin") return p === "/admin";
14
+ if (href === "/admin/tickets") {
15
+ return p === "/admin/tickets" || p.startsWith("/admin/tickets/") && p !== "/admin/tickets/new";
16
+ }
17
+ return p === href;
18
+ }
19
+ function AdminNav({
20
+ signOutAction
21
+ }) {
22
+ const pathname = usePathname();
23
+ const copy = useCopy();
24
+ const nav = [
25
+ { href: "/admin", label: copy.admin.dashboard, icon: LayoutDashboard },
26
+ { href: "/admin/tickets", label: copy.admin.tickets, icon: ListChecks },
27
+ { href: "/admin/tickets/new", label: copy.admin.issueTicket, icon: FilePlus2 }
28
+ ];
29
+ return /* @__PURE__ */ jsxs("nav", { className: "flex items-center gap-0.5 sm:gap-1", children: [
30
+ nav.map((it) => {
31
+ const active = isActive(pathname, it.href);
32
+ const Icon = it.icon;
33
+ return /* @__PURE__ */ jsxs(
34
+ Link,
35
+ {
36
+ href: it.href,
37
+ className: cn(
38
+ "flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-sm font-medium transition-colors",
39
+ active ? "bg-white/15 text-gov-foreground" : "text-gov-foreground/80 hover:bg-white/10 hover:text-gov-foreground"
40
+ ),
41
+ children: [
42
+ /* @__PURE__ */ jsx(Icon, { className: "size-4 shrink-0" }),
43
+ /* @__PURE__ */ jsx("span", { className: "hidden md:inline", children: it.label })
44
+ ]
45
+ },
46
+ it.href
47
+ );
48
+ }),
49
+ /* @__PURE__ */ jsxs(Dialog, { children: [
50
+ /* @__PURE__ */ jsxs(
51
+ DialogTrigger,
52
+ {
53
+ render: /* @__PURE__ */ jsx(
54
+ "button",
55
+ {
56
+ type: "button",
57
+ className: "flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-sm font-medium text-gov-foreground/80 transition-colors hover:bg-white/10 hover:text-gov-foreground"
58
+ }
59
+ ),
60
+ children: [
61
+ /* @__PURE__ */ jsx(LogOut, { className: "size-4 shrink-0" }),
62
+ /* @__PURE__ */ jsx("span", { className: "hidden md:inline", children: "Sign out" })
63
+ ]
64
+ }
65
+ ),
66
+ /* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-sm", children: [
67
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
68
+ /* @__PURE__ */ jsx(DialogTitle, { children: "Sign out?" }),
69
+ /* @__PURE__ */ jsx(DialogDescription, { children: "You'll need to sign in again to issue or manage tickets." })
70
+ ] }),
71
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
72
+ /* @__PURE__ */ jsx(DialogClose, { render: /* @__PURE__ */ jsx(Button, { variant: "outline" }), children: "Cancel" }),
73
+ /* @__PURE__ */ jsx("form", { action: signOutAction, children: /* @__PURE__ */ jsxs(Button, { type: "submit", variant: "destructive", className: "gap-2", children: [
74
+ /* @__PURE__ */ jsx(LogOut, { className: "size-4" }),
75
+ "Sign out"
76
+ ] }) })
77
+ ] })
78
+ ] })
79
+ ] })
80
+ ] });
81
+ }
82
+
83
+ export { AdminNav };
@@ -0,0 +1,15 @@
1
+ import * as React from 'react';
2
+ import { NewTicketInput } from '../data.js';
3
+ import { ViolationCatalogItem, Officer } from '../types.js';
4
+
5
+ /** The Server Action that issues the ticket, injected by the consuming page. */
6
+ type CreateTicketAction = (input: NewTicketInput) => Promise<{
7
+ error?: string;
8
+ } | void>;
9
+ declare function IssuanceForm({ catalog, officers, createAction, }: {
10
+ catalog: ViolationCatalogItem[];
11
+ officers: Officer[];
12
+ createAction: CreateTicketAction;
13
+ }): React.JSX.Element;
14
+
15
+ export { type CreateTicketAction, IssuanceForm };
@@ -0,0 +1,367 @@
1
+ "use client";
2
+ import { Textarea } from '../chunk-QCRVT2SS.js';
3
+ import { Input } from '../chunk-K3KIBHJF.js';
4
+ import { Label } from '../chunk-XQTVSNHC.js';
5
+ import { Skeleton } from '../chunk-EGKFELO3.js';
6
+ import { Checkbox } from '../chunk-BBQBKQA4.js';
7
+ import { TicketPreview } from '../chunk-GDOCD7LT.js';
8
+ import '../chunk-JEYT63LE.js';
9
+ import '../chunk-BIQ2J75Y.js';
10
+ import '../chunk-NSCIBSCW.js';
11
+ import { Alert, AlertDescription } from '../chunk-EYFZWQ4J.js';
12
+ import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '../chunk-SETIN6XP.js';
13
+ import '../chunk-OE525ZER.js';
14
+ import '../chunk-OWCGEEAZ.js';
15
+ import '../chunk-55FQP2DO.js';
16
+ import '../chunk-3KIDW4LT.js';
17
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from '../chunk-M35R6JLA.js';
18
+ import { Button } from '../chunk-I4WDVYHX.js';
19
+ import { useFormatters, useOvrConfig } from '../chunk-E2D7QT6N.js';
20
+ import { cn } from '../chunk-77QBZC7J.js';
21
+ import { localToManilaISO, toPreviewTicket } from '../chunk-B634JHKZ.js';
22
+ import * as React from 'react';
23
+ import { toast } from 'sonner';
24
+ import { FilePlus2, TriangleAlert, Loader2 } from 'lucide-react';
25
+ import { jsxs, jsx } from 'react/jsx-runtime';
26
+
27
+ 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";
28
+ function TextField({
29
+ id,
30
+ label,
31
+ value,
32
+ onChange,
33
+ placeholder,
34
+ required,
35
+ type = "text"
36
+ }) {
37
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
38
+ /* @__PURE__ */ jsxs(Label, { htmlFor: id, children: [
39
+ label,
40
+ required ? /* @__PURE__ */ jsx("span", { className: "text-destructive", children: " *" }) : null
41
+ ] }),
42
+ /* @__PURE__ */ jsx(
43
+ Input,
44
+ {
45
+ id,
46
+ type,
47
+ value,
48
+ onChange: (e) => onChange(e.target.value),
49
+ placeholder,
50
+ autoComplete: "off"
51
+ }
52
+ )
53
+ ] });
54
+ }
55
+ function ViolationRow({
56
+ item,
57
+ checked,
58
+ details,
59
+ onToggle,
60
+ onDetails,
61
+ formatPeso
62
+ }) {
63
+ return /* @__PURE__ */ jsxs(
64
+ "div",
65
+ {
66
+ className: cn(
67
+ "rounded-lg border p-3 transition-colors",
68
+ checked ? "border-primary/40 bg-primary/5" : "hover:bg-muted/40"
69
+ ),
70
+ children: [
71
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
72
+ /* @__PURE__ */ jsx(
73
+ Checkbox,
74
+ {
75
+ id: `v-${item.code}`,
76
+ checked,
77
+ onCheckedChange: (v) => onToggle(v === true),
78
+ className: "mt-0.5"
79
+ }
80
+ ),
81
+ /* @__PURE__ */ jsxs("label", { htmlFor: `v-${item.code}`, className: "flex-1 cursor-pointer", children: [
82
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center justify-between gap-2", children: [
83
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: item.title }),
84
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm font-medium tabular-nums", children: formatPeso(item.basicFine) })
85
+ ] }),
86
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-xs text-muted-foreground", children: item.code })
87
+ ] })
88
+ ] }),
89
+ checked ? /* @__PURE__ */ jsx(
90
+ Textarea,
91
+ {
92
+ value: details,
93
+ onChange: (e) => onDetails(e.target.value),
94
+ placeholder: "Add details \u2014 location, circumstances, plate, etc.",
95
+ rows: 2,
96
+ className: "mt-2.5"
97
+ }
98
+ ) : null
99
+ ]
100
+ }
101
+ );
102
+ }
103
+ function IssuanceForm({
104
+ catalog,
105
+ officers,
106
+ createAction
107
+ }) {
108
+ const { formatPeso, nowManilaLocalInput } = useFormatters();
109
+ const { municipality, rules } = useOvrConfig();
110
+ const [firstName, setFirstName] = React.useState("");
111
+ const [middleName, setMiddleName] = React.useState("");
112
+ const [lastName, setLastName] = React.useState("");
113
+ const [street, setStreet] = React.useState("");
114
+ const [barangay, setBarangay] = React.useState("");
115
+ const [cityMunicipality, setCityMunicipality] = React.useState(
116
+ municipality.shortName
117
+ );
118
+ const [province, setProvince] = React.useState(municipality.province);
119
+ const [licenseNumber, setLicenseNumber] = React.useState("");
120
+ const [plateNumber, setPlateNumber] = React.useState("");
121
+ const [contactNo, setContactNo] = React.useState("");
122
+ const [apprehendedLocal, setApprehendedLocal] = React.useState("");
123
+ const [placeOfViolation, setPlaceOfViolation] = React.useState("");
124
+ const [officerId, setOfficerId] = React.useState(officers[0]?.id ?? "");
125
+ const [selected, setSelected] = React.useState({});
126
+ const [remarks, setRemarks] = React.useState("");
127
+ const [mounted, setMounted] = React.useState(false);
128
+ const [open, setOpen] = React.useState(false);
129
+ const [error, setError] = React.useState(null);
130
+ const [isPending, startTransition] = React.useTransition();
131
+ React.useEffect(() => {
132
+ setMounted(true);
133
+ setApprehendedLocal(nowManilaLocalInput());
134
+ }, [nowManilaLocalInput]);
135
+ function toggle(code, checked) {
136
+ setSelected((prev) => {
137
+ const next = { ...prev };
138
+ if (checked) next[code] = next[code] ?? "";
139
+ else delete next[code];
140
+ return next;
141
+ });
142
+ }
143
+ const violator = {
144
+ firstName,
145
+ middleName: middleName || void 0,
146
+ lastName,
147
+ street,
148
+ barangay: barangay || void 0,
149
+ cityMunicipality,
150
+ province,
151
+ licenseNumber,
152
+ plateNumber: plateNumber || void 0,
153
+ contactNo: contactNo || void 0
154
+ };
155
+ const chosen = catalog.filter((c) => c.code in selected);
156
+ const total = chosen.reduce((s, c) => s + c.basicFine, 0);
157
+ const draft = {
158
+ violator,
159
+ apprehendedAtISO: apprehendedLocal ? localToManilaISO(apprehendedLocal) : "",
160
+ placeOfViolation: placeOfViolation || void 0,
161
+ officer: officers.find((o) => o.id === officerId),
162
+ violations: chosen.map((c) => ({ item: c, details: selected[c.code] })),
163
+ remarks: remarks || void 0
164
+ };
165
+ const preview = toPreviewTicket(draft, /* @__PURE__ */ new Date(), rules);
166
+ const valid = !!firstName.trim() && !!lastName.trim() && !!street.trim() && !!cityMunicipality.trim() && !!province.trim() && !!licenseNumber.trim() && !!apprehendedLocal && !!officerId && chosen.length > 0;
167
+ function issue() {
168
+ setError(null);
169
+ const input = {
170
+ violator,
171
+ apprehendedAt: localToManilaISO(apprehendedLocal),
172
+ placeOfViolation: placeOfViolation || void 0,
173
+ officerId,
174
+ violations: Object.entries(selected).map(([catalogCode, details]) => ({
175
+ catalogCode,
176
+ details: details || void 0
177
+ })),
178
+ remarks: remarks || void 0
179
+ };
180
+ startTransition(async () => {
181
+ const res = await createAction(input);
182
+ if (res?.error) {
183
+ setError(res.error);
184
+ toast.error(res.error);
185
+ }
186
+ });
187
+ }
188
+ const traffic = catalog.filter((c) => c.category === "TRAFFIC");
189
+ const ordinance = catalog.filter((c) => c.category === "ORDINANCE");
190
+ return /* @__PURE__ */ jsxs("div", { className: "grid gap-6 lg:grid-cols-5", children: [
191
+ /* @__PURE__ */ jsxs("div", { className: "space-y-6 lg:col-span-3", children: [
192
+ /* @__PURE__ */ jsxs(Card, { children: [
193
+ /* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, { className: "text-base", children: "Violator details" }) }),
194
+ /* @__PURE__ */ jsxs(CardContent, { className: "grid gap-4 sm:grid-cols-2", children: [
195
+ /* @__PURE__ */ jsx(TextField, { id: "firstName", label: "First name", value: firstName, onChange: setFirstName, required: true }),
196
+ /* @__PURE__ */ jsx(TextField, { id: "middleName", label: "Middle name", value: middleName, onChange: setMiddleName }),
197
+ /* @__PURE__ */ jsx(TextField, { id: "lastName", label: "Last name", value: lastName, onChange: setLastName, required: true }),
198
+ /* @__PURE__ */ jsx(TextField, { id: "license", label: "License number", value: licenseNumber, onChange: setLicenseNumber, required: true, placeholder: "N03-12-345678" }),
199
+ /* @__PURE__ */ jsx("div", { className: "sm:col-span-2", children: /* @__PURE__ */ jsx(TextField, { id: "street", label: "Address line", value: street, onChange: setStreet, required: true, placeholder: "House no. / Purok / Zone / Street" }) }),
200
+ /* @__PURE__ */ jsx("div", { className: "sm:col-span-2", children: /* @__PURE__ */ jsx(TextField, { id: "barangay", label: "Barangay", value: barangay, onChange: setBarangay, placeholder: "e.g. Amungan" }) }),
201
+ /* @__PURE__ */ jsx(TextField, { id: "city", label: "City / Municipality", value: cityMunicipality, onChange: setCityMunicipality, required: true }),
202
+ /* @__PURE__ */ jsx(TextField, { id: "province", label: "Province", value: province, onChange: setProvince, required: true }),
203
+ /* @__PURE__ */ jsx(TextField, { id: "plate", label: "Plate number", value: plateNumber, onChange: setPlateNumber, placeholder: "ABC 1234" }),
204
+ /* @__PURE__ */ jsx(TextField, { id: "contact", label: "Contact number", value: contactNo, onChange: setContactNo, placeholder: "0917 555 0000" })
205
+ ] })
206
+ ] }),
207
+ /* @__PURE__ */ jsxs(Card, { children: [
208
+ /* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx(CardTitle, { className: "text-base", children: "Apprehension details" }) }),
209
+ /* @__PURE__ */ jsxs(CardContent, { className: "grid gap-4 sm:grid-cols-2", children: [
210
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
211
+ /* @__PURE__ */ jsxs(Label, { htmlFor: "apprehendedAt", children: [
212
+ "Date & time",
213
+ /* @__PURE__ */ jsx("span", { className: "text-destructive", children: " *" })
214
+ ] }),
215
+ /* @__PURE__ */ jsx(
216
+ Input,
217
+ {
218
+ id: "apprehendedAt",
219
+ type: "datetime-local",
220
+ value: apprehendedLocal,
221
+ onChange: (e) => setApprehendedLocal(e.target.value)
222
+ }
223
+ )
224
+ ] }),
225
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
226
+ /* @__PURE__ */ jsxs(Label, { htmlFor: "officer", children: [
227
+ "Apprehending officer",
228
+ /* @__PURE__ */ jsx("span", { className: "text-destructive", children: " *" })
229
+ ] }),
230
+ /* @__PURE__ */ jsx(
231
+ "select",
232
+ {
233
+ id: "officer",
234
+ value: officerId,
235
+ onChange: (e) => setOfficerId(e.target.value),
236
+ className: fieldClass,
237
+ children: officers.map((o) => /* @__PURE__ */ jsxs("option", { value: o.id, children: [
238
+ o.name,
239
+ o.badgeNo ? ` (${o.badgeNo})` : ""
240
+ ] }, o.id))
241
+ }
242
+ )
243
+ ] }),
244
+ /* @__PURE__ */ jsx("div", { className: "sm:col-span-2", children: /* @__PURE__ */ jsx(TextField, { id: "place", label: "Place of violation", value: placeOfViolation, onChange: setPlaceOfViolation, placeholder: `Street / landmark, ${municipality.shortName}` }) })
245
+ ] })
246
+ ] }),
247
+ /* @__PURE__ */ jsxs(Card, { children: [
248
+ /* @__PURE__ */ jsxs(CardHeader, { children: [
249
+ /* @__PURE__ */ jsx(CardTitle, { className: "text-base", children: "Violations" }),
250
+ /* @__PURE__ */ jsx(CardDescription, { children: "Tick each violation and add details. Fines total automatically." })
251
+ ] }),
252
+ /* @__PURE__ */ jsxs(CardContent, { className: "space-y-4", children: [
253
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
254
+ /* @__PURE__ */ jsx("p", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: "Traffic" }),
255
+ /* @__PURE__ */ jsx("div", { className: "space-y-2", children: traffic.map((c) => /* @__PURE__ */ jsx(
256
+ ViolationRow,
257
+ {
258
+ item: c,
259
+ checked: c.code in selected,
260
+ details: selected[c.code] ?? "",
261
+ onToggle: (v) => toggle(c.code, v),
262
+ onDetails: (v) => setSelected((p) => ({ ...p, [c.code]: v })),
263
+ formatPeso
264
+ },
265
+ c.code
266
+ )) })
267
+ ] }),
268
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
269
+ /* @__PURE__ */ jsx("p", { className: "text-xs font-medium uppercase tracking-wide text-muted-foreground", children: "Ordinance" }),
270
+ /* @__PURE__ */ jsx("div", { className: "space-y-2", children: ordinance.map((c) => /* @__PURE__ */ jsx(
271
+ ViolationRow,
272
+ {
273
+ item: c,
274
+ checked: c.code in selected,
275
+ details: selected[c.code] ?? "",
276
+ onToggle: (v) => toggle(c.code, v),
277
+ onDetails: (v) => setSelected((p) => ({ ...p, [c.code]: v })),
278
+ formatPeso
279
+ },
280
+ c.code
281
+ )) })
282
+ ] })
283
+ ] })
284
+ ] }),
285
+ /* @__PURE__ */ jsxs(Card, { children: [
286
+ /* @__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
+ ) })
296
+ ] })
297
+ ] }),
298
+ /* @__PURE__ */ jsx("div", { className: "lg:col-span-2", children: /* @__PURE__ */ jsxs("div", { className: "space-y-4 lg:sticky lg:top-6", children: [
299
+ mounted ? /* @__PURE__ */ jsx(TicketPreview, { ticket: preview }) : /* @__PURE__ */ jsx(Skeleton, { className: "h-80 w-full rounded-xl" }),
300
+ /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "flex items-center justify-between gap-3", children: [
301
+ /* @__PURE__ */ jsxs("div", { children: [
302
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "Total fines" }),
303
+ /* @__PURE__ */ jsx("p", { className: "text-xl font-semibold tabular-nums", children: formatPeso(total) })
304
+ ] }),
305
+ /* @__PURE__ */ jsxs(
306
+ Button,
307
+ {
308
+ type: "button",
309
+ disabled: !valid,
310
+ onClick: () => {
311
+ setError(null);
312
+ setOpen(true);
313
+ },
314
+ className: "h-10 gap-2 px-4",
315
+ children: [
316
+ /* @__PURE__ */ jsx(FilePlus2, { className: "size-4" }),
317
+ "Review & issue"
318
+ ]
319
+ }
320
+ )
321
+ ] }) }),
322
+ !valid ? /* @__PURE__ */ jsx("p", { className: "text-center text-xs text-muted-foreground", children: "Fill the required fields and select at least one violation." }) : null
323
+ ] }) }),
324
+ /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: setOpen, children: /* @__PURE__ */ jsxs(DialogContent, { children: [
325
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
326
+ /* @__PURE__ */ jsx(DialogTitle, { children: "Confirm & issue ticket" }),
327
+ /* @__PURE__ */ jsxs(DialogDescription, { children: [
328
+ "This issues an official OVR for",
329
+ " ",
330
+ /* @__PURE__ */ jsxs("span", { className: "font-medium text-foreground", children: [
331
+ firstName,
332
+ " ",
333
+ lastName
334
+ ] }),
335
+ " ",
336
+ "with ",
337
+ chosen.length,
338
+ " ",
339
+ chosen.length === 1 ? "violation" : "violations",
340
+ " totaling",
341
+ " ",
342
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: formatPeso(total) }),
343
+ ". It becomes immediately searchable by the citizen."
344
+ ] })
345
+ ] }),
346
+ error ? /* @__PURE__ */ jsxs(Alert, { variant: "destructive", children: [
347
+ /* @__PURE__ */ jsx(TriangleAlert, {}),
348
+ /* @__PURE__ */ jsx(AlertDescription, { children: error })
349
+ ] }) : null,
350
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
351
+ /* @__PURE__ */ jsx(
352
+ DialogClose,
353
+ {
354
+ render: /* @__PURE__ */ jsx(Button, { variant: "outline", disabled: isPending }),
355
+ children: "Cancel"
356
+ }
357
+ ),
358
+ /* @__PURE__ */ jsxs(Button, { onClick: issue, disabled: isPending, className: "gap-2", children: [
359
+ isPending ? /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin" }) : /* @__PURE__ */ jsx(FilePlus2, { className: "size-4" }),
360
+ "Confirm & issue"
361
+ ] })
362
+ ] })
363
+ ] }) })
364
+ ] });
365
+ }
366
+
367
+ export { IssuanceForm };
@@ -0,0 +1,10 @@
1
+ import * as React$1 from 'react';
2
+
3
+ declare function StatCard({ label, value, icon, accent, }: {
4
+ label: string;
5
+ value: string | number;
6
+ icon: React.ReactNode;
7
+ accent?: string;
8
+ }): React$1.JSX.Element;
9
+
10
+ export { StatCard };
@@ -0,0 +1,29 @@
1
+ import { Card, CardContent } from '../chunk-SETIN6XP.js';
2
+ import { cn } from '../chunk-77QBZC7J.js';
3
+ import { jsx, jsxs } from 'react/jsx-runtime';
4
+
5
+ function StatCard({
6
+ label,
7
+ value,
8
+ icon,
9
+ accent = "bg-muted text-muted-foreground"
10
+ }) {
11
+ return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "flex items-center gap-3", children: [
12
+ /* @__PURE__ */ jsx(
13
+ "div",
14
+ {
15
+ className: cn(
16
+ "flex size-10 shrink-0 items-center justify-center rounded-lg",
17
+ accent
18
+ ),
19
+ children: icon
20
+ }
21
+ ),
22
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
23
+ /* @__PURE__ */ jsx("p", { className: "truncate text-2xl font-semibold tabular-nums", children: value }),
24
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: label })
25
+ ] })
26
+ ] }) });
27
+ }
28
+
29
+ export { StatCard };
@@ -0,0 +1,9 @@
1
+ import * as React from 'react';
2
+ import { Ticket } from '../types.js';
3
+
4
+ /** A live, miniature Order of Payment — exactly what the citizen will later see. */
5
+ declare function TicketPreview({ ticket }: {
6
+ ticket: Ticket;
7
+ }): React.JSX.Element;
8
+
9
+ export { TicketPreview };
@@ -0,0 +1,13 @@
1
+ "use client";
2
+ export { TicketPreview } from '../chunk-GDOCD7LT.js';
3
+ import '../chunk-JEYT63LE.js';
4
+ import '../chunk-BIQ2J75Y.js';
5
+ import '../chunk-NSCIBSCW.js';
6
+ import '../chunk-SETIN6XP.js';
7
+ import '../chunk-OE525ZER.js';
8
+ import '../chunk-OWCGEEAZ.js';
9
+ import '../chunk-55FQP2DO.js';
10
+ import '../chunk-3KIDW4LT.js';
11
+ import '../chunk-E2D7QT6N.js';
12
+ import '../chunk-77QBZC7J.js';
13
+ import '../chunk-B634JHKZ.js';
@@ -0,0 +1,8 @@
1
+ import * as React from 'react';
2
+ import { Ticket } from '../types.js';
3
+
4
+ declare function TicketsTable({ tickets }: {
5
+ tickets: Ticket[];
6
+ }): React.JSX.Element;
7
+
8
+ export { TicketsTable };
@@ -0,0 +1,50 @@
1
+ "use client";
2
+ import { StatusBadge } from '../chunk-OE525ZER.js';
3
+ import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from '../chunk-OWCGEEAZ.js';
4
+ import '../chunk-55FQP2DO.js';
5
+ import { Money } from '../chunk-3KIDW4LT.js';
6
+ import { useFormatters } from '../chunk-E2D7QT6N.js';
7
+ import '../chunk-77QBZC7J.js';
8
+ import { formalName } from '../chunk-B634JHKZ.js';
9
+ import Link from 'next/link';
10
+ import { ChevronRight } from 'lucide-react';
11
+ import { jsx, jsxs } from 'react/jsx-runtime';
12
+
13
+ function TicketsTable({ tickets }) {
14
+ const { formatDate } = useFormatters();
15
+ if (tickets.length === 0) {
16
+ return /* @__PURE__ */ jsx("div", { className: "px-4 py-10 text-center text-sm text-muted-foreground", children: "No tickets match this view." });
17
+ }
18
+ return /* @__PURE__ */ jsxs(Table, { children: [
19
+ /* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
20
+ /* @__PURE__ */ jsx(TableHead, { children: "Ticket No." }),
21
+ /* @__PURE__ */ jsx(TableHead, { children: "Violator" }),
22
+ /* @__PURE__ */ jsx(TableHead, { className: "hidden sm:table-cell", children: "Apprehended" }),
23
+ /* @__PURE__ */ jsx(TableHead, { children: "Status" }),
24
+ /* @__PURE__ */ jsx(TableHead, { className: "text-right", children: "Amount" }),
25
+ /* @__PURE__ */ jsx(TableHead, { className: "w-8" })
26
+ ] }) }),
27
+ /* @__PURE__ */ jsx(TableBody, { children: tickets.map((t) => {
28
+ const href = `/admin/tickets/${encodeURIComponent(t.ovrTicketNo)}`;
29
+ const amount = t.status === "PAID" ? t.payment?.amount ?? 0 : t.totalAmountDue;
30
+ return /* @__PURE__ */ jsxs(TableRow, { children: [
31
+ /* @__PURE__ */ jsx(TableCell, { className: "font-mono text-xs", children: /* @__PURE__ */ jsx(Link, { href, className: "hover:underline", children: t.ovrTicketNo }) }),
32
+ /* @__PURE__ */ jsx(TableCell, { className: "font-medium", children: formalName(t.violator) }),
33
+ /* @__PURE__ */ jsx(TableCell, { className: "hidden text-muted-foreground sm:table-cell", children: formatDate(t.apprehendedAt) }),
34
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(StatusBadge, { status: t.status }) }),
35
+ /* @__PURE__ */ jsx(TableCell, { className: "text-right", children: /* @__PURE__ */ jsx(Money, { value: amount }) }),
36
+ /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(
37
+ Link,
38
+ {
39
+ href,
40
+ className: "text-muted-foreground hover:text-foreground",
41
+ "aria-label": `Open ${t.ovrTicketNo}`,
42
+ children: /* @__PURE__ */ jsx(ChevronRight, { className: "size-4" })
43
+ }
44
+ ) })
45
+ ] }, t.ovrTicketNo);
46
+ }) })
47
+ ] });
48
+ }
49
+
50
+ export { TicketsTable };
@@ -0,0 +1,5 @@
1
+ import * as React from 'react';
2
+
3
+ declare function CitizenNav(): React.JSX.Element;
4
+
5
+ export { CitizenNav };
@@ -0,0 +1,33 @@
1
+ "use client";
2
+ import { useCopy } from '../chunk-E2D7QT6N.js';
3
+ import { cn } from '../chunk-77QBZC7J.js';
4
+ import '../chunk-B634JHKZ.js';
5
+ import Link from 'next/link';
6
+ import { usePathname } from 'next/navigation';
7
+ import { jsx } from 'react/jsx-runtime';
8
+
9
+ function CitizenNav() {
10
+ const pathname = usePathname();
11
+ const copy = useCopy();
12
+ const items = [
13
+ { href: "/citizen", label: "Home", exact: true },
14
+ { href: "/citizen/search", label: copy.citizen.home.searchCta }
15
+ ];
16
+ return /* @__PURE__ */ jsx("nav", { className: "flex items-center gap-1", children: items.map((it) => {
17
+ const active = it.exact ? pathname === it.href : pathname.startsWith(it.href);
18
+ return /* @__PURE__ */ jsx(
19
+ Link,
20
+ {
21
+ href: it.href,
22
+ className: cn(
23
+ "rounded-md px-2.5 py-1.5 text-sm font-medium transition-colors sm:px-3",
24
+ active ? "bg-white/15 text-gov-foreground" : "text-gov-foreground/80 hover:bg-white/10 hover:text-gov-foreground"
25
+ ),
26
+ children: it.label
27
+ },
28
+ it.href
29
+ );
30
+ }) });
31
+ }
32
+
33
+ export { CitizenNav };
@@ -0,0 +1,14 @@
1
+ import * as React from 'react';
2
+ import { PayAction } from './payment-qr-dialog.js';
3
+
4
+ declare function PaymentForm({ ovrTicketNo, lastName, violatorName, basicFines, penalty, amount, payAction, }: {
5
+ ovrTicketNo: string;
6
+ lastName: string;
7
+ violatorName: string;
8
+ basicFines: number;
9
+ penalty: number;
10
+ amount: number;
11
+ payAction: PayAction;
12
+ }): React.JSX.Element;
13
+
14
+ export { PaymentForm };