Vertrag: neuer Status ONGOING (Läuft weiter auf unbestimmte Zeit)

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 00:22:03 +02:00
parent cc6fa9be47
commit 0b78323c7b
9 changed files with 45 additions and 7 deletions
@@ -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';
+5
View File
@@ -644,6 +644,11 @@ enum ContractStatus {
CANCELLED CANCELLED
EXPIRED EXPIRED
DEACTIVATED 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 { model Contract {
@@ -198,11 +198,13 @@ export async function getCockpitData(opts?: { customerIds?: number[] }): Promise
? { customerId: { in: opts.customerIds } } ? { 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({ const contracts = await prisma.contract.findMany({
where: { where: {
status: { status: {
in: ['ACTIVE', 'PENDING', 'DRAFT', 'CANCELLED', 'DEACTIVATED', 'EXPIRED'], in: ['ACTIVE', 'PENDING', 'DRAFT', 'CANCELLED', 'DEACTIVATED', 'EXPIRED', 'ONGOING'],
}, },
...customerScopeFilter, ...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 // Prüfen ob aktiver Folgevertrag existiert - dann keine Kündigungswarnungen nötig
const hasActiveFollowUp = contract.followUpContract?.status === 'ACTIVE'; 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) // 1. KÜNDIGUNGSFRIST (nur wenn kein aktiver Folgevertrag UND Snooze nicht aktiv)
// Snooze unterdrückt NUR Fristen-bezogene Warnungen! // Snooze unterdrückt NUR Fristen-bezogene Warnungen!
if (!hasActiveFollowUp && !snoozeActive) { if (!hasActiveFollowUp && !snoozeActive && !skipDeadlines) {
const cancellationDeadline = calculateCancellationDeadline( const cancellationDeadline = calculateCancellationDeadline(
contract.endDate, contract.endDate,
contract.cancellationPeriod?.code 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! // Snooze unterdrückt NUR Fristen-bezogene Warnungen!
if (!snoozeActive) { if (!snoozeActive && !skipDeadlines) {
const daysToEnd = daysUntil(contract.endDate); const daysToEnd = daysUntil(contract.endDate);
if (daysToEnd !== null && daysToEnd <= okDays) { if (daysToEnd !== null && daysToEnd <= okDays) {
const urgency = getUrgencyByDays(daysToEnd, criticalDays, warningDays, 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) // 13b. ZWISCHENRECHNUNG FEHLT/ÜBERFÄLLIG (wenn Vertrag > 12 Monate läuft)
// Für alle Status außer DRAFT und nicht gekündigt/deaktiviert // 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! // 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); const startDate = new Date(contract.startDate);
startDate.setHours(0, 0, 0, 0); startDate.setHours(0, 0, 0, 0);
const daysSinceStart = Math.floor((now.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)); const daysSinceStart = Math.floor((now.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
@@ -24,6 +24,7 @@ const statusLabels: Record<ContractStatus, string> = {
CANCELLED: 'Gekündigt', CANCELLED: 'Gekündigt',
EXPIRED: 'Abgelaufen', EXPIRED: 'Abgelaufen',
DEACTIVATED: 'Deaktiviert', DEACTIVATED: 'Deaktiviert',
ONGOING: 'Läuft weiter (unbestimmt)',
}; };
const statusVariants: Record<ContractStatus, 'success' | 'warning' | 'danger' | 'default'> = { const statusVariants: Record<ContractStatus, 'success' | 'warning' | 'danger' | 'default'> = {
@@ -33,6 +34,7 @@ const statusVariants: Record<ContractStatus, 'success' | 'warning' | 'danger' |
EXPIRED: 'danger', EXPIRED: 'danger',
DRAFT: 'default', DRAFT: 'default',
DEACTIVATED: 'default', DEACTIVATED: 'default',
ONGOING: 'success',
}; };
interface ContractDetailModalProps { interface ContractDetailModalProps {
@@ -42,6 +42,7 @@ const statusLabels: Record<ContractStatus, string> = {
CANCELLED: 'Gekündigt', CANCELLED: 'Gekündigt',
EXPIRED: 'Abgelaufen', EXPIRED: 'Abgelaufen',
DEACTIVATED: 'Deaktiviert', DEACTIVATED: 'Deaktiviert',
ONGOING: 'Läuft weiter (unbestimmt)',
}; };
const statusVariants: Record<ContractStatus, 'success' | 'warning' | 'danger' | 'default'> = { const statusVariants: Record<ContractStatus, 'success' | 'warning' | 'danger' | 'default'> = {
@@ -51,6 +52,7 @@ const statusVariants: Record<ContractStatus, 'success' | 'warning' | 'danger' |
EXPIRED: 'danger', EXPIRED: 'danger',
DRAFT: 'default', DRAFT: 'default',
DEACTIVATED: 'default', DEACTIVATED: 'default',
ONGOING: 'success',
}; };
// Status-Erklärungen für Info-Modal // Status-Erklärungen für Info-Modal
@@ -58,6 +60,7 @@ const statusDescriptions = [
{ status: 'DRAFT', label: 'Entwurf', description: 'Vertrag wird noch vorbereitet', color: 'text-gray-600' }, { status: 'DRAFT', label: 'Entwurf', description: 'Vertrag wird noch vorbereitet', color: 'text-gray-600' },
{ status: 'PENDING', label: 'Ausstehend', description: 'Wartet auf Aktivierung', color: 'text-yellow-600' }, { status: 'PENDING', label: 'Ausstehend', description: 'Wartet auf Aktivierung', color: 'text-yellow-600' },
{ status: 'ACTIVE', label: 'Aktiv', description: 'Vertrag läuft normal', color: 'text-green-600' }, { status: 'ACTIVE', label: 'Aktiv', description: 'Vertrag läuft normal', color: 'text-green-600' },
{ status: 'ONGOING', label: 'Läuft weiter (unbestimmt)', description: 'Vertrag läuft unbefristet alle Fristen im Cockpit werden ignoriert', color: 'text-green-700' },
{ status: 'EXPIRED', label: 'Abgelaufen', description: 'Laufzeit vorbei, läuft aber ohne Kündigung weiter', color: 'text-orange-600' }, { status: 'EXPIRED', label: 'Abgelaufen', description: 'Laufzeit vorbei, läuft aber ohne Kündigung weiter', color: 'text-orange-600' },
{ status: 'CANCELLED', label: 'Gekündigt', description: 'Aktive Kündigung eingereicht, Vertrag endet', color: 'text-red-600' }, { status: 'CANCELLED', label: 'Gekündigt', description: 'Aktive Kündigung eingereicht, Vertrag endet', color: 'text-red-600' },
{ status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' }, { status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' },
@@ -45,6 +45,7 @@ const statusOptions = [
{ value: 'DRAFT', label: 'Entwurf' }, { value: 'DRAFT', label: 'Entwurf' },
{ value: 'PENDING', label: 'Ausstehend' }, { value: 'PENDING', label: 'Ausstehend' },
{ value: 'ACTIVE', label: 'Aktiv' }, { value: 'ACTIVE', label: 'Aktiv' },
{ value: 'ONGOING', label: 'Läuft weiter (unbestimmt)' },
{ value: 'CANCELLED', label: 'Gekündigt' }, { value: 'CANCELLED', label: 'Gekündigt' },
{ value: 'EXPIRED', label: 'Abgelaufen' }, { value: 'EXPIRED', label: 'Abgelaufen' },
{ value: 'DEACTIVATED', label: 'Deaktiviert' }, { value: 'DEACTIVATED', label: 'Deaktiviert' },
@@ -55,6 +56,7 @@ const statusDescriptions = [
{ status: 'DRAFT', label: 'Entwurf', description: 'Vertrag wird noch vorbereitet', color: 'text-gray-600' }, { status: 'DRAFT', label: 'Entwurf', description: 'Vertrag wird noch vorbereitet', color: 'text-gray-600' },
{ status: 'PENDING', label: 'Ausstehend', description: 'Wartet auf Aktivierung', color: 'text-yellow-600' }, { status: 'PENDING', label: 'Ausstehend', description: 'Wartet auf Aktivierung', color: 'text-yellow-600' },
{ status: 'ACTIVE', label: 'Aktiv', description: 'Vertrag läuft normal', color: 'text-green-600' }, { status: 'ACTIVE', label: 'Aktiv', description: 'Vertrag läuft normal', color: 'text-green-600' },
{ status: 'ONGOING', label: 'Läuft weiter (unbestimmt)', description: 'Vertrag läuft unbefristet alle Fristen im Cockpit werden ignoriert', color: 'text-green-700' },
{ status: 'EXPIRED', label: 'Abgelaufen', description: 'Laufzeit vorbei, läuft aber ohne Kündigung weiter', color: 'text-orange-600' }, { status: 'EXPIRED', label: 'Abgelaufen', description: 'Laufzeit vorbei, läuft aber ohne Kündigung weiter', color: 'text-orange-600' },
{ status: 'CANCELLED', label: 'Gekündigt', description: 'Aktive Kündigung eingereicht, Vertrag endet', color: 'text-red-600' }, { status: 'CANCELLED', label: 'Gekündigt', description: 'Aktive Kündigung eingereicht, Vertrag endet', color: 'text-red-600' },
{ status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' }, { status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' },
@@ -34,6 +34,7 @@ const statusLabels: Record<ContractStatus, string> = {
CANCELLED: 'Gekündigt', CANCELLED: 'Gekündigt',
EXPIRED: 'Abgelaufen', EXPIRED: 'Abgelaufen',
DEACTIVATED: 'Deaktiviert', DEACTIVATED: 'Deaktiviert',
ONGOING: 'Läuft weiter (unbestimmt)',
}; };
const statusVariants: Record<ContractStatus, 'success' | 'warning' | 'danger' | 'default'> = { const statusVariants: Record<ContractStatus, 'success' | 'warning' | 'danger' | 'default'> = {
@@ -43,6 +44,7 @@ const statusVariants: Record<ContractStatus, 'success' | 'warning' | 'danger' |
EXPIRED: 'danger', EXPIRED: 'danger',
DRAFT: 'default', DRAFT: 'default',
DEACTIVATED: 'default', DEACTIVATED: 'default',
ONGOING: 'success',
}; };
// Status-Erklärungen für Info-Modal // Status-Erklärungen für Info-Modal
@@ -50,6 +52,7 @@ const statusDescriptions = [
{ status: 'DRAFT', label: 'Entwurf', description: 'Vertrag wird noch vorbereitet', color: 'text-gray-600' }, { status: 'DRAFT', label: 'Entwurf', description: 'Vertrag wird noch vorbereitet', color: 'text-gray-600' },
{ status: 'PENDING', label: 'Ausstehend', description: 'Wartet auf Aktivierung', color: 'text-yellow-600' }, { status: 'PENDING', label: 'Ausstehend', description: 'Wartet auf Aktivierung', color: 'text-yellow-600' },
{ status: 'ACTIVE', label: 'Aktiv', description: 'Vertrag läuft normal', color: 'text-green-600' }, { status: 'ACTIVE', label: 'Aktiv', description: 'Vertrag läuft normal', color: 'text-green-600' },
{ status: 'ONGOING', label: 'Läuft weiter (unbestimmt)', description: 'Vertrag läuft unbefristet alle Fristen im Cockpit werden ignoriert', color: 'text-green-700' },
{ status: 'EXPIRED', label: 'Abgelaufen', description: 'Laufzeit vorbei, läuft aber ohne Kündigung weiter', color: 'text-orange-600' }, { status: 'EXPIRED', label: 'Abgelaufen', description: 'Laufzeit vorbei, läuft aber ohne Kündigung weiter', color: 'text-orange-600' },
{ status: 'CANCELLED', label: 'Gekündigt', description: 'Aktive Kündigung eingereicht, Vertrag endet', color: 'text-red-600' }, { status: 'CANCELLED', label: 'Gekündigt', description: 'Aktive Kündigung eingereicht, Vertrag endet', color: 'text-red-600' },
{ status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' }, { status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' },
@@ -1718,12 +1718,14 @@ function ContractsTab({
EXPIRED: 'danger', EXPIRED: 'danger',
DRAFT: 'default', DRAFT: 'default',
DEACTIVATED: 'default', DEACTIVATED: 'default',
ONGOING: 'success',
}; };
const statusDescriptions = [ const statusDescriptions = [
{ status: 'DRAFT', label: 'Entwurf', description: 'Vertrag wird noch vorbereitet', color: 'text-gray-600' }, { status: 'DRAFT', label: 'Entwurf', description: 'Vertrag wird noch vorbereitet', color: 'text-gray-600' },
{ status: 'PENDING', label: 'Ausstehend', description: 'Wartet auf Aktivierung', color: 'text-yellow-600' }, { status: 'PENDING', label: 'Ausstehend', description: 'Wartet auf Aktivierung', color: 'text-yellow-600' },
{ status: 'ACTIVE', label: 'Aktiv', description: 'Vertrag läuft normal', color: 'text-green-600' }, { status: 'ACTIVE', label: 'Aktiv', description: 'Vertrag läuft normal', color: 'text-green-600' },
{ status: 'ONGOING', label: 'Läuft weiter (unbestimmt)', description: 'Vertrag läuft unbefristet alle Fristen im Cockpit werden ignoriert', color: 'text-green-700' },
{ status: 'EXPIRED', label: 'Abgelaufen', description: 'Laufzeit vorbei, läuft aber ohne Kündigung weiter', color: 'text-orange-600' }, { status: 'EXPIRED', label: 'Abgelaufen', description: 'Laufzeit vorbei, läuft aber ohne Kündigung weiter', color: 'text-orange-600' },
{ status: 'CANCELLED', label: 'Gekündigt', description: 'Aktive Kündigung eingereicht, Vertrag endet', color: 'text-red-600' }, { status: 'CANCELLED', label: 'Gekündigt', description: 'Aktive Kündigung eingereicht, Vertrag endet', color: 'text-red-600' },
{ status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' }, { status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' },
+1 -1
View File
@@ -424,7 +424,7 @@ export interface Tariff {
} }
export type ContractType = 'ELECTRICITY' | 'GAS' | 'DSL' | 'CABLE' | 'FIBER' | 'MOBILE' | 'TV' | 'CAR_INSURANCE'; export type ContractType = 'ELECTRICITY' | 'GAS' | 'DSL' | 'CABLE' | 'FIBER' | 'MOBILE' | 'TV' | 'CAR_INSURANCE';
export type ContractStatus = 'DRAFT' | 'PENDING' | 'ACTIVE' | 'CANCELLED' | 'EXPIRED' | 'DEACTIVATED'; export type ContractStatus = 'DRAFT' | 'PENDING' | 'ACTIVE' | 'CANCELLED' | 'EXPIRED' | 'DEACTIVATED' | 'ONGOING';
export interface Contract { export interface Contract {
id: number; id: number;