Files
opencrm/frontend/src/components/contracts/ContractDetailModal.tsx
T
duffyduckandClaude Opus 4.8 fc3131059b Neuer Vertragsstatus "Gekuendigt / bestaetigt" + Kuendigungs-Workflow
Bisheriges "Gekuendigt" (CANCELLED) umbenannt in "Gekuendigt / Bestaetigung
abwarten" und wird jetzt automatisch gesetzt, sobald ein Kuendigungsschreiben
hochgeladen wird. Neuer Status CANCELLED_CONFIRMED ("Gekuendigt / bestaetigt")
wird automatisch gesetzt, sobald ein Kuendigungsbestaetigungsdatum vorliegt
(Dokument fuellt das Datum oder manuell) + Vertragsende = Kuendigungsdatum.

Schema: Enum-Wert CANCELLED_CONFIRMED + Migration (idempotentes MODIFY COLUMN);
Daten-Migration hebt bestehende CANCELLED (alte Logik: nur bei Bestaetigung
gesetzt) auf CANCELLED_CONFIRMED.

Backend: neue Trigger maybeMarkAwaitingConfirmationOnLetter (Schreiben->CANCELLED)
im Upload-Handler; maybeCancelOnCancellationConfirmation setzt jetzt
CANCELLED_CONFIRMED (auch aus CANCELLED). Cockpit-Semantik mitgewandert
(Fristen-Skip/"beendet" fuer CANCELLED_CONFIRMED; Ladeliste + Kuendigungs-
bestaetigungs-Filter erweitert).

Frontend: Labels/Farben/Status-Erklaerungen + Status-Dropdown in ContractList,
ContractDetail, ContractForm, ContractDetailModal, CustomerDetail
(CANCELLED orange "abwarten", CANCELLED_CONFIRMED rot).

Verifiziert: tsc+build gruen; Schreiben->CANCELLED, Bestaetigung->
CANCELLED_CONFIRMED+Enddatum; Daten-Migration idempotent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 18:14:34 +02:00

233 lines
8.3 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import { contractApi } from '../../services/api';
import Modal from '../ui/Modal';
import Badge from '../ui/Badge';
import Card from '../ui/Card';
import CopyButton from '../ui/CopyButton';
import type { ContractType, ContractStatus } from '../../types';
const typeLabels: Record<ContractType, string> = {
ELECTRICITY: 'Strom',
GAS: 'Gas',
DSL: 'DSL',
CABLE: 'Kabelinternet',
FIBER: 'Glasfaser',
MOBILE: 'Mobilfunk',
TV: 'TV',
CAR_INSURANCE: 'KFZ-Versicherung',
};
const statusLabels: Record<ContractStatus, string> = {
DRAFT: 'Entwurf',
PENDING: 'Ausstehend',
ACTIVE: 'Aktiv',
CANCELLED: 'Gekündigt / Bestätigung abwarten',
CANCELLED_CONFIRMED: 'Gekündigt / bestätigt',
EXPIRED: 'Abgelaufen',
DEACTIVATED: 'Deaktiviert',
ONGOING: 'Läuft weiter (unbestimmt)',
};
const statusVariants: Record<ContractStatus, 'success' | 'warning' | 'danger' | 'default'> = {
ACTIVE: 'success',
PENDING: 'warning',
CANCELLED: 'warning',
CANCELLED_CONFIRMED: 'danger',
EXPIRED: 'danger',
DRAFT: 'default',
DEACTIVATED: 'default',
ONGOING: 'success',
};
interface ContractDetailModalProps {
contractId: number;
isOpen: boolean;
onClose: () => void;
}
export default function ContractDetailModal({ contractId, isOpen, onClose }: ContractDetailModalProps) {
const { data, isLoading, error } = useQuery({
queryKey: ['contract', contractId],
queryFn: () => contractApi.getById(contractId),
enabled: isOpen,
});
const c = data?.data;
return (
<Modal isOpen={isOpen} onClose={onClose} title="Vertragsdetails" size="xl">
{isLoading && (
<div className="text-center py-8 text-gray-500">Laden...</div>
)}
{error && (
<div className="text-center py-8 text-red-600">Fehler beim Laden des Vertrags</div>
)}
{c && (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center gap-3 pb-4 border-b">
<span className="text-xl font-bold font-mono flex items-center gap-2">
{c.contractNumber}
<CopyButton value={c.contractNumber} />
</span>
<Badge>{typeLabels[c.type as ContractType] || c.type}</Badge>
<Badge variant={statusVariants[c.status as ContractStatus] || 'default'}>
{statusLabels[c.status as ContractStatus] || c.status}
</Badge>
</div>
{/* Anbieter & Tarif */}
{(c.providerName || c.provider?.name || c.tariffName || c.tariff?.name
|| c.customerNumberAtProvider || c.contractNumberAtProvider) && (
<Card title="Anbieter & Tarif">
<div className="grid grid-cols-2 gap-4">
{(c.providerName || c.provider?.name) && (
<div>
<dt className="text-sm text-gray-500">Anbieter</dt>
<dd className="flex items-center gap-1">
{c.providerName || c.provider?.name}
<CopyButton value={c.providerName || c.provider?.name || ''} />
</dd>
</div>
)}
{(c.tariffName || c.tariff?.name) && (
<div>
<dt className="text-sm text-gray-500">Tarif</dt>
<dd className="flex items-center gap-1">
{c.tariffName || c.tariff?.name}
<CopyButton value={c.tariffName || c.tariff?.name || ''} />
</dd>
</div>
)}
{c.customerNumberAtProvider && (
<div>
<dt className="text-sm text-gray-500">Kundennummer beim Anbieter</dt>
<dd className="font-mono flex items-center gap-1">
{c.customerNumberAtProvider}
<CopyButton value={c.customerNumberAtProvider} />
</dd>
</div>
)}
{c.contractNumberAtProvider && (
<div>
<dt className="text-sm text-gray-500">Vertragsnummer beim Anbieter</dt>
<dd className="font-mono flex items-center gap-1">
{c.contractNumberAtProvider}
<CopyButton value={c.contractNumberAtProvider} />
</dd>
</div>
)}
</div>
</Card>
)}
{/* Zähler (nur bei Strom/Gas) */}
{(c.type === 'ELECTRICITY' || c.type === 'GAS') && c.energyDetails?.meter && (
<Card title="Zähler">
<div className="grid grid-cols-2 gap-4">
<div>
<dt className="text-sm text-gray-500">Zählernummer</dt>
<dd className="font-mono flex items-center gap-1">
{c.energyDetails.meter.meterNumber}
<CopyButton value={c.energyDetails.meter.meterNumber} />
</dd>
</div>
{c.energyDetails.maloId && (
<div>
<dt className="text-sm text-gray-500">MaLo-ID</dt>
<dd className="font-mono flex items-center gap-1">
{c.energyDetails.maloId}
<CopyButton value={c.energyDetails.maloId} />
</dd>
</div>
)}
</div>
</Card>
)}
{/* Laufzeit */}
<Card title="Laufzeit">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{c.startDate && (
<div>
<dt className="text-sm text-gray-500">Vertragsbeginn</dt>
<dd>{new Date(c.startDate).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}</dd>
</div>
)}
{c.endDate && (
<div>
<dt className="text-sm text-gray-500">Vertragsende</dt>
<dd>{new Date(c.endDate).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })}</dd>
</div>
)}
{c.contractDuration && (
<div>
<dt className="text-sm text-gray-500">Laufzeit</dt>
<dd>{c.contractDuration.description}</dd>
</div>
)}
{c.cancellationPeriod && (
<div>
<dt className="text-sm text-gray-500">Kündigungsfrist</dt>
<dd>{c.cancellationPeriod.description}</dd>
</div>
)}
</div>
</Card>
{/* Portal-Zugangsdaten */}
{(c.portalUsername || c.provider?.portalUrl) && (
<Card title="Portal-Zugangsdaten">
<div className="grid grid-cols-2 gap-4">
{c.provider?.portalUrl && (
<div>
<dt className="text-sm text-gray-500">Portal-URL</dt>
<dd>
<a
href={c.provider.portalUrl}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
{c.provider.portalUrl}
</a>
</dd>
</div>
)}
{c.portalUsername && (
<div>
<dt className="text-sm text-gray-500">Benutzername</dt>
<dd className="font-mono flex items-center gap-1">
{c.portalUsername}
<CopyButton value={c.portalUsername} />
</dd>
</div>
)}
</div>
</Card>
)}
{/* Adresse */}
{c.address && (
<Card title="Lieferadresse">
<p>
{c.address.street} {c.address.houseNumber}
</p>
<p>{c.address.postalCode} {c.address.city}</p>
</Card>
)}
{/* Notizen */}
{c.notes && (
<Card title="Notizen">
<p className="whitespace-pre-wrap text-gray-700">{c.notes}</p>
</Card>
)}
</div>
)}
</Modal>
);
}