From fc3131059b2ae25a101b5e6a375815c0232ca847 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Thu, 13 Aug 2026 18:14:34 +0200 Subject: [PATCH] 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 --- .../migration.sql | 15 ++++++ backend/prisma/schema.prisma | 3 ++ .../src/controllers/contract.controller.ts | 4 +- backend/src/routes/upload.routes.ts | 13 +++-- .../src/services/contractCockpit.service.ts | 20 +++++--- .../contractStatusScheduler.service.ts | 49 +++++++++++++++++-- docs/todo.md | 22 +++++++++ .../contracts/ContractDetailModal.tsx | 6 ++- .../email/AssignToContractModal.tsx | 3 +- .../src/pages/contracts/ContractDetail.tsx | 9 ++-- frontend/src/pages/contracts/ContractForm.tsx | 6 ++- frontend/src/pages/contracts/ContractList.tsx | 9 ++-- .../src/pages/customers/CustomerDetail.tsx | 22 +++++++-- frontend/src/types/index.ts | 2 +- 14 files changed, 150 insertions(+), 33 deletions(-) create mode 100644 backend/prisma/migrations/20260813200000_contract_status_cancelled_confirmed/migration.sql diff --git a/backend/prisma/migrations/20260813200000_contract_status_cancelled_confirmed/migration.sql b/backend/prisma/migrations/20260813200000_contract_status_cancelled_confirmed/migration.sql new file mode 100644 index 00000000..458a4ce5 --- /dev/null +++ b/backend/prisma/migrations/20260813200000_contract_status_cancelled_confirmed/migration.sql @@ -0,0 +1,15 @@ +-- Neuer Vertragsstatus CANCELLED_CONFIRMED ("Gekündigt / bestätigt"): +-- gesetzt, sobald eine Kündigungsbestätigung (Datum/Dokument) vorliegt. +-- CANCELLED bleibt "Gekündigt / Bestätigung abwarten" (Kündigungsschreiben raus). +-- MODIFY COLUMN ist idempotent (erneutes Ausführen ergibt denselben Zustand). +ALTER TABLE `Contract` MODIFY COLUMN `status` + ENUM('DRAFT','PENDING','ACTIVE','CANCELLED','CANCELLED_CONFIRMED','EXPIRED','DEACTIVATED','ONGOING') + NOT NULL DEFAULT 'DRAFT'; + +-- Daten-Migration: bestehende CANCELLED-Verträge auf CANCELLED_CONFIRMED heben. +-- Die alte Logik setzte CANCELLED NUR, wenn eine Kündigungsbestätigung +-- (Datum ODER Dokument) vorlag – diese Verträge sind also bereits bestätigt. +-- CANCELLED bedeutet ab jetzt "Bestätigung abwarten" (nur Kündigungsschreiben). +UPDATE `Contract` SET `status` = 'CANCELLED_CONFIRMED' + WHERE `status` = 'CANCELLED' + AND (`cancellationConfirmationPath` IS NOT NULL OR `cancellationConfirmationDate` IS NOT NULL); diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index c3400dee..762de0d0 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -703,7 +703,10 @@ enum ContractStatus { DRAFT PENDING ACTIVE + // Kündigungsschreiben raus, Bestätigung des Anbieters steht noch aus. CANCELLED + // Kündigung vom Anbieter bestätigt (Bestätigungsdatum/-dokument liegt vor). + CANCELLED_CONFIRMED EXPIRED DEACTIVATED // "Läuft weiter auf unbestimmte Zeit" – Vertrag ist unbefristet aktiv. diff --git a/backend/src/controllers/contract.controller.ts b/backend/src/controllers/contract.controller.ts index 13174d86..b150f5bc 100644 --- a/backend/src/controllers/contract.controller.ts +++ b/backend/src/controllers/contract.controller.ts @@ -259,8 +259,8 @@ export async function updateContract(req: AuthRequest, res: Response): Promise diff --git a/backend/src/routes/upload.routes.ts b/backend/src/routes/upload.routes.ts index 0e4a7a3f..053ea98d 100644 --- a/backend/src/routes/upload.routes.ts +++ b/backend/src/routes/upload.routes.ts @@ -14,7 +14,7 @@ import { } from '../utils/accessControl.js'; import { validateOptionalIsoDate } from '../utils/sanitize.js'; import { validateUploadedFile } from '../middleware/uploadFileTypeValidator.js'; -import { maybeCancelOnCancellationConfirmation } from '../services/contractStatusScheduler.service.js'; +import { maybeCancelOnCancellationConfirmation, maybeMarkAwaitingConfirmationOnLetter } from '../services/contractStatusScheduler.service.js'; import { deleteFileAbsolute, deleteUploadByRelativePath } from '../utils/fileCleanup.js'; // Pentest 56.1 (HIGH, 2026-06-01): Upload-Endpoints prüften nur die @@ -691,12 +691,17 @@ async function handleContractDocumentUpload( data: updateData, }); - // Wenn eine Kündigungsbestätigung (nicht "Optionen") hochgeladen wurde: - // Vertrag ACTIVE → CANCELLED und Vertragsende = Kündigungsdatum. - // "Optionen" ist für Vertrags-Änderungen gedacht, nicht für echte Kündigungen. + // Kündigungsbestätigung (nicht "Optionen") hochgeladen → Vertrag auf + // "Gekündigt / bestätigt" (CANCELLED_CONFIRMED) + Vertragsende = Kündigungs- + // datum. "Optionen" ist für Vertrags-Änderungen gedacht, nicht für Kündigungen. if (fieldName === 'cancellationConfirmationPath') { await maybeCancelOnCancellationConfirmation(contractId, req); } + // Kündigungsschreiben hochgeladen → Vertrag auf "Gekündigt / Bestätigung + // abwarten" (CANCELLED), sofern noch nicht (bestätigt) gekündigt. + if (fieldName === 'cancellationLetterPath') { + await maybeMarkAwaitingConfirmationOnLetter(contractId, req); + } res.json({ success: true, diff --git a/backend/src/services/contractCockpit.service.ts b/backend/src/services/contractCockpit.service.ts index 00510e28..dbfb23c7 100644 --- a/backend/src/services/contractCockpit.service.ts +++ b/backend/src/services/contractCockpit.service.ts @@ -207,7 +207,7 @@ export async function getCockpitData(opts?: { customerIds?: number[] }): Promise const contracts = await prisma.contract.findMany({ where: { status: { - in: ['ACTIVE', 'PENDING', 'DRAFT', 'CANCELLED', 'DEACTIVATED', 'EXPIRED', 'ONGOING'], + in: ['ACTIVE', 'PENDING', 'DRAFT', 'CANCELLED', 'CANCELLED_CONFIRMED', 'DEACTIVATED', 'EXPIRED', 'ONGOING'], }, ...customerScopeFilter, }, @@ -363,8 +363,10 @@ export async function getCockpitData(opts?: { customerIds?: number[] }): Promise // Fristen semantisch keinen Sinn ergeben: // - ONGOING : "Läuft weiter auf unbestimmte Zeit" → kein Ende // - DEACTIVATED : manuell beendet/archiviert → nichts mehr zu erinnern - // - CANCELLED : Kündigung schon eingereicht → Vertragsende ist - // die Konsequenz der Kündigung, keine Frist mehr + // - CANCELLED_CONFIRMED : Kündigung vom Anbieter bestätigt → + // Vertragsende steht fest, keine Frist mehr. + // (CANCELLED = nur "Bestätigung abwarten" skippt NICHT, + // damit die "Kündigungsbestätigung fehlt"-Warnung greift.) // Die Schlussrechnung-Fehlt-Warnung (Block 13a) bleibt aktiv, weil // sie WEGEN dieser Status feuert (kein separater Guard). DSGVO-/ // Consent-Checks (Block 8) sind pro Kunde, nicht pro Vertrag, und @@ -375,7 +377,7 @@ export async function getCockpitData(opts?: { customerIds?: number[] }): Promise const skipDeadlines = contract.status === 'ONGOING' || contract.status === 'DEACTIVATED' - || contract.status === 'CANCELLED'; + || contract.status === 'CANCELLED_CONFIRMED'; // 1. KÜNDIGUNGSFRIST (nur wenn kein aktiver Folgevertrag UND Snooze nicht aktiv) // Snooze unterdrückt NUR Fristen-bezogene Warnungen! @@ -628,9 +630,11 @@ export async function getCockpitData(opts?: { customerIds?: number[] }): Promise const now = new Date(); now.setHours(0, 0, 0, 0); - // 13a. SCHLUSSRECHNUNG FEHLT (nur wenn Vertrag gekündigt/deaktiviert ist) - // "Beendet" = CANCELLED oder DEACTIVATED (nicht nur Laufzeit abgelaufen!) - const isContractTerminated = contract.status === 'CANCELLED' || contract.status === 'DEACTIVATED'; + // 13a. SCHLUSSRECHNUNG FEHLT (nur wenn Vertrag bestätigt gekündigt/deaktiviert) + // "Beendet" = CANCELLED_CONFIRMED (Kündigung bestätigt) oder DEACTIVATED + // (nicht nur Laufzeit abgelaufen; NICHT "Bestätigung abwarten"/CANCELLED, + // da dort das Vertragsende noch nicht feststeht). + const isContractTerminated = contract.status === 'CANCELLED_CONFIRMED' || contract.status === 'DEACTIVATED'; if (isContractTerminated) { const hasFinalInvoice = invoices.some(inv => inv.invoiceType === 'FINAL'); @@ -783,7 +787,7 @@ export async function getCockpitData(opts?: { customerIds?: number[] }): Promise // CANCELLED/EXPIRED. Eigene Liste, weil solche Verträge (v.a. bereits // CANCELLED/EXPIRED) sonst mangels „Issue" nicht in der Cockpit-Liste // auftauchen. - const CANCEL_CONF_STATUSES: ContractStatus[] = ['ACTIVE', 'DRAFT', 'CANCELLED', 'EXPIRED']; + const CANCEL_CONF_STATUSES: ContractStatus[] = ['ACTIVE', 'DRAFT', 'CANCELLED', 'CANCELLED_CONFIRMED', 'EXPIRED']; const cancellationConfirmations: CockpitContract[] = contracts .filter( (c) => diff --git a/backend/src/services/contractStatusScheduler.service.ts b/backend/src/services/contractStatusScheduler.service.ts index e11223b0..3bbd7ba9 100644 --- a/backend/src/services/contractStatusScheduler.service.ts +++ b/backend/src/services/contractStatusScheduler.service.ts @@ -195,9 +195,13 @@ export async function maybeCancelOnCancellationConfirmation( const updateData: Record = {}; const changes: Record = {}; - if (c.status === 'ACTIVE') { - updateData.status = 'CANCELLED'; - changes.status = { vorher: 'ACTIVE', nachher: 'CANCELLED' }; + // Kündigungsbestätigung liegt vor (Datum/Dokument) → "Gekündigt / bestätigt". + // Von jedem noch "lebenden" Status heben – auch aus "Gekündigt / Bestätigung + // abwarten" (CANCELLED) – aber NIE aus DRAFT (Vorlage) oder DEACTIVATED + // (archiviert). Ein bereits bestätigter Vertrag bleibt bestätigt (No-op). + if (['ACTIVE', 'PENDING', 'ONGOING', 'EXPIRED', 'CANCELLED'].includes(c.status)) { + updateData.status = 'CANCELLED_CONFIRMED'; + changes.status = { vorher: c.status, nachher: 'CANCELLED_CONFIRMED' }; } // Vertragsende = Kündigungsdatum (Bestätigungsdatum), falls vorhanden. @@ -227,6 +231,45 @@ export async function maybeCancelOnCancellationConfirmation( }); } +/** + * Setzt einen Vertrag automatisch auf "Gekündigt / Bestätigung abwarten" + * (CANCELLED), sobald ein Kündigungsschreiben hinterlegt wurde. Hebt nur noch + * "lebende" Verträge (ACTIVE/PENDING/ONGOING/EXPIRED) an – ein bereits bestätigt + * gekündigter Vertrag (CANCELLED_CONFIRMED) wird NICHT zurückgestuft, ein + * Entwurf (DRAFT) oder archivierter (DEACTIVATED) nicht angefasst. + */ +export async function maybeMarkAwaitingConfirmationOnLetter( + contractId: number, + req: unknown, +): Promise { + const c = await prisma.contract.findUnique({ + where: { id: contractId }, + select: { + status: true, + contractNumber: true, + customerId: true, + cancellationLetterPath: true, + }, + }); + if (!c || !c.cancellationLetterPath) return; + if (!['ACTIVE', 'PENDING', 'ONGOING', 'EXPIRED'].includes(c.status)) return; + + await prisma.contract.update({ + where: { id: contractId }, + data: { status: 'CANCELLED' }, + }); + + await logChange({ + req, + action: 'UPDATE', + resourceType: 'Contract', + resourceId: contractId.toString(), + label: `Vertrag ${c.contractNumber} automatisch auf "Gekündigt / Bestätigung abwarten" gesetzt (Kündigungsschreiben)`, + details: { status: { vorher: c.status, nachher: 'CANCELLED' }, trigger: 'Kündigungsschreiben' }, + customerId: c.customerId, + }); +} + export async function maybeActivateOnDeliveryConfirmation( contractId: number, documentType: string, diff --git a/docs/todo.md b/docs/todo.md index e1ea00b0..89aacd41 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -97,6 +97,28 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung ## ✅ Erledigt +- [x] **🗂️ Neuer Vertragsstatus „Gekündigt / bestätigt" + Kündigungs-Workflow** (2026-08-13) + - Bisheriges **„Gekündigt"** umbenannt in **„Gekündigt / Bestätigung abwarten"** + (Status `CANCELLED`) – wird jetzt automatisch gesetzt, sobald ein + **Kündigungsschreiben** (`cancellationLetterPath`) hochgeladen wird + (aus ACTIVE/PENDING/ONGOING/EXPIRED; nie DRAFT/DEACTIVATED/bereits bestätigt). + - **Neuer Status „Gekündigt / bestätigt"** (`CANCELLED_CONFIRMED`) – automatisch, + sobald ein **Kündigungsbestätigungsdatum** vorliegt (per Bestätigungsdokument, + das das Datum füllt, ODER manuell) + Vertragsende = Kündigungsdatum. Hebt auch + aus „Bestätigung abwarten" hoch. + - **Schema:** Enum-Wert `CANCELLED_CONFIRMED`, Migration + `20260813200000_contract_status_cancelled_confirmed` (idempotentes + `MODIFY COLUMN`). **Daten-Migration:** bestehende `CANCELLED` (unter alter + Logik nur bei vorhandener Bestätigung gesetzt) → `CANCELLED_CONFIRMED`. + - **Cockpit-Semantik mitgewandert:** Fristen-Skip + „beendet" (Schlussrechnung) + gelten jetzt für `CANCELLED_CONFIRMED` (nicht mehr das reine „abwarten"); + „Kündigungsbestätigung fehlt"-Warnung greift dadurch weiter für CANCELLED. + `CANCELLED_CONFIRMED` in Ladeliste + Kündigungsbestätigungs-Filter aufgenommen. + - **Frontend:** Labels/Farben/Status-Erklärungen + Status-Dropdown in + ContractList, ContractDetail, ContractForm, ContractDetailModal, CustomerDetail + (CANCELLED = orange „abwarten", CANCELLED_CONFIRMED = rot). Verifiziert: + Schreiben→CANCELLED, Bestätigung→CANCELLED_CONFIRMED+Enddatum. + - [x] **🔒 Mass-Assignment-Schutz: Bankkarte/Adresse/Ausweis (Pentest R155)** (2026-08-13) - Controller reichten rohen `req.body` an Prisma durch → `customerId` (Owner) und `id` (PK) waren per Update mutierbar (staff-only, kein Cross-Tenant, aber diff --git a/frontend/src/components/contracts/ContractDetailModal.tsx b/frontend/src/components/contracts/ContractDetailModal.tsx index d29b63b8..7c317907 100644 --- a/frontend/src/components/contracts/ContractDetailModal.tsx +++ b/frontend/src/components/contracts/ContractDetailModal.tsx @@ -21,7 +21,8 @@ const statusLabels: Record = { DRAFT: 'Entwurf', PENDING: 'Ausstehend', ACTIVE: 'Aktiv', - CANCELLED: 'Gekündigt', + CANCELLED: 'Gekündigt / Bestätigung abwarten', + CANCELLED_CONFIRMED: 'Gekündigt / bestätigt', EXPIRED: 'Abgelaufen', DEACTIVATED: 'Deaktiviert', ONGOING: 'Läuft weiter (unbestimmt)', @@ -30,7 +31,8 @@ const statusLabels: Record = { const statusVariants: Record = { ACTIVE: 'success', PENDING: 'warning', - CANCELLED: 'danger', + CANCELLED: 'warning', + CANCELLED_CONFIRMED: 'danger', EXPIRED: 'danger', DRAFT: 'default', DEACTIVATED: 'default', diff --git a/frontend/src/components/email/AssignToContractModal.tsx b/frontend/src/components/email/AssignToContractModal.tsx index 2344bcec..7fa1c326 100644 --- a/frontend/src/components/email/AssignToContractModal.tsx +++ b/frontend/src/components/email/AssignToContractModal.tsx @@ -138,7 +138,8 @@ export default function AssignToContractModal({ px-2 py-0.5 text-xs rounded-full ${contract.status === 'ACTIVE' ? 'bg-green-100 text-green-800' : contract.status === 'PENDING' ? 'bg-yellow-100 text-yellow-800' : - contract.status === 'CANCELLED' ? 'bg-red-100 text-red-800' : + contract.status === 'CANCELLED' ? 'bg-orange-100 text-orange-800' : + contract.status === 'CANCELLED_CONFIRMED' ? 'bg-red-100 text-red-800' : 'bg-gray-100 text-gray-800'} `}> {contract.status} diff --git a/frontend/src/pages/contracts/ContractDetail.tsx b/frontend/src/pages/contracts/ContractDetail.tsx index 57976fc6..84ced351 100644 --- a/frontend/src/pages/contracts/ContractDetail.tsx +++ b/frontend/src/pages/contracts/ContractDetail.tsx @@ -43,7 +43,8 @@ const statusLabels: Record = { DRAFT: 'Entwurf', PENDING: 'Ausstehend', ACTIVE: 'Aktiv', - CANCELLED: 'Gekündigt', + CANCELLED: 'Gekündigt / Bestätigung abwarten', + CANCELLED_CONFIRMED: 'Gekündigt / bestätigt', EXPIRED: 'Abgelaufen', DEACTIVATED: 'Deaktiviert', ONGOING: 'Läuft weiter (unbestimmt)', @@ -52,7 +53,8 @@ const statusLabels: Record = { const statusVariants: Record = { ACTIVE: 'success', PENDING: 'warning', - CANCELLED: 'danger', + CANCELLED: 'warning', + CANCELLED_CONFIRMED: 'danger', EXPIRED: 'danger', DRAFT: 'default', DEACTIVATED: 'default', @@ -66,7 +68,8 @@ const statusDescriptions = [ { 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: 'CANCELLED', label: 'Gekündigt', description: 'Aktive Kündigung eingereicht, Vertrag endet', color: 'text-red-600' }, + { status: 'CANCELLED', label: 'Gekündigt / Bestätigung abwarten', description: 'Kündigungsschreiben raus – Bestätigung des Anbieters steht noch aus', color: 'text-orange-600' }, + { status: 'CANCELLED_CONFIRMED', label: 'Gekündigt / bestätigt', description: 'Kündigung vom Anbieter bestätigt, Vertrag endet', color: 'text-red-600' }, { status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' }, ]; diff --git a/frontend/src/pages/contracts/ContractForm.tsx b/frontend/src/pages/contracts/ContractForm.tsx index 8265acda..4ba9ff1f 100644 --- a/frontend/src/pages/contracts/ContractForm.tsx +++ b/frontend/src/pages/contracts/ContractForm.tsx @@ -48,7 +48,8 @@ const statusOptions = [ { value: 'PENDING', label: 'Ausstehend' }, { value: 'ACTIVE', label: 'Aktiv' }, { value: 'ONGOING', label: 'Läuft weiter (unbestimmt)' }, - { value: 'CANCELLED', label: 'Gekündigt' }, + { value: 'CANCELLED', label: 'Gekündigt / Bestätigung abwarten' }, + { value: 'CANCELLED_CONFIRMED', label: 'Gekündigt / bestätigt' }, { value: 'EXPIRED', label: 'Abgelaufen' }, { value: 'DEACTIVATED', label: 'Deaktiviert' }, ]; @@ -60,7 +61,8 @@ const statusDescriptions = [ { 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: 'CANCELLED', label: 'Gekündigt', description: 'Aktive Kündigung eingereicht, Vertrag endet', color: 'text-red-600' }, + { status: 'CANCELLED', label: 'Gekündigt / Bestätigung abwarten', description: 'Kündigungsschreiben raus – Bestätigung des Anbieters steht noch aus', color: 'text-orange-600' }, + { status: 'CANCELLED_CONFIRMED', label: 'Gekündigt / bestätigt', description: 'Kündigung vom Anbieter bestätigt, Vertrag endet', color: 'text-red-600' }, { status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' }, ]; diff --git a/frontend/src/pages/contracts/ContractList.tsx b/frontend/src/pages/contracts/ContractList.tsx index e8c73395..a313b7e8 100644 --- a/frontend/src/pages/contracts/ContractList.tsx +++ b/frontend/src/pages/contracts/ContractList.tsx @@ -31,7 +31,8 @@ const statusLabels: Record = { DRAFT: 'Entwurf', PENDING: 'Ausstehend', ACTIVE: 'Aktiv', - CANCELLED: 'Gekündigt', + CANCELLED: 'Gekündigt / Bestätigung abwarten', + CANCELLED_CONFIRMED: 'Gekündigt / bestätigt', EXPIRED: 'Abgelaufen', DEACTIVATED: 'Deaktiviert', ONGOING: 'Läuft weiter (unbestimmt)', @@ -40,7 +41,8 @@ const statusLabels: Record = { const statusVariants: Record = { ACTIVE: 'success', PENDING: 'warning', - CANCELLED: 'danger', + CANCELLED: 'warning', + CANCELLED_CONFIRMED: 'danger', EXPIRED: 'danger', DRAFT: 'default', DEACTIVATED: 'default', @@ -54,7 +56,8 @@ const statusDescriptions = [ { 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: 'CANCELLED', label: 'Gekündigt', description: 'Aktive Kündigung eingereicht, Vertrag endet', color: 'text-red-600' }, + { status: 'CANCELLED', label: 'Gekündigt / Bestätigung abwarten', description: 'Kündigungsschreiben raus – Bestätigung des Anbieters steht noch aus', color: 'text-orange-600' }, + { status: 'CANCELLED_CONFIRMED', label: 'Gekündigt / bestätigt', description: 'Kündigung vom Anbieter bestätigt, Vertrag endet', color: 'text-red-600' }, { status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' }, ]; diff --git a/frontend/src/pages/customers/CustomerDetail.tsx b/frontend/src/pages/customers/CustomerDetail.tsx index 96e149c2..5920a484 100644 --- a/frontend/src/pages/customers/CustomerDetail.tsx +++ b/frontend/src/pages/customers/CustomerDetail.tsx @@ -1556,7 +1556,8 @@ function MetersTab({ )} {ct.status} @@ -1739,10 +1740,22 @@ function ContractsTab({ CAR_INSURANCE: 'KFZ-Versicherung', }; + const statusLabels: Record = { + 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 = { ACTIVE: 'success', PENDING: 'warning', - CANCELLED: 'danger', + CANCELLED: 'warning', + CANCELLED_CONFIRMED: 'danger', EXPIRED: 'danger', DRAFT: 'default', DEACTIVATED: 'default', @@ -1755,7 +1768,8 @@ function ContractsTab({ { 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: 'CANCELLED', label: 'Gekündigt', description: 'Aktive Kündigung eingereicht, Vertrag endet', color: 'text-red-600' }, + { status: 'CANCELLED', label: 'Gekündigt / Bestätigung abwarten', description: 'Kündigungsschreiben raus – Bestätigung des Anbieters steht noch aus', color: 'text-orange-600' }, + { status: 'CANCELLED_CONFIRMED', label: 'Gekündigt / bestätigt', description: 'Kündigung vom Anbieter bestätigt, Vertrag endet', color: 'text-red-600' }, { status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' }, ]; @@ -1818,7 +1832,7 @@ function ContractsTab({ {typeLabels[contract.type] || contract.type} - {contract.status} + {statusLabels[contract.status] || contract.status} {depth === 0 && !isPredecessor && (