--- phase: 7 slug: dkv-fleet-module status: draft shadcn_initialized: false preset: none created: 2026-06-26 --- # Phase 7 — UI Design Contract: DKV Fleet Module > Visual and interaction contract for frontend phases. Generated by gsd-ui-researcher, verified by gsd-ui-checker. > > This module spans **3 frontend surfaces**: module main page (history + exports), > module settings page (inbox config + vehicle CRUD), and general settings SMTP page. > Sections beyond the base template are phase-specific extensions required for a > multi-surface module. --- ## Design System | Property | Value | Source | |----------|-------|--------| | Tool | manual — shadcn CSS-variable convention; no CLI (`components.json` absent) | codebase audit | | Preset | not applicable | — | | Component library | manual hand-written components following shadcn token convention (no Radix dependency) | codebase audit | | Icon library | inline SVG only — no external icon package | codebase audit (admin-sidebar.tsx) | | Font | Inter via `var(--font-sans)` — system-ui fallback | globals.css | | Border radius | `var(--radius)` = 0.5rem (8px) | globals.css | | Color space | OKLCH throughout | globals.css | **Registry safety:** Not applicable — no `components.json`, no CLI registry consumption. **shadcn gate:** components.json NOT found. Project has established manual approach; do not initialize CLI. All new components must hand-write following existing patterns in `apps/web/src/components/`. --- ## Spacing Scale Declared values (must be multiples of 4). Source: codebase audit of existing components. | Token | Value | Tailwind class | Usage | |-------|-------|----------------|-------| | xs | 4px | `gap-1`, `p-1` | Icon gaps, inline element padding | | sm | 8px | `gap-2`, `p-2` | Compact element spacing, icon-to-label gap | | lg | 16px | `p-4`, `gap-4` | Default section padding, form field gap | | xl | 24px | `p-6`, `gap-6` | Card padding, section breaks | | 2xl | 32px | `gap-8` | Layout column gaps | | 3xl | 48px | — | Major page-level spacing | ### Component Exceptions The following sub-4 spacing values appear in inherited component patterns. They are **not named scale tokens** — do not use them for new layout decisions. | Tailwind classes | Origin | Why not a token | |-----------------|--------|-----------------| | `px-3 py-1.5` (12px / 6px) | `CalendarSourceForm` `h-9` input pattern | Inherited browser-default input sizing; compensates for 36px target height within the 8-point grid constraint. Apply only inside `` and ``. Clicking [CSV importieren] triggers the hidden input. After file selected → inline dialog (NOT a full modal; a contained `
` in the page below the button, with border and padding): ``` Importmodus: ○ Zusammenführen — neue Fahrzeuge hinzufügen, vorhandene behalten (default) ● Ersetzen — alle vorhandenen Fahrzeuge löschen und neu importieren ↳ [Warning text, text-xs text-destructive]: "Alle vorhandenen Fahrzeuge werden ersetzt." [Abbrechen] [Importieren] ``` "Importieren" with Replace mode selected → destructive confirm dialog (standard browser-style: title + body + [Abbrechen] [Ersetzen]) Post-import: inline success message `text-sm` at green color + auto-dismiss after 4 seconds + table refresh. --- ### Surface C: General Settings SMTP **File:** `apps/web/src/app/(portal)/settings/general/smtp/page.tsx` **SettingsSidebar extension** (`apps/web/src/components/settings/settings-sidebar.tsx`): Add new "Allgemein" section **above** the existing "Dashboard" section: ```tsx // New section — insert before existing Dashboard items

{t('categoryGeneral')}

``` Active/inactive link styling: identical to existing items in `settings-sidebar.tsx` (copy verbatim). **SmtpConfigForm:** File: `apps/web/src/app/(portal)/settings/general/smtp/components/SmtpConfigForm.tsx` Page heading: `

SMTP-Konfiguration

` — follows `settings/dashboard/page.tsx` heading pattern exactly. Form fields (in order, follow CalendarSourceForm pattern, `max-w-md` on all inputs): ``` Host * [text input, placeholder: "smtp.example.com"] Port * [number input, default 587] Verschlüsselung * [select: Keine | STARTTLS | SSL-TLS, default STARTTLS] Benutzername [text input, optional] Passwort [password input, optional, with show/hide toggle] Absenderadresse * [email input, placeholder: "tessera@example.com"] [help text: "Die E-Mail-Adresse, die als Absender angezeigt wird."] ``` Form actions row: `flex gap-3 pt-2` - "Einstellungen speichern" (primary button) - "Verbindung testen" (secondary button) Connection test feedback: appears below the action row: - Loading: `text-sm text-muted-foreground` — "Verbindung wird getestet..." - Success: `text-sm` green — "Verbindung erfolgreich" - Error: `text-sm text-destructive` — "Verbindung fehlgeschlagen: {fehlermeldung}" - Auto-clears after 6 seconds --- ### StatusBadge Component **File:** `apps/web/src/app/(portal)/modules/dkv-fleet/components/StatusBadge.tsx` ``` type Status = 'Verarbeitet' | 'Fehler' | 'Versand fehlgeschlagen' props: { status: Status; errorMessage?: string } ``` Shape: `` Colors (inline style — not CSS classes, as these OKLCH values are not in Tailwind config): - Verarbeitet: `color: oklch(0.40 0.15 148)`, `background: oklch(0.40 0.15 148 / 0.10)` - Fehler: `color: var(--destructive)`, `background: oklch(0.55 0.2 27 / 0.10)` - Versand fehlgeschlagen: `color: oklch(0.50 0.13 70)`, `background: oklch(0.50 0.13 70 / 0.10)` "Fehler" and "Versand fehlgeschlagen" states: add `title={errorMessage}` attribute when `errorMessage` is provided — renders browser tooltip on hover. --- ### Interaction Contracts Summary | Interaction | Trigger | Feedback | Error handling | |------------|---------|---------|----------------| | "Jetzt prüfen" | button click | loading state on button; table refetch on success | error banner above table; button re-enables | | Delete vehicle | trash icon click | confirm dialog appears | none (server error: inline `text-destructive` below table) | | Edit vehicle row | pencil icon click | row switches to edit-mode inputs inline | validation: Kennzeichen required; inline `text-destructive text-xs` below field | | Save vehicle row | checkmark click | row returns to view mode with new values | server error: `text-destructive text-xs` appears below row | | CSV import | file selected | import mode dialog appears | file parse error: `text-destructive text-sm` in dialog | | Save InboxConfig | "Speichern" click | button disables during save; re-enables on done | inline error below button | | Test inbox connection | "Verbindung testen" click | inline loading → success/error text | shown inline below button | | Save SmtpConfig | "Speichern" click | button disables during save; re-enables | inline error below button | | Test SMTP | "Verbindung testen" click | inline loading → success/error text | shown inline, auto-clears 6s | | Export file download | filename link click | browser download dialog | n/a (HTTP error: browser default) | **Toast pattern:** Use only for async success states that don't have a natural location (e.g., CSV import success). Toast: fixed bottom-right, `rounded border border-border bg-card shadow-md px-4 py-3 text-sm`, auto-dismiss 4 seconds. --- ### i18n Translation Keys New keys required for this phase. Add to `apps/web/src/i18n/messages/{de,en}.json`. **Namespace `dkvFleet` (new):** ```json { "pageTitle": "DKV Flotte", "checkNow": "Jetzt prüfen", "checking": "Wird geprüft...", "lastChecked": "Zuletzt geprüft: {time} Uhr", "neverChecked": "Noch nie geprüft", "settingsTitle": "Moduleinstellungen", "historyTitle": "Verarbeitungshistorie", "exportFilesTitle": "Letzte Exportdateien", "emptyHistory": "Noch keine Rechnungen verarbeitet", "emptyHistoryBody": "Klicke auf 'Jetzt prüfen', um den Posteingang zu prüfen. Neue Rechnungen erscheinen hier automatisch.", "emptyExports": "Noch keine Exportdateien vorhanden", "emptyVehicles": "Keine Fahrzeuge hinterlegt", "emptyVehiclesBody": "Füge Fahrzeuge manuell hinzu oder lade eine CSV-Datei hoch.", "status": { "processed": "Verarbeitet", "error": "Fehler", "sendFailed": "Versand fehlgeschlagen" }, "col": { "dateTime": "Datum/Zeit", "invoiceNumber": "Rechnungsnummer", "vehicles": "Fahrzeuge", "transactions": "Transaktionen", "status": "Status", "exportFile": "Exportdatei", "licensePlate": "Kennzeichen", "brand": "Marke", "model": "Modell", "driver": "Fahrer", "actions": "Aktionen" }, "addVehicle": "Fahrzeug hinzufügen", "editVehicle": "Fahrzeug bearbeiten", "deleteVehicle": "Fahrzeug löschen", "deleteVehicleBody": "Das Fahrzeug {plate} wird dauerhaft aus der Stammdatenliste entfernt. Diese Aktion kann nicht rückgängig gemacht werden.", "importCsv": "CSV importieren", "importMode": "Importmodus", "importMerge": "Zusammenführen — neue Fahrzeuge hinzufügen, vorhandene behalten", "importReplace": "Ersetzen — alle vorhandenen Fahrzeuge löschen und neu importieren", "importReplaceWarning": "Alle vorhandenen Fahrzeuge werden ersetzt.", "importReplaceConfirmTitle": "Fahrzeuge ersetzen?", "importReplaceConfirmBody": "Alle vorhandenen Fahrzeuge werden durch die CSV-Daten ersetzt. Diese Aktion kann nicht rückgängig gemacht werden.", "importReplaceConfirmButton": "Ersetzen", "importSuccess": "{count} Fahrzeuge importiert", "vehicleCount": "{count} Fahrzeuge", "tabs": { "inbox": "Posteingang", "vehicles": "Fahrzeuge" }, "form": { "protocol": "Protokoll", "host": "Host", "port": "Port", "encryption": "Verschlüsselung", "encryptionNone": "Keine", "folder": "Ordner", "senderFilter": "Absenderfilter", "pollInterval": "Abrufintervall", "pollIntervalSuffix": "Minuten", "username": "Benutzername", "password": "Passwort", "exportRecipient": "Export-Empfänger", "vehicleFormat": "Fahrzeug-Formatstring", "vehicleFormatHelp": "Verfügbare Platzhalter: {Marke}, {Modell}, {Kennzeichen}, {Fahrer}", "active": "Aktiv", "testConnection": "Verbindung testen", "testTesting": "Verbindung wird getestet...", "testSuccess": "Verbindung erfolgreich", "testFailed": "Verbindung fehlgeschlagen: {error}", "save": "Einstellungen speichern", "cancel": "Bearbeitung abbrechen", "cancelDialog": "Abbrechen", "showPassword": "Passwort anzeigen", "hidePassword": "Passwort verbergen" }, "errors": { "pollFailed": "Posteingang konnte nicht geprüft werden. Überprüfe die Konfiguration unter 'Moduleinstellungen › Posteingang'.", "smtpFailed": "Export wurde erstellt, konnte aber nicht gesendet werden. Die Datei ist weiterhin zum Download verfügbar." } } ``` **Namespace `settings` (extend existing — add keys):** ```json { "categoryGeneral": "Allgemein", "categorySmtp": "SMTP", "smtp": { "title": "SMTP-Konfiguration", "host": "Host", "port": "Port", "encryption": "Verschlüsselung", "username": "Benutzername", "password": "Passwort", "fromAddress": "Absenderadresse (Von)", "fromAddressHelp": "Die E-Mail-Adresse, die als Absender angezeigt wird.", "save": "Einstellungen speichern", "test": "Verbindung testen" } } ``` --- ### Component File Map | Component | File path | Surface | New / Extend | |-----------|-----------|---------|--------------| | InvoiceHistoryTable | `.../modules/dkv-fleet/components/InvoiceHistoryTable.tsx` | A | New | | StatusBadge | `.../modules/dkv-fleet/components/StatusBadge.tsx` | A | New | | ExportFileList | `.../modules/dkv-fleet/components/ExportFileList.tsx` | A | New | | InboxConfigForm | `.../modules/dkv-fleet/settings/components/InboxConfigForm.tsx` | B | New | | VehicleTable | `.../modules/dkv-fleet/settings/components/VehicleTable.tsx` | B | New | | CsvImportButton | `.../modules/dkv-fleet/settings/components/CsvImportButton.tsx` | B | New | | SmtpConfigForm | `.../settings/general/smtp/components/SmtpConfigForm.tsx` | C | New | | SettingsSidebar | `apps/web/src/components/settings/settings-sidebar.tsx` | C | Extend (add Allgemein section) | Base path prefix for module components: `apps/web/src/app/(portal)/` --- ## Checker Sign-Off - [ ] Dimension 1 Copywriting: PASS - [ ] Dimension 2 Visuals: PASS - [ ] Dimension 3 Color: PASS - [ ] Dimension 4 Typography: PASS - [ ] Dimension 5 Spacing: PASS - [ ] Dimension 6 Registry Safety: PASS **Approval:** pending --- *Sources: CONTEXT.md (D-01 to D-21), RESEARCH.md (Architecture Patterns, Standard Stack), globals.css (all OKLCH tokens), settings-sidebar.tsx + calendar-source-form.tsx + admin-sidebar.tsx (component patterns), modules/domaincheck/page.tsx + settings/dashboard/page.tsx (heading typography patterns)*