diff --git a/backend/prisma/migrations/20260704100000_contract_status_ongoing/migration.sql b/backend/prisma/migrations/20260704100000_contract_status_ongoing/migration.sql new file mode 100644 index 00000000..7dcdb723 --- /dev/null +++ b/backend/prisma/migrations/20260704100000_contract_status_ongoing/migration.sql @@ -0,0 +1,11 @@ +-- Neuer Vertrags-Status ONGOING ("Läuft weiter auf unbestimmte Zeit"). +-- Verträge in diesem Status werden vom Cockpit bei Fristen-/Reminder- +-- Berechnungen ignoriert, weil es kein Ende gibt, das getriggered werden +-- könnte. MariaDB ENUM: neue Werte müssen an das Ende gehängt werden, +-- sonst verschieben sich die internen Indices und alle bestehenden Rows +-- werden neu geschrieben. + +ALTER TABLE `Contract` + MODIFY COLUMN `status` + ENUM('DRAFT', 'PENDING', 'ACTIVE', 'CANCELLED', 'EXPIRED', 'DEACTIVATED', 'ONGOING') + NOT NULL DEFAULT 'DRAFT'; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 364e82ae..efc651ee 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -644,6 +644,11 @@ enum ContractStatus { CANCELLED EXPIRED DEACTIVATED + // "Läuft weiter auf unbestimmte Zeit" – Vertrag ist unbefristet aktiv. + // Cockpit-Fristen (Kündigungsvorlauf, Verlängerungs-Reminder etc.) + // ignorieren diesen Status, weil es kein Ende gibt, das getriggered + // werden könnte. + ONGOING } model Contract { diff --git a/backend/src/services/contractCockpit.service.ts b/backend/src/services/contractCockpit.service.ts index fb447370..f88c084e 100644 --- a/backend/src/services/contractCockpit.service.ts +++ b/backend/src/services/contractCockpit.service.ts @@ -198,11 +198,13 @@ export async function getCockpitData(opts?: { customerIds?: number[] }): Promise ? { customerId: { in: opts.customerIds } } : {}; - // Lade alle relevanten Verträge (inkl. CANCELLED/DEACTIVATED für Schlussrechnung-Check) + // Lade alle relevanten Verträge (inkl. CANCELLED/DEACTIVATED für + // Schlussrechnung-Check, inkl. ONGOING damit DSGVO-/Consent-Warnungen + // weiter greifen – Fristen-Blöcke werden pro Vertrag geskippt, s.u.). const contracts = await prisma.contract.findMany({ where: { status: { - in: ['ACTIVE', 'PENDING', 'DRAFT', 'CANCELLED', 'DEACTIVATED', 'EXPIRED'], + in: ['ACTIVE', 'PENDING', 'DRAFT', 'CANCELLED', 'DEACTIVATED', 'EXPIRED', 'ONGOING'], }, ...customerScopeFilter, }, @@ -354,9 +356,16 @@ export async function getCockpitData(opts?: { customerIds?: number[] }): Promise // Prüfen ob aktiver Folgevertrag existiert - dann keine Kündigungswarnungen nötig const hasActiveFollowUp = contract.followUpContract?.status === 'ACTIVE'; + // ONGOING = "Läuft weiter auf unbestimmte Zeit": es gibt kein Ende, + // also alle Fristen-bezogenen Blöcke (Kündigung, Verlängerung, + // Lieferbestätigung) für diesen Vertrag komplett skippen. DSGVO-/ + // Consent-Checks laufen weiter, weil die pro Kunde greifen, nicht + // pro Vertrag. + const skipDeadlines = contract.status === 'ONGOING'; + // 1. KÜNDIGUNGSFRIST (nur wenn kein aktiver Folgevertrag UND Snooze nicht aktiv) // Snooze unterdrückt NUR Fristen-bezogene Warnungen! - if (!hasActiveFollowUp && !snoozeActive) { + if (!hasActiveFollowUp && !snoozeActive && !skipDeadlines) { const cancellationDeadline = calculateCancellationDeadline( contract.endDate, contract.cancellationPeriod?.code @@ -401,9 +410,9 @@ export async function getCockpitData(opts?: { customerIds?: number[] }): Promise } } - // 2. VERTRAGSENDE (nur wenn Snooze nicht aktiv) + // 2. VERTRAGSENDE (nur wenn Snooze nicht aktiv, und nicht ONGOING) // Snooze unterdrückt NUR Fristen-bezogene Warnungen! - if (!snoozeActive) { + if (!snoozeActive && !skipDeadlines) { const daysToEnd = daysUntil(contract.endDate); if (daysToEnd !== null && daysToEnd <= okDays) { const urgency = getUrgencyByDays(daysToEnd, criticalDays, warningDays, okDays); @@ -621,7 +630,8 @@ export async function getCockpitData(opts?: { customerIds?: number[] }): Promise // 13b. ZWISCHENRECHNUNG FEHLT/ÜBERFÄLLIG (wenn Vertrag > 12 Monate läuft) // Für alle Status außer DRAFT und nicht gekündigt/deaktiviert // Auch EXPIRED zählt hier, da der Vertrag ohne Kündigung weiterläuft! - if (contract.startDate && contract.status !== 'DRAFT' && !isContractTerminated) { + // ONGOING wird ebenfalls ausgeschlossen – ist ein Fristen-Check. + if (contract.startDate && contract.status !== 'DRAFT' && !isContractTerminated && !skipDeadlines) { const startDate = new Date(contract.startDate); startDate.setHours(0, 0, 0, 0); const daysSinceStart = Math.floor((now.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)); diff --git a/frontend/src/components/contracts/ContractDetailModal.tsx b/frontend/src/components/contracts/ContractDetailModal.tsx index a95faee1..d29b63b8 100644 --- a/frontend/src/components/contracts/ContractDetailModal.tsx +++ b/frontend/src/components/contracts/ContractDetailModal.tsx @@ -24,6 +24,7 @@ const statusLabels: Record = { CANCELLED: 'Gekündigt', EXPIRED: 'Abgelaufen', DEACTIVATED: 'Deaktiviert', + ONGOING: 'Läuft weiter (unbestimmt)', }; const statusVariants: Record = { @@ -33,6 +34,7 @@ const statusVariants: Record = { CANCELLED: 'Gekündigt', EXPIRED: 'Abgelaufen', DEACTIVATED: 'Deaktiviert', + ONGOING: 'Läuft weiter (unbestimmt)', }; const statusVariants: Record = { @@ -51,6 +52,7 @@ const statusVariants: Record = { CANCELLED: 'Gekündigt', EXPIRED: 'Abgelaufen', DEACTIVATED: 'Deaktiviert', + ONGOING: 'Läuft weiter (unbestimmt)', }; const statusVariants: Record = { @@ -43,6 +44,7 @@ const statusVariants: Record