@gelabs/ovr 0.4.2 → 0.4.4
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/accounts.d.ts +52 -0
- package/dist/accounts.js +13 -0
- package/dist/audit.d.ts +13 -0
- package/dist/audit.js +11 -0
- package/dist/auth-auth.d.ts +1 -1
- package/dist/auth-rate-limit.d.ts +11 -2
- package/dist/auth-rate-limit.js +1 -1
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +1 -1
- package/dist/chunk-4EDMZRGS.js +98 -0
- package/dist/chunk-4JMHQKMK.js +268 -0
- package/dist/chunk-6662IONX.js +69 -0
- package/dist/chunk-6WMPBAUH.js +18 -0
- package/dist/{chunk-77ULDXQX.js → chunk-6ZJSEM4Y.js} +6 -1
- package/dist/chunk-7GVZZWK3.js +74 -0
- package/dist/chunk-ACXED4UH.js +403 -0
- package/dist/chunk-BBTAG3FD.js +165 -0
- package/dist/chunk-HX7QT2FE.js +452 -0
- package/dist/chunk-LC3S47FM.js +468 -0
- package/dist/chunk-NPTR7GFZ.js +432 -0
- package/dist/chunk-SLQRZBMR.js +66 -0
- package/dist/chunk-UBUXPJLN.js +29 -0
- package/dist/chunk-UN6Z4WGF.js +31 -0
- package/dist/chunk-YE3D2DYY.js +83 -0
- package/dist/citizen.d.ts +5 -0
- package/dist/citizen.js +12 -0
- package/dist/config.d.ts +2 -2
- package/dist/core-i18n.d.ts +1 -1
- package/dist/core.d.ts +2 -2
- package/dist/dashboard.d.ts +10 -0
- package/dist/dashboard.js +3 -0
- package/dist/data-mock-store.d.ts +1 -1
- package/dist/{format-C7MSwUHK.d.ts → format-VyCUfF8R.d.ts} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/notifications.d.ts +6 -0
- package/dist/notifications.js +11 -0
- package/dist/officers.d.ts +27 -0
- package/dist/officers.js +11 -0
- package/dist/offline.d.ts +1 -1
- package/dist/payments.d.ts +15 -0
- package/dist/payments.js +11 -0
- package/dist/roles.d.ts +37 -0
- package/dist/roles.js +12 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/tickets.d.ts +6 -0
- package/dist/tickets.js +24 -0
- package/dist/ui-components-admin/accounts-manager.d.ts +3 -52
- package/dist/ui-components-admin/accounts-manager.js +11 -472
- package/dist/ui-components-admin/admin-nav.js +11 -83
- package/dist/ui-components-admin/issuance-form.js +15 -432
- package/dist/ui-components-admin/logs-viewer.d.ts +3 -13
- package/dist/ui-components-admin/logs-viewer.js +8 -98
- package/dist/ui-components-admin/notifications-list.js +6 -66
- package/dist/ui-components-admin/officers-manager.d.ts +3 -27
- package/dist/ui-components-admin/officers-manager.js +9 -268
- package/dist/ui-components-admin/roles-manager.d.ts +3 -37
- package/dist/ui-components-admin/roles-manager.js +10 -403
- package/dist/ui-components-admin/stat-card.d.ts +2 -10
- package/dist/ui-components-admin/stat-card.js +3 -29
- package/dist/ui-components-admin/ticket-preview.js +2 -2
- package/dist/ui-components-admin/tickets-table.js +7 -69
- package/dist/ui-components-admin/violations-manager.js +13 -452
- package/dist/ui-components-citizen/citizen-nav.js +3 -31
- package/dist/ui-components-citizen/payment-form.d.ts +3 -14
- package/dist/ui-components-citizen/payment-form.js +9 -165
- package/dist/ui-components-citizen/payment-qr-dialog.js +2 -2
- package/dist/ui-components-citizen/ticket-not-found.js +4 -18
- package/dist/ui-components-citizen/violation-history-table.js +6 -74
- package/dist/ui-config.d.ts +2 -2
- package/dist/ui-server.d.ts +2 -2
- package/dist/violations.d.ts +3 -0
- package/dist/violations.js +14 -0
- package/package.json +46 -6
- package/dist/{chunk-ZUMEOZ22.js → chunk-JTSTNZAB.js} +1 -1
- package/dist/{chunk-TLG4C2XI.js → chunk-QCAURREW.js} +1 -1
- package/dist/{schema-CdsFQxIg.d.ts → schema-BUhh_mKX.d.ts} +108 -108
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { UserAccount, Officer, RoleDef } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Account management UI (GE-013). Super admins create & manage who can sign in to
|
|
6
|
+
* the enforcer portal; each account's role decides what it can see/do. The server
|
|
7
|
+
* actions (create / activate / reset password) are INJECTED by the page so this
|
|
8
|
+
* component stays free of any app-specific data wiring — mirrors IssuanceForm.
|
|
9
|
+
*
|
|
10
|
+
* Online-only: account management needs the server (argon2 + DB). After a mutation
|
|
11
|
+
* we refresh the route so the server-rendered list reflects the change.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** What the create form submits — the page hashes the password server-side. */
|
|
15
|
+
interface CreateAccountInput {
|
|
16
|
+
username: string;
|
|
17
|
+
password: string;
|
|
18
|
+
role: string;
|
|
19
|
+
officerId: string | null;
|
|
20
|
+
}
|
|
21
|
+
type CreateAccountAction = (input: CreateAccountInput) => Promise<{
|
|
22
|
+
error?: string;
|
|
23
|
+
} | void>;
|
|
24
|
+
type SetAccountActiveAction = (id: string, active: boolean) => Promise<{
|
|
25
|
+
error?: string;
|
|
26
|
+
} | void>;
|
|
27
|
+
type ResetAccountPasswordAction = (id: string, password: string) => Promise<{
|
|
28
|
+
error?: string;
|
|
29
|
+
} | void>;
|
|
30
|
+
interface EditAccountInput {
|
|
31
|
+
username?: string;
|
|
32
|
+
role?: string;
|
|
33
|
+
officerId?: string | null;
|
|
34
|
+
}
|
|
35
|
+
type EditAccountAction = (id: string, patch: EditAccountInput) => Promise<{
|
|
36
|
+
error?: string;
|
|
37
|
+
} | void>;
|
|
38
|
+
interface AccountsManagerProps {
|
|
39
|
+
users: UserAccount[];
|
|
40
|
+
officers: Officer[];
|
|
41
|
+
/** Roles assignable to accounts (system + custom). */
|
|
42
|
+
roles: RoleDef[];
|
|
43
|
+
/** The signed-in user's id — so they can't deactivate their own account. */
|
|
44
|
+
currentUserId?: string;
|
|
45
|
+
createAction: CreateAccountAction;
|
|
46
|
+
editAction: EditAccountAction;
|
|
47
|
+
setActiveAction: SetAccountActiveAction;
|
|
48
|
+
resetPasswordAction: ResetAccountPasswordAction;
|
|
49
|
+
}
|
|
50
|
+
declare function AccountsManager({ users, officers, roles, currentUserId, createAction, editAction, setActiveAction, resetPasswordAction, }: AccountsManagerProps): React.JSX.Element;
|
|
51
|
+
|
|
52
|
+
export { AccountsManager, type AccountsManagerProps, type CreateAccountAction, type CreateAccountInput, type EditAccountAction, type EditAccountInput, type ResetAccountPasswordAction, type SetAccountActiveAction };
|
package/dist/accounts.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { AccountsManager } from './chunk-LC3S47FM.js';
|
|
2
|
+
import './chunk-XQTVSNHC.js';
|
|
3
|
+
import './chunk-M35R6JLA.js';
|
|
4
|
+
import './chunk-6YFZLXFP.js';
|
|
5
|
+
import './chunk-K3KIBHJF.js';
|
|
6
|
+
import './chunk-QZRRFE6E.js';
|
|
7
|
+
import './chunk-OWCGEEAZ.js';
|
|
8
|
+
import './chunk-55FQP2DO.js';
|
|
9
|
+
import './chunk-I4WDVYHX.js';
|
|
10
|
+
import './chunk-TJSNVTVB.js';
|
|
11
|
+
import './chunk-SETIN6XP.js';
|
|
12
|
+
import './chunk-77QBZC7J.js';
|
|
13
|
+
import './chunk-BI4EGLPG.js';
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ActivityLog } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Activity-log viewer (GE-022). Read-only audit trail with client-side filters
|
|
6
|
+
* (action dropdown + actor/detail search) over the entries fetched by the page.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
declare function LogsViewer({ entries }: {
|
|
10
|
+
entries: ActivityLog[];
|
|
11
|
+
}): React.JSX.Element;
|
|
12
|
+
|
|
13
|
+
export { LogsViewer };
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { LogsViewer } from './chunk-4EDMZRGS.js';
|
|
2
|
+
import './chunk-6YFZLXFP.js';
|
|
3
|
+
import './chunk-K3KIBHJF.js';
|
|
4
|
+
import './chunk-QZRRFE6E.js';
|
|
5
|
+
import './chunk-OWCGEEAZ.js';
|
|
6
|
+
import './chunk-55FQP2DO.js';
|
|
7
|
+
import './chunk-I4WDVYHX.js';
|
|
8
|
+
import './chunk-TJSNVTVB.js';
|
|
9
|
+
import './chunk-SETIN6XP.js';
|
|
10
|
+
import './chunk-77QBZC7J.js';
|
|
11
|
+
import './chunk-BI4EGLPG.js';
|
package/dist/auth-auth.d.ts
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Fixed-window rate limiting backed by Redis. Used to throttle admin login
|
|
3
3
|
* attempts (per username/IP) before the expensive argon2 verify runs.
|
|
4
|
+
*
|
|
5
|
+
* Toggle: set `EOVR_RATE_LIMIT_DISABLED` to a truthy value (`1`/`true`/`yes`/`on`)
|
|
6
|
+
* to bypass throttling entirely — every call is allowed and Redis is never
|
|
7
|
+
* touched. Intended for staging/QA where repeated login attempts must not lock
|
|
8
|
+
* you out. Leave it unset (the default) to keep throttling on in production.
|
|
4
9
|
*/
|
|
5
10
|
|
|
6
11
|
interface RateLimitResult {
|
|
7
12
|
allowed: boolean;
|
|
8
13
|
remaining: number;
|
|
9
14
|
}
|
|
15
|
+
/** Whether rate limiting is switched off via `EOVR_RATE_LIMIT_DISABLED`. */
|
|
16
|
+
declare function isRateLimitDisabled(): boolean;
|
|
10
17
|
/**
|
|
11
18
|
* Increment a counter for `key`; the first hit in a window sets its TTL. Returns
|
|
12
|
-
* `allowed: false` once more than `max` hits occur within `windowSeconds`.
|
|
19
|
+
* `allowed: false` once more than `max` hits occur within `windowSeconds`. When
|
|
20
|
+
* `EOVR_RATE_LIMIT_DISABLED` is truthy this short-circuits to always-allowed and
|
|
21
|
+
* never touches Redis.
|
|
13
22
|
*/
|
|
14
23
|
declare function rateLimit(key: string, max: number, windowSeconds: number): Promise<RateLimitResult>;
|
|
15
24
|
|
|
16
|
-
export { type RateLimitResult, rateLimit };
|
|
25
|
+
export { type RateLimitResult, isRateLimitDisabled, rateLimit };
|
package/dist/auth-rate-limit.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { rateLimit } from './chunk-
|
|
1
|
+
export { isRateLimitDisabled, rateLimit } from './chunk-6ZJSEM4Y.js';
|
|
2
2
|
import './chunk-ASFB24ZY.js';
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { getRedis } from './auth-redis.js';
|
|
2
2
|
export { SESSION_COOKIE, SessionData, cookieSecure, createSession, destroySession, getSession } from './auth-session.js';
|
|
3
|
-
export { RateLimitResult, rateLimit } from './auth-rate-limit.js';
|
|
3
|
+
export { RateLimitResult, isRateLimitDisabled, rateLimit } from './auth-rate-limit.js';
|
|
4
4
|
export { ADMIN_COOKIE, Auth, AuthDeps, createAuth } from './auth-auth.js';
|
|
5
5
|
import 'ioredis';
|
|
6
|
-
import './schema-
|
|
6
|
+
import './schema-BUhh_mKX.js';
|
|
7
7
|
import 'zod';
|
package/dist/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { ADMIN_COOKIE, createAuth } from './chunk-AJ2RZTVX.js';
|
|
2
|
-
export { rateLimit } from './chunk-
|
|
2
|
+
export { isRateLimitDisabled, rateLimit } from './chunk-6ZJSEM4Y.js';
|
|
3
3
|
export { SESSION_COOKIE, cookieSecure, createSession, destroySession, getSession } from './chunk-KIKDXRM5.js';
|
|
4
4
|
export { getRedis } from './chunk-ASFB24ZY.js';
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { usePagination, Pagination } from './chunk-6YFZLXFP.js';
|
|
2
|
+
import { Input } from './chunk-K3KIBHJF.js';
|
|
3
|
+
import { ACTIVITY_ACTION_LABELS } from './chunk-QZRRFE6E.js';
|
|
4
|
+
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from './chunk-OWCGEEAZ.js';
|
|
5
|
+
import { Badge } from './chunk-55FQP2DO.js';
|
|
6
|
+
import { useCopy, useOvrConfig } from './chunk-TJSNVTVB.js';
|
|
7
|
+
import { Card, CardContent } from './chunk-SETIN6XP.js';
|
|
8
|
+
import * as React from 'react';
|
|
9
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
10
|
+
|
|
11
|
+
var fieldClass = "h-9 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 dark:bg-input/30";
|
|
12
|
+
function actionLabel(action) {
|
|
13
|
+
return ACTIVITY_ACTION_LABELS[action] ?? action;
|
|
14
|
+
}
|
|
15
|
+
function LogsViewer({ entries }) {
|
|
16
|
+
const t = useCopy().admin.logsPage;
|
|
17
|
+
const { rules } = useOvrConfig();
|
|
18
|
+
const [action, setAction] = React.useState("");
|
|
19
|
+
const [q, setQ] = React.useState("");
|
|
20
|
+
const fmt = (iso) => new Date(iso).toLocaleString(rules.locale, {
|
|
21
|
+
timeZone: rules.timeZone,
|
|
22
|
+
year: "numeric",
|
|
23
|
+
month: "short",
|
|
24
|
+
day: "2-digit",
|
|
25
|
+
hour: "numeric",
|
|
26
|
+
minute: "2-digit"
|
|
27
|
+
});
|
|
28
|
+
const actions = Array.from(new Set(entries.map((e) => e.action)));
|
|
29
|
+
const query = q.trim().toLowerCase();
|
|
30
|
+
const filtered = entries.filter((e) => {
|
|
31
|
+
if (action && e.action !== action) return false;
|
|
32
|
+
if (query) {
|
|
33
|
+
const hay = `${e.actorUsername ?? ""} ${e.summary}`.toLowerCase();
|
|
34
|
+
if (!hay.includes(query)) return false;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
});
|
|
38
|
+
const { pageItems, page, setPage, totalPages, from, to, total } = usePagination(filtered, 20, `${action}|${query}`);
|
|
39
|
+
return /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-4xl p-4 sm:p-6 lg:p-8", children: [
|
|
40
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-6", children: [
|
|
41
|
+
/* @__PURE__ */ jsx("h1", { className: "font-heading text-2xl font-semibold tracking-tight", children: t.title }),
|
|
42
|
+
/* @__PURE__ */ jsx("p", { className: "max-w-2xl text-sm text-muted-foreground", children: t.subtitle })
|
|
43
|
+
] }),
|
|
44
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-4 flex flex-wrap items-center gap-2", children: [
|
|
45
|
+
/* @__PURE__ */ jsxs(
|
|
46
|
+
"select",
|
|
47
|
+
{
|
|
48
|
+
value: action,
|
|
49
|
+
onChange: (e) => setAction(e.target.value),
|
|
50
|
+
className: `${fieldClass} w-auto`,
|
|
51
|
+
"aria-label": t.action,
|
|
52
|
+
children: [
|
|
53
|
+
/* @__PURE__ */ jsx("option", { value: "", children: t.allActions }),
|
|
54
|
+
actions.map((a) => /* @__PURE__ */ jsx("option", { value: a, children: actionLabel(a) }, a))
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
),
|
|
58
|
+
/* @__PURE__ */ jsx(
|
|
59
|
+
Input,
|
|
60
|
+
{
|
|
61
|
+
value: q,
|
|
62
|
+
onChange: (e) => setQ(e.target.value),
|
|
63
|
+
placeholder: t.search,
|
|
64
|
+
className: "h-9 w-auto min-w-56"
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
] }),
|
|
68
|
+
/* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { className: "p-0", children: filtered.length === 0 ? /* @__PURE__ */ jsx("p", { className: "p-8 text-center text-sm text-muted-foreground", children: t.empty }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
69
|
+
/* @__PURE__ */ jsxs(Table, { children: [
|
|
70
|
+
/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
71
|
+
/* @__PURE__ */ jsx(TableHead, { children: t.when }),
|
|
72
|
+
/* @__PURE__ */ jsx(TableHead, { children: t.actor }),
|
|
73
|
+
/* @__PURE__ */ jsx(TableHead, { children: t.action }),
|
|
74
|
+
/* @__PURE__ */ jsx(TableHead, { children: t.detail })
|
|
75
|
+
] }) }),
|
|
76
|
+
/* @__PURE__ */ jsx(TableBody, { children: pageItems.map((e) => /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
77
|
+
/* @__PURE__ */ jsx(TableCell, { className: "whitespace-nowrap text-muted-foreground", children: fmt(e.createdAt) }),
|
|
78
|
+
/* @__PURE__ */ jsx(TableCell, { className: "font-medium", children: e.actorUsername ?? t.system }),
|
|
79
|
+
/* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(Badge, { variant: "secondary", children: actionLabel(e.action) }) }),
|
|
80
|
+
/* @__PURE__ */ jsx(TableCell, { className: "text-muted-foreground", children: e.summary })
|
|
81
|
+
] }, e.id)) })
|
|
82
|
+
] }),
|
|
83
|
+
/* @__PURE__ */ jsx(
|
|
84
|
+
Pagination,
|
|
85
|
+
{
|
|
86
|
+
page,
|
|
87
|
+
totalPages,
|
|
88
|
+
from,
|
|
89
|
+
to,
|
|
90
|
+
total,
|
|
91
|
+
onPage: setPage
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
] }) }) })
|
|
95
|
+
] });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { LogsViewer };
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { Label } from './chunk-XQTVSNHC.js';
|
|
2
|
+
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from './chunk-M35R6JLA.js';
|
|
3
|
+
import { usePagination, Pagination } from './chunk-6YFZLXFP.js';
|
|
4
|
+
import { Input } from './chunk-K3KIBHJF.js';
|
|
5
|
+
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from './chunk-OWCGEEAZ.js';
|
|
6
|
+
import { Button } from './chunk-I4WDVYHX.js';
|
|
7
|
+
import { useCopy } from './chunk-TJSNVTVB.js';
|
|
8
|
+
import { Card, CardContent } from './chunk-SETIN6XP.js';
|
|
9
|
+
import * as React from 'react';
|
|
10
|
+
import { toast } from 'sonner';
|
|
11
|
+
import { useRouter } from 'next/navigation';
|
|
12
|
+
import { UserPlus, Pencil, Loader2, Trash2 } from 'lucide-react';
|
|
13
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
14
|
+
|
|
15
|
+
function OfficersManager({
|
|
16
|
+
officers,
|
|
17
|
+
createAction,
|
|
18
|
+
updateAction,
|
|
19
|
+
deleteAction
|
|
20
|
+
}) {
|
|
21
|
+
const t = useCopy().admin.officersPage;
|
|
22
|
+
const router = useRouter();
|
|
23
|
+
const { pageItems, page, setPage, totalPages, from, to, total } = usePagination(officers, 12);
|
|
24
|
+
return /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-4xl p-4 sm:p-6 lg:p-8", children: [
|
|
25
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-6 flex items-center justify-between gap-3", children: [
|
|
26
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
27
|
+
/* @__PURE__ */ jsx("h1", { className: "font-heading text-2xl font-semibold tracking-tight", children: t.title }),
|
|
28
|
+
/* @__PURE__ */ jsx("p", { className: "max-w-2xl text-sm text-muted-foreground", children: t.subtitle })
|
|
29
|
+
] }),
|
|
30
|
+
/* @__PURE__ */ jsx(
|
|
31
|
+
OfficerDialog,
|
|
32
|
+
{
|
|
33
|
+
mode: "create",
|
|
34
|
+
createAction,
|
|
35
|
+
onDone: () => router.refresh()
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
] }),
|
|
39
|
+
/* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { className: "p-0", children: officers.length === 0 ? /* @__PURE__ */ jsx("p", { className: "p-8 text-center text-sm text-muted-foreground", children: t.empty }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
40
|
+
/* @__PURE__ */ jsxs(Table, { children: [
|
|
41
|
+
/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
42
|
+
/* @__PURE__ */ jsx(TableHead, { children: t.name }),
|
|
43
|
+
/* @__PURE__ */ jsx(TableHead, { children: t.badge }),
|
|
44
|
+
/* @__PURE__ */ jsx(TableHead, { children: t.office }),
|
|
45
|
+
/* @__PURE__ */ jsx(TableHead, { className: "text-right", children: t.actions })
|
|
46
|
+
] }) }),
|
|
47
|
+
/* @__PURE__ */ jsx(TableBody, { children: pageItems.map((o) => /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
48
|
+
/* @__PURE__ */ jsx(TableCell, { className: "font-medium", children: o.name }),
|
|
49
|
+
/* @__PURE__ */ jsx(TableCell, { className: "text-muted-foreground", children: o.badgeNo ?? "\u2014" }),
|
|
50
|
+
/* @__PURE__ */ jsx(TableCell, { className: "text-muted-foreground", children: o.office }),
|
|
51
|
+
/* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-1.5", children: [
|
|
52
|
+
/* @__PURE__ */ jsx(
|
|
53
|
+
OfficerDialog,
|
|
54
|
+
{
|
|
55
|
+
mode: "edit",
|
|
56
|
+
officer: o,
|
|
57
|
+
updateAction,
|
|
58
|
+
onDone: () => router.refresh()
|
|
59
|
+
}
|
|
60
|
+
),
|
|
61
|
+
/* @__PURE__ */ jsx(
|
|
62
|
+
DeleteOfficerButton,
|
|
63
|
+
{
|
|
64
|
+
officer: o,
|
|
65
|
+
deleteAction,
|
|
66
|
+
onDone: () => router.refresh()
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
] }) })
|
|
70
|
+
] }, o.id)) })
|
|
71
|
+
] }),
|
|
72
|
+
/* @__PURE__ */ jsx(
|
|
73
|
+
Pagination,
|
|
74
|
+
{
|
|
75
|
+
page,
|
|
76
|
+
totalPages,
|
|
77
|
+
from,
|
|
78
|
+
to,
|
|
79
|
+
total,
|
|
80
|
+
onPage: setPage
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
] }) }) })
|
|
84
|
+
] });
|
|
85
|
+
}
|
|
86
|
+
function OfficerDialog({
|
|
87
|
+
mode,
|
|
88
|
+
officer,
|
|
89
|
+
createAction,
|
|
90
|
+
updateAction,
|
|
91
|
+
onDone
|
|
92
|
+
}) {
|
|
93
|
+
const t = useCopy().admin.officersPage;
|
|
94
|
+
const [open, setOpen] = React.useState(false);
|
|
95
|
+
const [name, setName] = React.useState(officer?.name ?? "");
|
|
96
|
+
const [badgeNo, setBadgeNo] = React.useState(officer?.badgeNo ?? "");
|
|
97
|
+
const [office, setOffice] = React.useState(officer?.office ?? "");
|
|
98
|
+
const [submitting, setSubmitting] = React.useState(false);
|
|
99
|
+
function resetTo(o) {
|
|
100
|
+
setName(o?.name ?? "");
|
|
101
|
+
setBadgeNo(o?.badgeNo ?? "");
|
|
102
|
+
setOffice(o?.office ?? "");
|
|
103
|
+
}
|
|
104
|
+
async function submit(e) {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
if (!name.trim()) return toast.error(`${t.name} is required.`);
|
|
107
|
+
if (!office.trim()) return toast.error(`${t.office} is required.`);
|
|
108
|
+
setSubmitting(true);
|
|
109
|
+
try {
|
|
110
|
+
const input = {
|
|
111
|
+
name: name.trim(),
|
|
112
|
+
badgeNo: badgeNo.trim() || void 0,
|
|
113
|
+
office: office.trim()
|
|
114
|
+
};
|
|
115
|
+
const res = mode === "create" ? await createAction?.(input) : await updateAction?.(officer.id, input);
|
|
116
|
+
if (res?.error) {
|
|
117
|
+
toast.error(res.error);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
toast.success(
|
|
121
|
+
`${mode === "create" ? t.create : t.editTitle}: ${name.trim()}`
|
|
122
|
+
);
|
|
123
|
+
setOpen(false);
|
|
124
|
+
onDone();
|
|
125
|
+
} finally {
|
|
126
|
+
setSubmitting(false);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return /* @__PURE__ */ jsxs(
|
|
130
|
+
Dialog,
|
|
131
|
+
{
|
|
132
|
+
open,
|
|
133
|
+
onOpenChange: (o) => {
|
|
134
|
+
if (o) resetTo(officer);
|
|
135
|
+
setOpen(o);
|
|
136
|
+
},
|
|
137
|
+
children: [
|
|
138
|
+
/* @__PURE__ */ jsxs(
|
|
139
|
+
DialogTrigger,
|
|
140
|
+
{
|
|
141
|
+
render: mode === "create" ? /* @__PURE__ */ jsx(Button, { className: "gap-1.5" }) : /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", className: "gap-1.5" }),
|
|
142
|
+
children: [
|
|
143
|
+
mode === "create" ? /* @__PURE__ */ jsx(UserPlus, { className: "size-4" }) : /* @__PURE__ */ jsx(Pencil, { className: "size-3.5" }),
|
|
144
|
+
/* @__PURE__ */ jsx("span", { className: mode === "create" ? "" : "hidden sm:inline", children: mode === "create" ? t.newOfficer : t.edit })
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
),
|
|
148
|
+
/* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-md", children: [
|
|
149
|
+
/* @__PURE__ */ jsxs(DialogHeader, { children: [
|
|
150
|
+
/* @__PURE__ */ jsx(DialogTitle, { children: mode === "create" ? t.newOfficer : t.editTitle }),
|
|
151
|
+
/* @__PURE__ */ jsx(DialogDescription, { children: t.subtitle })
|
|
152
|
+
] }),
|
|
153
|
+
/* @__PURE__ */ jsxs("form", { onSubmit: submit, className: "space-y-4", children: [
|
|
154
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
155
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "off-name", children: t.name }),
|
|
156
|
+
/* @__PURE__ */ jsx(
|
|
157
|
+
Input,
|
|
158
|
+
{
|
|
159
|
+
id: "off-name",
|
|
160
|
+
value: name,
|
|
161
|
+
onChange: (e) => setName(e.target.value),
|
|
162
|
+
placeholder: t.namePlaceholder,
|
|
163
|
+
autoComplete: "off",
|
|
164
|
+
autoFocus: true
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
] }),
|
|
168
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-4 sm:grid-cols-2", children: [
|
|
169
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
170
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "off-badge", children: t.badge }),
|
|
171
|
+
/* @__PURE__ */ jsx(
|
|
172
|
+
Input,
|
|
173
|
+
{
|
|
174
|
+
id: "off-badge",
|
|
175
|
+
value: badgeNo,
|
|
176
|
+
onChange: (e) => setBadgeNo(e.target.value),
|
|
177
|
+
placeholder: t.badgePlaceholder,
|
|
178
|
+
autoComplete: "off"
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
] }),
|
|
182
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
183
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: "off-office", children: t.office }),
|
|
184
|
+
/* @__PURE__ */ jsx(
|
|
185
|
+
Input,
|
|
186
|
+
{
|
|
187
|
+
id: "off-office",
|
|
188
|
+
value: office,
|
|
189
|
+
onChange: (e) => setOffice(e.target.value),
|
|
190
|
+
placeholder: t.officePlaceholder,
|
|
191
|
+
autoComplete: "off"
|
|
192
|
+
}
|
|
193
|
+
)
|
|
194
|
+
] })
|
|
195
|
+
] }),
|
|
196
|
+
/* @__PURE__ */ jsxs(DialogFooter, { children: [
|
|
197
|
+
/* @__PURE__ */ jsx(DialogClose, { render: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline" }), children: t.cancel }),
|
|
198
|
+
/* @__PURE__ */ jsxs(Button, { type: "submit", disabled: submitting, className: "gap-1.5", children: [
|
|
199
|
+
submitting ? /* @__PURE__ */ jsx(Loader2, { className: "size-4 animate-spin" }) : null,
|
|
200
|
+
submitting ? mode === "create" ? t.creating : t.saving : mode === "create" ? t.create : t.save
|
|
201
|
+
] })
|
|
202
|
+
] })
|
|
203
|
+
] })
|
|
204
|
+
] })
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
function DeleteOfficerButton({
|
|
210
|
+
officer,
|
|
211
|
+
deleteAction,
|
|
212
|
+
onDone
|
|
213
|
+
}) {
|
|
214
|
+
const t = useCopy().admin.officersPage;
|
|
215
|
+
const [deleting, setDeleting] = React.useState(false);
|
|
216
|
+
async function remove() {
|
|
217
|
+
setDeleting(true);
|
|
218
|
+
try {
|
|
219
|
+
const res = await deleteAction(officer.id);
|
|
220
|
+
if (res?.error) {
|
|
221
|
+
toast.error(res.error);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
toast.success(`${t.delete}: ${officer.name}`);
|
|
225
|
+
onDone();
|
|
226
|
+
} finally {
|
|
227
|
+
setDeleting(false);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return /* @__PURE__ */ jsxs(Dialog, { children: [
|
|
231
|
+
/* @__PURE__ */ jsxs(
|
|
232
|
+
DialogTrigger,
|
|
233
|
+
{
|
|
234
|
+
render: /* @__PURE__ */ jsx(Button, { variant: "destructive", size: "sm", className: "gap-1.5" }),
|
|
235
|
+
children: [
|
|
236
|
+
/* @__PURE__ */ jsx(Trash2, { className: "size-3.5" }),
|
|
237
|
+
/* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: t.delete })
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
),
|
|
241
|
+
/* @__PURE__ */ jsxs(DialogContent, { className: "sm:max-w-sm", children: [
|
|
242
|
+
/* @__PURE__ */ jsxs(DialogHeader, { children: [
|
|
243
|
+
/* @__PURE__ */ jsx(DialogTitle, { children: t.deleteConfirmTitle }),
|
|
244
|
+
/* @__PURE__ */ jsxs(DialogDescription, { children: [
|
|
245
|
+
officer.name,
|
|
246
|
+
" \u2014 ",
|
|
247
|
+
t.deleteConfirmBody
|
|
248
|
+
] })
|
|
249
|
+
] }),
|
|
250
|
+
/* @__PURE__ */ jsxs(DialogFooter, { children: [
|
|
251
|
+
/* @__PURE__ */ jsx(DialogClose, { render: /* @__PURE__ */ jsx(Button, { variant: "outline" }), children: t.cancel }),
|
|
252
|
+
/* @__PURE__ */ jsxs(
|
|
253
|
+
DialogClose,
|
|
254
|
+
{
|
|
255
|
+
render: /* @__PURE__ */ jsx(Button, { variant: "destructive", className: "gap-2", disabled: deleting }),
|
|
256
|
+
onClick: remove,
|
|
257
|
+
children: [
|
|
258
|
+
/* @__PURE__ */ jsx(Trash2, { className: "size-4" }),
|
|
259
|
+
t.delete
|
|
260
|
+
]
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
] })
|
|
264
|
+
] })
|
|
265
|
+
] });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export { OfficersManager };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { usePagination, Pagination } from './chunk-6YFZLXFP.js';
|
|
2
|
+
import { StatusBadge } from './chunk-OE525ZER.js';
|
|
3
|
+
import { Money } from './chunk-BVI5XDDA.js';
|
|
4
|
+
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from './chunk-OWCGEEAZ.js';
|
|
5
|
+
import { useFormatters } from './chunk-TJSNVTVB.js';
|
|
6
|
+
import { formalName } from './chunk-BI4EGLPG.js';
|
|
7
|
+
import { useRouter } from 'next/navigation';
|
|
8
|
+
import { ChevronRight } from 'lucide-react';
|
|
9
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
10
|
+
|
|
11
|
+
function TicketsTable({ tickets }) {
|
|
12
|
+
const { formatDate } = useFormatters();
|
|
13
|
+
const router = useRouter();
|
|
14
|
+
const { pageItems, page, setPage, totalPages, from, to, total } = usePagination(tickets, 15);
|
|
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(Fragment, { children: [
|
|
19
|
+
/* @__PURE__ */ jsxs(Table, { children: [
|
|
20
|
+
/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
21
|
+
/* @__PURE__ */ jsx(TableHead, { children: "Ticket No." }),
|
|
22
|
+
/* @__PURE__ */ jsx(TableHead, { children: "Violator" }),
|
|
23
|
+
/* @__PURE__ */ jsx(TableHead, { className: "hidden sm:table-cell", children: "Apprehended" }),
|
|
24
|
+
/* @__PURE__ */ jsx(TableHead, { children: "Status" }),
|
|
25
|
+
/* @__PURE__ */ jsx(TableHead, { className: "text-right", children: "Amount" }),
|
|
26
|
+
/* @__PURE__ */ jsx(TableHead, { className: "w-8" })
|
|
27
|
+
] }) }),
|
|
28
|
+
/* @__PURE__ */ jsx(TableBody, { children: pageItems.map((t) => {
|
|
29
|
+
const href = `/admin/tickets/${encodeURIComponent(t.ovrTicketNo)}`;
|
|
30
|
+
const amount = t.status === "PAID" ? t.payment?.amount ?? 0 : t.totalAmountDue;
|
|
31
|
+
return /* @__PURE__ */ jsxs(
|
|
32
|
+
TableRow,
|
|
33
|
+
{
|
|
34
|
+
onClick: () => router.push(href),
|
|
35
|
+
onKeyDown: (e) => {
|
|
36
|
+
if (e.key === "Enter") router.push(href);
|
|
37
|
+
},
|
|
38
|
+
role: "link",
|
|
39
|
+
tabIndex: 0,
|
|
40
|
+
"aria-label": `Open ticket ${t.ovrTicketNo}`,
|
|
41
|
+
className: "cursor-pointer",
|
|
42
|
+
children: [
|
|
43
|
+
/* @__PURE__ */ jsx(TableCell, { className: "font-mono text-xs", children: t.ovrTicketNo }),
|
|
44
|
+
/* @__PURE__ */ jsx(TableCell, { className: "font-medium", children: formalName(t.violator) }),
|
|
45
|
+
/* @__PURE__ */ jsx(TableCell, { className: "hidden text-muted-foreground sm:table-cell", children: formatDate(t.apprehendedAt) }),
|
|
46
|
+
/* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(StatusBadge, { status: t.status }) }),
|
|
47
|
+
/* @__PURE__ */ jsx(TableCell, { className: "text-right", children: /* @__PURE__ */ jsx(Money, { value: amount }) }),
|
|
48
|
+
/* @__PURE__ */ jsx(TableCell, { className: "text-muted-foreground", children: /* @__PURE__ */ jsx(ChevronRight, { className: "size-4" }) })
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
t.ovrTicketNo
|
|
52
|
+
);
|
|
53
|
+
}) })
|
|
54
|
+
] }),
|
|
55
|
+
/* @__PURE__ */ jsx(
|
|
56
|
+
Pagination,
|
|
57
|
+
{
|
|
58
|
+
page,
|
|
59
|
+
totalPages,
|
|
60
|
+
from,
|
|
61
|
+
to,
|
|
62
|
+
total,
|
|
63
|
+
onPage: setPage
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
] });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { TicketsTable };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { buttonVariants } from './chunk-I4WDVYHX.js';
|
|
2
|
+
import { useCopy } from './chunk-TJSNVTVB.js';
|
|
3
|
+
import { Card, CardContent } from './chunk-SETIN6XP.js';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { FileQuestion } from 'lucide-react';
|
|
6
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
7
|
+
|
|
8
|
+
function TicketNotFound() {
|
|
9
|
+
const copy = useCopy();
|
|
10
|
+
return /* @__PURE__ */ jsx("div", { className: "mx-auto w-full max-w-lg px-4 py-16 text-center", children: /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "flex flex-col items-center gap-3 py-8", children: [
|
|
11
|
+
/* @__PURE__ */ jsx("div", { className: "flex size-12 items-center justify-center rounded-full bg-muted text-muted-foreground", children: /* @__PURE__ */ jsx(FileQuestion, { className: "size-6" }) }),
|
|
12
|
+
/* @__PURE__ */ jsx("h1", { className: "font-heading text-lg font-semibold", children: copy.citizen.notFoundTitle }),
|
|
13
|
+
/* @__PURE__ */ jsx("p", { className: "max-w-sm text-sm text-muted-foreground", children: copy.citizen.notFoundBody }),
|
|
14
|
+
/* @__PURE__ */ jsx(Link, { href: "/citizen/search", className: buttonVariants({}), children: copy.citizen.ticket.backToSearch })
|
|
15
|
+
] }) }) });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { TicketNotFound };
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { getRedis } from './chunk-ASFB24ZY.js';
|
|
2
2
|
import 'server-only';
|
|
3
3
|
|
|
4
|
+
function isRateLimitDisabled() {
|
|
5
|
+
const v = process.env.EOVR_RATE_LIMIT_DISABLED?.trim().toLowerCase();
|
|
6
|
+
return v === "1" || v === "true" || v === "yes" || v === "on";
|
|
7
|
+
}
|
|
4
8
|
async function rateLimit(key, max, windowSeconds) {
|
|
9
|
+
if (isRateLimitDisabled()) return { allowed: true, remaining: max };
|
|
5
10
|
const redis = getRedis();
|
|
6
11
|
const k = `eovr:rl:${key}`;
|
|
7
12
|
const count = await redis.incr(k);
|
|
@@ -9,4 +14,4 @@ async function rateLimit(key, max, windowSeconds) {
|
|
|
9
14
|
return { allowed: count <= max, remaining: Math.max(0, max - count) };
|
|
10
15
|
}
|
|
11
16
|
|
|
12
|
-
export { rateLimit };
|
|
17
|
+
export { isRateLimitDisabled, rateLimit };
|