feat(07-05): dkv-api client + Surface A — history table + export list + check-now
- dkv-api.ts: typed fetch client for all /dkv/* routes (credentials: include) - StatusBadge: OKLCH inline styles per status (T-07-14: React text nodes only) - InvoiceHistoryTable: 6-column table, 5 skeleton rows, pagination >25, refreshKey - ExportFileList: up to 10 unique export filenames derived from history (T-07-13) - page.tsx: Jetzt prüfen trigger, error banner, refreshKey lift, DKV-04/DKV-01
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* DKV Fleet Module API client.
|
||||
* Consumes the /dkv/* REST surface built in Plan 04.
|
||||
* All calls use credentials: 'include' for cookie-based auth.
|
||||
*
|
||||
* Security (T-07-12): DkvConfig never exposes the password —
|
||||
* only hasPassword (boolean) is returned by the server.
|
||||
* The password field is only sent when the user types a new one.
|
||||
*/
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
||||
|
||||
// --- Types mirroring the Plan 04 backend contract ---
|
||||
|
||||
/**
|
||||
* DKV module inbox configuration.
|
||||
* Note: password is NEVER returned (T-07-12). Only hasPassword is exposed.
|
||||
*/
|
||||
export interface DkvConfig {
|
||||
protocol: 'imap' | 'exchange';
|
||||
host: string;
|
||||
port: number;
|
||||
encryption: 'none' | 'starttls' | 'ssl-tls';
|
||||
folder: string;
|
||||
senderFilter?: string | null;
|
||||
pollIntervalMin: number;
|
||||
isActive: boolean;
|
||||
exportRecipient: string;
|
||||
vehicleFormatString: string;
|
||||
username?: string | null;
|
||||
/** true if a password is stored server-side — never the actual secret */
|
||||
hasPassword: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A single entry in the DKV invoice processing history.
|
||||
* Mirrors DkvInvoiceHistory Prisma model.
|
||||
*/
|
||||
export interface DkvHistoryEntry {
|
||||
id: string;
|
||||
datumZeit: string;
|
||||
rechnungsnummer: string;
|
||||
anzahlFahrzeuge: number;
|
||||
anzahlTransaktionen: number;
|
||||
status: 'Verarbeitet' | 'Fehler' | 'Versand fehlgeschlagen';
|
||||
errorMessage?: string | null;
|
||||
exportFilename?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vehicle master record for DKV→driver mapping (DKV-03).
|
||||
*/
|
||||
export interface DkvVehicle {
|
||||
id: string;
|
||||
kennzeichen: string;
|
||||
marke: string;
|
||||
modell: string;
|
||||
fahrer: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for creating a new vehicle.
|
||||
*/
|
||||
export interface CreateVehiclePayload {
|
||||
kennzeichen: string;
|
||||
marke: string;
|
||||
modell: string;
|
||||
fahrer: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload for updating an existing vehicle. All fields optional.
|
||||
*/
|
||||
export interface UpdateVehiclePayload {
|
||||
kennzeichen?: string;
|
||||
marke?: string;
|
||||
modell?: string;
|
||||
fahrer?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginated history response from GET /dkv/history.
|
||||
*/
|
||||
export interface DkvHistoryPage {
|
||||
items: DkvHistoryEntry[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
// --- API functions ---
|
||||
|
||||
/**
|
||||
* Fetch the current DKV module inbox configuration.
|
||||
* GET /dkv/config
|
||||
*/
|
||||
export async function fetchConfig(): Promise<DkvConfig> {
|
||||
const res = await fetch(`${API_URL}/dkv/config`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to fetch DKV config');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the DKV module inbox configuration.
|
||||
* PUT /dkv/config
|
||||
* Only include `password` in payload when the user has entered a new one.
|
||||
*/
|
||||
export async function saveConfig(
|
||||
payload: Partial<DkvConfig> & { password?: string },
|
||||
): Promise<DkvConfig> {
|
||||
const res = await fetch(`${API_URL}/dkv/config`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to save DKV config');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the inbox connection with the current (or pending) config.
|
||||
* POST /dkv/test-connection
|
||||
* Returns { success: boolean, message?: string }
|
||||
*/
|
||||
export async function testConnection(
|
||||
payload: Partial<DkvConfig> & { password?: string },
|
||||
): Promise<{ success: boolean; message?: string }> {
|
||||
const res = await fetch(`${API_URL}/dkv/test-connection`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to test DKV connection');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger an immediate inbox check (manual poll).
|
||||
* POST /dkv/check-now
|
||||
*/
|
||||
export async function checkNow(): Promise<void> {
|
||||
const res = await fetch(`${API_URL}/dkv/check-now`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to trigger DKV inbox check');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch paginated invoice processing history.
|
||||
* GET /dkv/history?page=&limit=
|
||||
*/
|
||||
export async function fetchHistory(
|
||||
page = 1,
|
||||
limit = 25,
|
||||
): Promise<DkvHistoryPage> {
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', String(page));
|
||||
params.set('limit', String(limit));
|
||||
|
||||
const res = await fetch(`${API_URL}/dkv/history?${params.toString()}`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to fetch DKV history');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all vehicles from the DKV vehicle master list.
|
||||
* GET /dkv/vehicles
|
||||
*/
|
||||
export async function fetchVehicles(): Promise<DkvVehicle[]> {
|
||||
const res = await fetch(`${API_URL}/dkv/vehicles`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to fetch DKV vehicles');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new vehicle entry.
|
||||
* POST /dkv/vehicles
|
||||
*/
|
||||
export async function createVehicle(
|
||||
payload: CreateVehiclePayload,
|
||||
): Promise<DkvVehicle> {
|
||||
const res = await fetch(`${API_URL}/dkv/vehicles`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to create DKV vehicle');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing vehicle.
|
||||
* PUT /dkv/vehicles/:id
|
||||
*/
|
||||
export async function updateVehicle(
|
||||
id: string,
|
||||
payload: UpdateVehiclePayload,
|
||||
): Promise<DkvVehicle> {
|
||||
const res = await fetch(`${API_URL}/dkv/vehicles/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to update DKV vehicle');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a vehicle from the master list.
|
||||
* DELETE /dkv/vehicles/:id
|
||||
*/
|
||||
export async function deleteVehicle(id: string): Promise<void> {
|
||||
const res = await fetch(`${API_URL}/dkv/vehicles/${id}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to delete DKV vehicle');
|
||||
}
|
||||
|
||||
/**
|
||||
* Import vehicles from a CSV file.
|
||||
* POST /dkv/vehicles/import (multipart/form-data)
|
||||
* mode: 'merge' | 'replace'
|
||||
* Do NOT set Content-Type manually — browser sets it with boundary.
|
||||
*/
|
||||
export async function importVehiclesCsv(
|
||||
file: File,
|
||||
mode: 'merge' | 'replace',
|
||||
): Promise<{ count: number }> {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
form.append('mode', mode);
|
||||
|
||||
const res = await fetch(`${API_URL}/dkv/vehicles/import`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: form,
|
||||
// Content-Type is NOT set manually — fetch sets multipart/form-data with boundary
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to import DKV vehicles CSV');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the full download URL for an export file.
|
||||
* Pure helper — no fetch call. Server enforces the DKV_*.xlsx whitelist (T-07-13).
|
||||
*/
|
||||
export function exportFileUrl(filename: string): string {
|
||||
return `${API_URL}/dkv/exports/${encodeURIComponent(filename)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user