9efa3bab1d
- Fix orphaned DkvModuleConfig: tenantId pointed to deleted tenant, updated to Default tenant - DKV controller: return 404 instead of HTTP 200 null when no config exists - IMAP provider: also detect PDFs sent as application/octet-stream (check filename extension) - IMAP provider: add seen:false filter so already-processed emails are skipped on re-poll - IMAP provider: mark email as \Seen after successful PDF download to prevent reprocessing - Frontend dkv-api: handle 404 from fetchConfig as "not yet configured" (returns null) - InboxConfigForm: show warning banner when config not yet saved in DB - InboxConfigForm: add "Jetzt prüfen" button to manually trigger POST /dkv/check-now Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
265 lines
7.0 KiB
TypeScript
265 lines
7.0 KiB
TypeScript
/**
|
|
* 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;
|
|
domain?: 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 | null> {
|
|
const res = await fetch(`${API_URL}/dkv/config`, {
|
|
credentials: 'include',
|
|
});
|
|
if (res.status === 404) return null;
|
|
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<{ status: string; message: string; checkedAt: string }> {
|
|
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');
|
|
return res.json();
|
|
}
|
|
|
|
/**
|
|
* 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; mode: string }> {
|
|
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)}`;
|
|
}
|