From 0b78323c7b2caae6ee7cf2f60eba81dab51bdbaa Mon Sep 17 00:00:00 2001 From: duffyduck Date: Mon, 6 Jul 2026 00:22:03 +0200 Subject: [PATCH] =?UTF-8?q?Vertrag:=20neuer=20Status=20ONGOING=20(L=C3=A4u?= =?UTF-8?q?ft=20weiter=20auf=20unbestimmte=20Zeit)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enum-Wert ONGOING neu, MariaDB-Migration (ans Ende gehängt, damit keine Rows umgeschrieben werden). Prisma Client neu generiert. Cockpit-Logik: ONGOING wird in die Query mit reingeholt (damit DSGVO-/Consent-Warnungen weiter greifen), aber alle Fristen- Blöcke pro Vertrag geskippt: - Kündigungsfrist - Vertragsende - Zwischenrechnung-Frist Kein Skip für Daten-Qualität (fehlende Adresse, Bank, Portal- Zugang etc.) – die sind auch für unbefristete Verträge relevant. Frontend: statusLabels/statusVariants/statusDescriptions in ContractDetail, ContractList, CustomerDetail und ContractDetailModal um ONGOING ergänzt. Status-Select im ContractForm bekommt die neue Option zwischen ACTIVE und CANCELLED. Co-Authored-By: Claude Opus 4.7 --- .../migration.sql | 11 ++++++++++ backend/prisma/schema.prisma | 5 +++++ .../src/services/contractCockpit.service.ts | 22 ++++++++++++++----- .../contracts/ContractDetailModal.tsx | 2 ++ .../src/pages/contracts/ContractDetail.tsx | 3 +++ frontend/src/pages/contracts/ContractForm.tsx | 2 ++ frontend/src/pages/contracts/ContractList.tsx | 3 +++ .../src/pages/customers/CustomerDetail.tsx | 2 ++ frontend/src/types/index.ts | 2 +- 9 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 backend/prisma/migrations/20260704100000_contract_status_ongoing/migration.sql 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