Vertrag: Auto-CANCELLED bei Kuendigungsbestaetigung + Cockpit-Filter

1) Auto-Status: Wird eine Kuendigungsbestaetigung hinzugefuegt (Datum
   ueber Formular und/oder Dokument-Upload), wird der Vertrag von ACTIVE
   auf CANCELLED gesetzt und das Vertragsende = Kuendigungsdatum.
   Zentrale Funktion maybeCancelOnCancellationConfirmation (idempotent,
   nur aus ACTIVE). Upload-Route ersetzt die alte Inline-Logik (setzt
   jetzt auch endDate); Update-Controller triggert nur bei neu/geaendertem
   Bestaetigungsdatum (manuelle Status-Korrekturen bleiben unangetastet).

2) Cockpit-Filter 'Kuendigungsbestaetigung': neue Liste
   cancellationConfirmations (Vertraege mit Bestaetigung in Status
   ACTIVE/DRAFT/CANCELLED) + Filter-Option im Cockpit-Dropdown. Eigene
   Liste, weil bereits CANCELLED-Vertraege mangels Issue sonst nicht in
   der Cockpit-Liste auftauchen.

Beides lokal verifiziert (Helper: ACTIVE->CANCELLED + endDate; Cockpit:
Vertrag erscheint in der Liste mit korrektem Status).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-08 15:13:05 +02:00
co-authored by Claude Opus 4.8
parent 5423c83009
commit 8169c74d8e
7 changed files with 159 additions and 20 deletions
@@ -102,6 +102,9 @@ export interface ReportedMeterReading {
export interface CockpitResult {
contracts: CockpitContract[];
// Verträge mit hinterlegter Kündigungsbestätigung (Dokument und/oder Datum)
// in Status ACTIVE/DRAFT/CANCELLED als eigener Filter im Cockpit.
cancellationConfirmations: CockpitContract[];
documentAlerts: DocumentAlert[];
reportedReadings: ReportedMeterReading[];
summary: CockpitSummary;
@@ -775,8 +778,55 @@ export async function getCockpitData(opts?: { customerIds?: number[] }): Promise
// Gemeldete Zählerstände (REPORTED Status)
const reportedReadings = await getReportedMeterReadings(opts?.customerIds);
// Filter „Kündigungsbestätigung vorhanden": alle Verträge mit
// cancellationConfirmationPath und/oder -Date in Status ACTIVE/DRAFT/
// CANCELLED. Eigene Liste, weil solche Verträge (v.a. bereits CANCELLED)
// sonst mangels „Issue" nicht in der Cockpit-Liste auftauchen.
const CANCEL_CONF_STATUSES: ContractStatus[] = ['ACTIVE', 'DRAFT', 'CANCELLED'];
const cancellationConfirmations: CockpitContract[] = contracts
.filter(
(c) =>
(c.cancellationConfirmationPath || c.cancellationConfirmationDate) &&
CANCEL_CONF_STATUSES.includes(c.status),
)
.map((c) => {
const customerName =
c.customer.companyName || `${c.customer.firstName} ${c.customer.lastName}`;
const dateStr = c.cancellationConfirmationDate
? new Date(c.cancellationConfirmationDate).toLocaleDateString('de-DE')
: null;
const parts: string[] = [];
if (c.cancellationConfirmationPath) parts.push('Dokument');
if (dateStr) parts.push(`Datum ${dateStr}`);
return {
id: c.id,
contractNumber: c.contractNumber,
type: c.type,
status: c.status,
customer: {
id: c.customer.id,
customerNumber: c.customer.customerNumber,
name: customerName,
},
provider: c.provider ? { id: c.provider.id, name: c.provider.name } : undefined,
tariff: c.tariff ? { id: c.tariff.id, name: c.tariff.name } : undefined,
providerName: c.providerName || undefined,
tariffName: c.tariffName || undefined,
issues: [
{
type: 'has_cancellation_confirmation',
label: 'Kündigungsbestätigung vorhanden',
urgency: 'none' as UrgencyLevel,
details: parts.join(' · ') || 'Kündigungsbestätigung hinterlegt',
},
],
highestUrgency: 'none' as UrgencyLevel,
};
});
return {
contracts: cockpitContracts,
cancellationConfirmations,
documentAlerts,
reportedReadings,
summary,