permits: admin CRUD (route + UI)

A permit is an aggregate (row + credentials + bound plates); create/update
treat it as one unit (child sets replaced on update). GET /api/permits (any
signed-in role, for lookup); POST/PUT/DELETE + POST /:id/revoke (admin only).
Validation: maxConcurrent positive-int-or-null (unbound); a permit must have at
least one credential OR one bound plate. Revoke is the soft common case (keeps
history, barred at the barrier); DELETE hard-removes — past ledger events that
reference it are untouched (append-only audit trail, independent of this row).

Web PermitManager in the admin shell: list + add/edit (holder, car-bound toggle,
validity, credentials, plates), revoke, delete. Makes permits usable without
hand-seeding (companion to the tariff composer).

Verified via inject: validation (empty / maxConcurrent=0 -> 400), create -> 201,
operator can LIST but not write (403), update replaces child rows, revoke ->
revoked, delete -> 204 then 404 with children cleaned.
This commit is contained in:
2026-06-15 19:53:03 +02:00
parent c24d99b0f4
commit 3429642edb
7 changed files with 409 additions and 0 deletions
+37
View File
@@ -240,3 +240,40 @@ export function publishTariffVersion(body: {
}): Promise<TariffVersion> {
return apiFetch("/api/tariff/versions", { method: "POST", body: JSON.stringify(body) });
}
// --- Permits --------------------------------------------------------------
export interface PermitCredential {
kind: "rf" | "qr";
value: string;
}
export interface Permit {
id: string;
holderName: string | null;
contact: string | null;
maxConcurrent: number | null;
validFrom: string | null;
validTo: string | null;
status: "active" | "suspended" | "revoked";
credentials: PermitCredential[];
plates: string[];
}
export type PermitInput = Omit<Permit, "id" | "status"> & {
status?: Permit["status"];
};
export function fetchPermits(): Promise<{ permits: Permit[] }> {
return apiFetch("/api/permits");
}
export function createPermit(body: PermitInput): Promise<Permit> {
return apiFetch("/api/permits", { method: "POST", body: JSON.stringify(body) });
}
export function updatePermit(id: string, body: PermitInput): Promise<Permit> {
return apiFetch(`/api/permits/${id}`, { method: "PUT", body: JSON.stringify(body) });
}
export function revokePermit(id: string): Promise<Permit> {
return apiFetch(`/api/permits/${id}/revoke`, { method: "POST" });
}
export function deletePermit(id: string): Promise<void> {
return apiFetch(`/api/permits/${id}`, { method: "DELETE" });
}