Files
tessera-ctl/apps/web/src/app/(portal)/modules/dkv-fleet/page.tsx
T
schalli 30e4bc3a4f style(web): Seiten auf Mosaik-Muster umgestellt
Einheitliche Seitenkoepfe mit Modul-Kachel, Knopf- und Linkklassen
statt gelber Schrift, Karten ohne harte Rahmen, Tabellenkoepfe ohne
Grossbuchstaben, Marktplatz mit Fluent-Reitern, Filterpillen und
Kachel-Karten, Einstellungs-/Verwaltungsnavigation mit Auswahlpille,
leere Zustaende mit Symbol im Kreis, Proxmox ohne leuchtende Schatten.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 14:49:01 +02:00

155 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useCallback, useState } from 'react';
import { useTranslations } from 'next-intl';
import { useParams } from 'next/navigation';
import Link from 'next/link';
import { PageHeader } from '@/components/layout/page-header';
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 params = useParams<{ category?: string; moduleSlug?: string }>();
const basePath = params.category && params.moduleSlug
? `/modules/${params.category}/${params.moduleSlug}`
: '/modules/dkv-fleet';
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-3 sm:p-6">
{/* Seitenkopf (Design „Mosaik“): App-Kachel, Titel, Stand; Aktionen rechts */}
<PageHeader
moduleSlug="dkv-fleet"
title={t('pageTitle')}
description={
lastChecked
? t('lastChecked', { time: formatLastChecked(lastChecked) })
: t('neverChecked')
}
actions={
<>
<Link
href={`${basePath}/settings`}
className="btn btn-secondary btn-icon"
title={t('settingsTitle')}
aria-label={t('settingsTitle')}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="3" />
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
</svg>
</Link>
<button
type="button"
onClick={handleCheckNow}
disabled={isChecking}
className="btn btn-primary"
>
{isChecking ? (
<>
<SpinnerIcon />
{t('checking')}
</>
) : (
t('checkNow')
)}
</button>
</>
}
/>
{/* 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>
);
}