Fix Folgevertrag aus deaktiviertem Vertrag + Kundendaten-Modal erweitern

1) Folgevertrag-Bug: Backend legt den Folgevertrag korrekt mit
   previousContractId an. Der Verlust passierte im Frontend - das
   Vorgaenger-Dropdown holt Vertraege ueber getAll, das DEACTIVATED
   standardmaessig ausblendet. Beim Bearbeiten des Folgevertrags war der
   deaktivierte Vorgaenger nicht als Option da -> Verknuepfung ging beim
   Speichern verloren.
   Fix: getAllContracts + Controller + contractApi.getAll um
   includeDeactivated erweitert; Vorgaenger-Dropdown nutzt es und markiert
   deaktivierte Vertraege mit '· deaktiviert'. Verifiziert (Flag inkludiert
   deaktivierte; Folgevertrag setzt previousContractId).

2) Kundendaten-Modal: zeigt jetzt zusaetzlich Lieferadresse + (falls
   abweichend) Rechnungsadresse des geoeffneten Vertrags, die Stressfrei-
   Adresse des Vertrags einzeln und darunter alle weiteren Stressfrei-
   Adressen des Kunden. CustomerInfoModal nimmt optionale Vertragskontext-
   Props; ContractDetail + ContractForm uebergeben sie.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-08 22:49:41 +02:00
co-authored by Claude Opus 4.8
parent a6b1dac606
commit 5f06ed9830
7 changed files with 113 additions and 13 deletions
@@ -1,6 +1,7 @@
import { useQuery } from '@tanstack/react-query';
import { X, RefreshCw, User } from 'lucide-react';
import { customerApi } from '../../services/api';
import { customerApi, stressfreiEmailApi } from '../../services/api';
import type { Address } from '../../types';
import { CopyableValue } from '../ui/CopyButton';
import Button from '../ui/Button';
@@ -8,6 +9,17 @@ interface CustomerInfoModalProps {
customerId: number;
open: boolean;
onClose: () => void;
// Optional: Kontext des geöffneten Vertrags. Wenn gesetzt, werden die
// Liefer-/Rechnungsadresse dieses Vertrags und seine Stressfrei-Adresse
// gesondert angezeigt.
deliveryAddress?: Address | null;
billingAddress?: Address | null;
contractStressfreiEmailId?: number | null;
}
function formatAddress(a?: Address | null): string {
if (!a) return '';
return `${a.street} ${a.houseNumber ?? ''}, ${a.postalCode} ${a.city}`.trim();
}
/**
@@ -15,7 +27,14 @@ interface CustomerInfoModalProps {
* 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) {
export default function CustomerInfoModal({
customerId,
open,
onClose,
deliveryAddress,
billingAddress,
contractStressfreiEmailId,
}: CustomerInfoModalProps) {
const { data, isLoading } = useQuery({
queryKey: ['customer-info-modal', customerId],
queryFn: () => customerApi.getById(customerId),
@@ -23,6 +42,14 @@ export default function CustomerInfoModal({ customerId, open, onClose }: Custome
staleTime: 30_000,
});
// Stressfrei-Wechseln-Adressen des Kunden (nur aktive).
const { data: stressfreiData } = useQuery({
queryKey: ['customer-info-modal-stressfrei', customerId],
queryFn: () => stressfreiEmailApi.getByCustomer(customerId),
enabled: open,
staleTime: 30_000,
});
if (!open) return null;
const c = data?.data;
@@ -32,9 +59,20 @@ export default function CustomerInfoModal({ customerId, open, onClose }: Custome
: '';
const primaryAddress = c?.addresses?.find((a) => a.isDefault) || c?.addresses?.[0];
const addressString = primaryAddress
? `${primaryAddress.street} ${primaryAddress.houseNumber ?? ''}, ${primaryAddress.postalCode} ${primaryAddress.city}`.trim()
: '';
const addressString = primaryAddress ? formatAddress(primaryAddress) : '';
const deliveryStr = formatAddress(deliveryAddress);
// Rechnungsadresse nur zeigen, wenn sie tatsächlich abweicht.
const billingStr =
billingAddress && billingAddress.id !== deliveryAddress?.id ? formatAddress(billingAddress) : '';
const stressfreiList = stressfreiData?.data ?? [];
const contractStressfrei = contractStressfreiEmailId
? stressfreiList.find((s) => s.id === contractStressfreiEmailId)
: undefined;
const otherStressfrei = stressfreiList.filter((s) => s.id !== contractStressfreiEmailId);
const hasContractContext = !!(deliveryStr || billingStr || contractStressfrei || otherStressfrei.length);
const formatDate = (iso?: string) => {
if (!iso) return '';
@@ -107,6 +145,34 @@ export default function CustomerInfoModal({ customerId, open, onClose }: Custome
value={c.commercialRegisterNumber}
/>
)}
{/* Vertragsbezogene Zusatzinfos */}
{hasContractContext && (
<div className="pt-3 mt-3 border-t border-gray-200 space-y-3">
{deliveryStr && (
<CopyableValue label="Lieferadresse (Vertrag)" value={deliveryStr} />
)}
{billingStr && (
<CopyableValue label="Rechnungsadresse (Vertrag)" value={billingStr} />
)}
{contractStressfrei && (
<CopyableValue
label="Stressfrei-Adresse (Vertrag)"
value={contractStressfrei.email}
/>
)}
{otherStressfrei.length > 0 && (
<div>
<dt className="text-sm text-gray-500 mb-1">Weitere Stressfrei-Adressen</dt>
<dd className="space-y-1">
{otherStressfrei.map((s) => (
<CopyableValue key={s.id} value={s.email} />
))}
</dd>
</div>
)}
</div>
)}
</dl>
)}
</div>