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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user