feat(07-05): dkv-api client + Surface A — history table + export list + check-now
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 43s
Tessera CI/CD / Tests (push) Waiting to run

- 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:
2026-06-27 00:36:28 +02:00
parent 316f8353b7
commit 2fe7bca6e8
5 changed files with 751 additions and 0 deletions
@@ -0,0 +1,139 @@
'use client';
import { useCallback, useState } from 'react';
import { useTranslations } from 'next-intl';
import { checkNow, type DkvHistoryEntry } from '@/lib/dkv-api';
import { InvoiceHistoryTable } from './components/InvoiceHistoryTable';
import { ExportFileList } from './components/ExportFileList';
/**
* Inline spinner SVG per 07-UI-SPEC (20×20, animate-spin, stroke="currentColor").
*/
function SpinnerIcon() {
return (
<svg
className="mr-2 h-4 w-4 animate-spin"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
</svg>
);
}
/**
* DKV Flotte module main page (Surface A from 07-UI-SPEC).
* Shows processing history (InvoiceHistoryTable) + export file list (ExportFileList).
* "Jetzt prüfen" triggers an immediate inbox poll, then refetches history.
*/
export default function DkvFleetPage() {
const t = useTranslations('dkvFleet');
const [isChecking, setIsChecking] = useState(false);
const [pollError, setPollError] = useState<string | null>(null);
const [refreshKey, setRefreshKey] = useState(0);
const [lastChecked, setLastChecked] = useState<Date | null>(null);
const [historyItems, setHistoryItems] = useState<DkvHistoryEntry[]>([]);
const handleCheckNow = async () => {
setIsChecking(true);
setPollError(null);
try {
await checkNow();
setLastChecked(new Date());
// Increment refreshKey to trigger InvoiceHistoryTable refetch
setRefreshKey((k) => k + 1);
} catch {
setPollError(t('errors.pollFailed'));
} finally {
setIsChecking(false);
}
};
const handleItemsLoaded = useCallback((items: DkvHistoryEntry[]) => {
setHistoryItems(items);
}, []);
const formatLastChecked = (date: Date): string => {
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${day}.${month}.${year}, ${hours}:${minutes}`;
};
return (
<div className="mx-auto max-w-5xl space-y-6 p-6">
{/* Page Header Row */}
<div className="flex items-start justify-between gap-4">
<div>
<h1 className="text-2xl font-semibold tracking-tight">
{t('pageTitle')}
</h1>
<p className="mt-1 text-sm text-muted-foreground">
{lastChecked
? t('lastChecked', { time: formatLastChecked(lastChecked) })
: t('neverChecked')}
</p>
</div>
{/* "Jetzt prüfen" button per 07-UI-SPEC button states */}
<button
type="button"
onClick={handleCheckNow}
disabled={isChecking}
className={`rounded bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 flex items-center${
isChecking ? ' opacity-80 cursor-not-allowed' : ''
}`}
>
{isChecking ? (
<>
<SpinnerIcon />
{t('checking')}
</>
) : (
t('checkNow')
)}
</button>
</div>
{/* Error banner — conditional per UI-SPEC */}
{pollError && (
<div className="flex items-start justify-between rounded border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
<span>{pollError}</span>
<button
type="button"
onClick={() => setPollError(null)}
className="ml-4 flex-shrink-0 font-medium hover:opacity-70"
aria-label="Schließen"
>
×
</button>
</div>
)}
{/* Invoice Processing History */}
<section>
<h2 className="text-lg font-semibold mb-4">{t('historyTitle')}</h2>
<InvoiceHistoryTable
refreshKey={refreshKey}
onItemsLoaded={handleItemsLoaded}
/>
</section>
{/* Last Export Files */}
<section>
<h2 className="text-sm font-semibold mb-2 mt-6">
{t('exportFilesTitle')}
</h2>
<ExportFileList historyItems={historyItems} />
</section>
</div>
);
}