@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.
- package/dist/chunk-3KIDW4LT.js +13 -0
- package/dist/chunk-4SZXBT56.js +26 -0
- package/dist/chunk-55FQP2DO.js +46 -0
- package/dist/{chunk-JV7QQ22Q.js → chunk-5YYR37CF.js} +10 -1
- package/dist/chunk-5Z2IAD5I.js +72 -0
- package/dist/chunk-BBQBKQA4.js +31 -0
- package/dist/chunk-BIQ2J75Y.js +54 -0
- package/dist/chunk-E2D7QT6N.js +92 -0
- package/dist/chunk-EGKFELO3.js +15 -0
- package/dist/chunk-EYFZWQ4J.js +74 -0
- package/dist/chunk-GDOCD7LT.js +46 -0
- package/dist/chunk-IF5UAVIE.js +25 -0
- package/dist/chunk-JEYT63LE.js +25 -0
- package/dist/chunk-K3KIBHJF.js +20 -0
- package/dist/chunk-M35R6JLA.js +142 -0
- package/dist/chunk-MDTRBOPQ.js +22 -0
- package/dist/chunk-NSCIBSCW.js +24 -0
- package/dist/chunk-OE525ZER.js +31 -0
- package/dist/chunk-OWCGEEAZ.js +107 -0
- package/dist/chunk-QCRVT2SS.js +18 -0
- package/dist/chunk-SETIN6XP.js +95 -0
- package/dist/chunk-WOPU6DI7.js +77 -0
- package/dist/chunk-XQTVSNHC.js +18 -0
- package/dist/core-i18n.d.ts +2 -2
- package/dist/core-i18n.js +1 -1
- package/dist/data-db.d.ts +1 -4
- package/dist/data-mock-store.js +53 -1
- package/dist/data-prisma-store.d.ts +3 -1
- package/dist/data-prisma-store.js +84 -1
- package/dist/data-seed-runner.d.ts +2 -1
- package/dist/data.d.ts +13 -1
- package/dist/offline.d.ts +126 -0
- package/dist/offline.js +376 -0
- package/dist/{types-DrrNO_Ak.d.ts → types-CtBC5-TW.d.ts} +8 -0
- package/dist/ui-components-admin/admin-nav.d.ts +7 -0
- package/dist/ui-components-admin/admin-nav.js +83 -0
- package/dist/ui-components-admin/issuance-form.d.ts +15 -0
- package/dist/ui-components-admin/issuance-form.js +367 -0
- package/dist/ui-components-admin/stat-card.d.ts +10 -0
- package/dist/ui-components-admin/stat-card.js +29 -0
- package/dist/ui-components-admin/ticket-preview.d.ts +9 -0
- package/dist/ui-components-admin/ticket-preview.js +13 -0
- package/dist/ui-components-admin/tickets-table.d.ts +8 -0
- package/dist/ui-components-admin/tickets-table.js +50 -0
- package/dist/ui-components-citizen/citizen-nav.d.ts +5 -0
- package/dist/ui-components-citizen/citizen-nav.js +33 -0
- package/dist/ui-components-citizen/payment-form.d.ts +14 -0
- package/dist/ui-components-citizen/payment-form.js +168 -0
- package/dist/ui-components-citizen/payment-qr-dialog.d.ts +30 -0
- package/dist/ui-components-citizen/payment-qr-dialog.js +9 -0
- package/dist/ui-components-citizen/ticket-not-found.d.ts +5 -0
- package/dist/ui-components-citizen/ticket-not-found.js +21 -0
- package/dist/ui-components-citizen/violation-history-table.d.ts +16 -0
- package/dist/ui-components-citizen/violation-history-table.js +77 -0
- package/dist/ui-components-shared/amount-summary.d.ts +8 -0
- package/dist/ui-components-shared/amount-summary.js +7 -0
- package/dist/ui-components-shared/money.d.ts +9 -0
- package/dist/ui-components-shared/money.js +5 -0
- package/dist/ui-components-shared/municipal-seal.d.ts +11 -0
- package/dist/ui-components-shared/municipal-seal.js +6 -0
- package/dist/ui-components-shared/official-header.d.ts +5 -0
- package/dist/ui-components-shared/official-header.js +7 -0
- package/dist/ui-components-shared/print-button.d.ts +8 -0
- package/dist/ui-components-shared/print-button.js +17 -0
- package/dist/ui-components-shared/seal.d.ts +12 -0
- package/dist/ui-components-shared/seal.js +2 -0
- package/dist/ui-components-shared/site-header.d.ts +13 -0
- package/dist/ui-components-shared/site-header.js +40 -0
- package/dist/ui-components-shared/sonner.d.ts +6 -0
- package/dist/ui-components-shared/sonner.js +38 -0
- package/dist/ui-components-shared/status-badge.d.ts +9 -0
- package/dist/ui-components-shared/status-badge.js +3 -0
- package/dist/ui-components-shared/theme-toggle.d.ts +7 -0
- package/dist/ui-components-shared/theme-toggle.js +6 -0
- package/dist/ui-components-shared/ticket-receipt.d.ts +12 -0
- package/dist/ui-components-shared/ticket-receipt.js +158 -0
- package/dist/ui-components-shared/violations-table.d.ts +8 -0
- package/dist/ui-components-shared/violations-table.js +7 -0
- package/dist/ui-components-ui/alert.js +2 -74
- package/dist/ui-components-ui/badge.d.ts +1 -1
- package/dist/ui-components-ui/badge.js +2 -46
- package/dist/ui-components-ui/button.d.ts +2 -2
- package/dist/ui-components-ui/card.js +2 -95
- package/dist/ui-components-ui/checkbox.js +2 -31
- package/dist/ui-components-ui/dialog.js +3 -142
- package/dist/ui-components-ui/input.js +2 -20
- package/dist/ui-components-ui/label.js +2 -18
- package/dist/ui-components-ui/separator.js +2 -24
- package/dist/ui-components-ui/skeleton.js +2 -15
- package/dist/ui-components-ui/table.js +2 -107
- package/dist/ui-components-ui/textarea.js +2 -18
- package/dist/ui-config.d.ts +19 -2
- package/dist/ui-config.js +2 -36
- package/dist/ui-server.d.ts +1 -1
- package/dist/ui-server.js +1 -1
- package/package.json +24 -4
- package/prisma/migrations/20260611112111_init/migration.sql +142 -0
- package/prisma/migrations/20260611112121_add_ticket_sequence/migration.sql +4 -0
- package/prisma/migrations/20260612000000_split_violator_address/migration.sql +26 -0
- package/prisma/migrations/20260612000100_barangay_optional/migration.sql +2 -0
- 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,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,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,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 };
|