Vertragsansicht: Kunden-Schnellansicht-Modal + Cent/Euro-Input

Feature 1 – Kunden-Schnellansicht:
Info-Icon neben dem Kundenlink im Vertragsdetail oeffnet ein Modal
mit den wichtigsten Kundendaten (Firma, Name, Geburtsdatum/-ort,
Gruendungsdatum, Adresse, Telefon, Mobil, E-Mail, Portal-E-Mail,
Steuer-/Handelsregisternr). Jedes Feld hat einen Copy-Button.
Lazy-Fetch via customerApi.getById, staleTime 30s.

Feature 2 – Cent/Euro-Doppel-Input:
Neben dem €/kWh-Arbeitspreis-Feld jetzt ein zweites ct/kWh-Feld.
Bidirektional gekoppelt – Tippen in € aktualisiert ct (×100),
Tippen in ct aktualisiert € (÷100). Backend speichert weiterhin
nur den Euro-Wert; Cent ist reine UI-Hilfe. Float-Rausch-Schutz
verhindert "0.25 → 25.0000000000004". Greift fuer unitPrice und
unitPriceNt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 14:41:37 +02:00
parent 20d42c5270
commit 771f46d2ac
4 changed files with 245 additions and 9 deletions
@@ -0,0 +1,122 @@
import { useQuery } from '@tanstack/react-query';
import { X, RefreshCw, User } from 'lucide-react';
import { customerApi } from '../../services/api';
import { CopyableValue } from '../ui/CopyButton';
import Button from '../ui/Button';
interface CustomerInfoModalProps {
customerId: number;
open: boolean;
onClose: () => void;
}
/**
* Schnellansicht der wichtigsten Kundendaten wird aus der
* Vertragsdetail-Seite per Info-Icon neben dem Kundennamen geöffnet.
* Jedes Feld hat einen Copy-Button rechts. Modal-only, schreibt nichts.
*/
export default function CustomerInfoModal({ customerId, open, onClose }: CustomerInfoModalProps) {
const { data, isLoading } = useQuery({
queryKey: ['customer-info-modal', customerId],
queryFn: () => customerApi.getById(customerId),
enabled: open,
staleTime: 30_000,
});
if (!open) return null;
const c = data?.data;
const fullName = c
? [c.salutation, c.firstName, c.lastName].filter(Boolean).join(' ')
: '';
const primaryAddress = c?.addresses?.find((a) => a.isDefault) || c?.addresses?.[0];
const addressString = primaryAddress
? `${primaryAddress.street} ${primaryAddress.houseNumber ?? ''}, ${primaryAddress.postalCode} ${primaryAddress.city}`.trim()
: '';
const formatDate = (iso?: string) => {
if (!iso) return '';
try {
return new Date(iso).toLocaleDateString('de-DE', {
day: '2-digit', month: '2-digit', year: 'numeric',
});
} catch {
return iso;
}
};
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl w-full max-w-lg max-h-[85vh] flex flex-col">
<div className="flex items-center justify-between px-5 py-3 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
<User className="w-5 h-5 text-blue-600" />
Kundendaten
{c && (
<span className="text-sm font-normal text-gray-500">
· {c.customerNumber}
</span>
)}
</h3>
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-700 p-1"
aria-label="Schließen"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="px-5 py-4 overflow-auto flex-1">
{isLoading && (
<div className="text-gray-500 text-sm flex items-center gap-2 py-6">
<RefreshCw className="w-4 h-4 animate-spin" />
Lade Kundendaten
</div>
)}
{c && (
<dl className="space-y-3">
{c.type === 'BUSINESS' && c.companyName && (
<CopyableValue label="Firma" value={c.companyName} />
)}
{fullName && <CopyableValue label="Name" value={fullName} />}
{c.birthDate && (
<CopyableValue label="Geburtsdatum" value={formatDate(c.birthDate)} />
)}
{c.birthPlace && <CopyableValue label="Geburtsort" value={c.birthPlace} />}
{c.foundingDate && (
<CopyableValue label="Gründungsdatum" value={formatDate(c.foundingDate)} />
)}
{addressString && (
<CopyableValue label="Adresse" value={addressString} />
)}
{c.phone && <CopyableValue label="Telefon" value={c.phone} />}
{c.mobile && <CopyableValue label="Mobil" value={c.mobile} />}
{c.email && <CopyableValue label="E-Mail" value={c.email} />}
{c.portalEmail && c.portalEmail !== c.email && (
<CopyableValue label="Portal-E-Mail" value={c.portalEmail} />
)}
{c.taxNumber && (
<CopyableValue label="Steuernummer" value={c.taxNumber} />
)}
{c.commercialRegisterNumber && (
<CopyableValue
label="Handelsregisternummer"
value={c.commercialRegisterNumber}
/>
)}
</dl>
)}
</div>
<div className="px-5 py-3 border-t border-gray-200 flex justify-end">
<Button variant="secondary" onClick={onClose}>
Schließen
</Button>
</div>
</div>
</div>
);
}