M1: implement operational core

Demo auth, seed import/reset, dashboard, vehicle/booking list+detail, audit trail. Backend: 19 tests passing, ruff clean. Frontend: React Router shell, typed API client, responsive pages. Verified end-to-end via curl and browser.
This commit is contained in:
NuklearRabbit
2026-08-01 21:20:53 +02:00
parent 04d26f1f2e
commit 03c5b60235
47 changed files with 2518 additions and 70 deletions
+52
View File
@@ -0,0 +1,52 @@
const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "";
export class ApiError extends Error {
status: number;
code: string;
correlationId: string;
constructor(status: number, code: string, message: string, correlationId: string) {
super(message);
this.status = status;
this.code = code;
this.correlationId = correlationId;
}
}
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const response = await fetch(`${API_BASE}${path}`, {
...init,
credentials: "include",
headers: {
"Content-Type": "application/json",
...(init?.headers ?? {}),
},
});
if (!response.ok) {
let body: { error?: { code: string; message: string; correlation_id: string } } | undefined;
try {
body = await response.json();
} catch {
body = undefined;
}
const error = body?.error;
throw new ApiError(
response.status,
error?.code ?? String(response.status),
error?.message ?? response.statusText,
error?.correlation_id ?? "",
);
}
if (response.status === 204) {
return undefined as T;
}
return (await response.json()) as T;
}
export const api = {
get: <T>(path: string) => request<T>(path),
post: <T>(path: string, body?: unknown, headers?: Record<string, string>) =>
request<T>(path, { method: "POST", body: body ? JSON.stringify(body) : undefined, headers }),
};
+131
View File
@@ -0,0 +1,131 @@
export type Role = "operations_manager" | "rental_employee";
export interface CurrentUser {
public_ref: string;
display_name: string;
role: Role;
}
export interface Vehicle {
public_ref: string;
make: string;
model: string;
model_year: number;
registration_number: string;
location: string;
operational_status: string;
odometer_km: number;
next_service_km: number;
active: boolean;
attention: boolean;
}
export interface BookingSummary {
public_ref: string;
customer_ref: string;
vehicle_ref: string;
starts_at: string;
ends_at: string;
status: string;
}
export interface Booking extends BookingSummary {
start_odometer_km: number | null;
end_odometer_km: number | null;
requirements_complete: boolean;
customer_name: string;
}
export interface Inspection {
public_ref: string;
booking_ref: string;
type: string;
fuel_level_percent: number;
cleanliness_ok: boolean;
damage_reported: boolean;
technical_warning: boolean;
odometer_km: number;
completed_at: string;
}
export interface MaintenanceRecord {
public_ref: string;
occurred_at: string;
odometer_km: number;
category: string;
summary: string;
}
export interface DataQualityIssue {
public_ref: string;
rule_type: string;
entity_type: string;
entity_ref: string;
severity: "low" | "medium" | "high";
status: "open" | "deferred" | "resolved" | "rejected";
evidence: Record<string, unknown>;
detected_at: string;
resolved_at: string | null;
}
export interface VehicleDetail extends Vehicle {
bookings: BookingSummary[];
inspections: Inspection[];
maintenance: MaintenanceRecord[];
quality_issues: DataQualityIssue[];
}
export interface DashboardMetrics {
available: number;
rented: number;
cleaning: number;
maintenance: number;
blocked: number;
open_quality_issues: number;
pending_or_failed_workflows: number;
}
export interface AttentionItem {
kind: string;
severity: "low" | "medium" | "high";
title: string;
detail: string;
link_type: "vehicle" | "booking" | "customer";
link_ref: string;
}
export interface TodayItem {
kind: "departure" | "return";
booking_ref: string;
vehicle_ref: string;
scheduled_at: string;
}
export interface AutomationRun {
event_id: string;
event_type: string;
aggregate_ref: string;
status: string;
attempts: number;
last_error: string | null;
occurred_at: string;
}
export interface Dashboard {
metrics: DashboardMetrics;
attention_items: AttentionItem[];
today: TodayItem[];
recent_automation: AutomationRun[];
}
export interface AuditEvent {
id: string;
actor_type: string;
actor_label: string;
action: string;
entity_type: string;
entity_id: string | null;
correlation_id: string;
occurred_at: string;
metadata: Record<string, unknown> | null;
}