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:
@@ -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 }),
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user