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 <noreply@anthropic.com>
This commit is contained in:
+15
@@ -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);
|
||||||
@@ -703,7 +703,10 @@ enum ContractStatus {
|
|||||||
DRAFT
|
DRAFT
|
||||||
PENDING
|
PENDING
|
||||||
ACTIVE
|
ACTIVE
|
||||||
|
// Kündigungsschreiben raus, Bestätigung des Anbieters steht noch aus.
|
||||||
CANCELLED
|
CANCELLED
|
||||||
|
// Kündigung vom Anbieter bestätigt (Bestätigungsdatum/-dokument liegt vor).
|
||||||
|
CANCELLED_CONFIRMED
|
||||||
EXPIRED
|
EXPIRED
|
||||||
DEACTIVATED
|
DEACTIVATED
|
||||||
// "Läuft weiter auf unbestimmte Zeit" – Vertrag ist unbefristet aktiv.
|
// "Läuft weiter auf unbestimmte Zeit" – Vertrag ist unbefristet aktiv.
|
||||||
|
|||||||
@@ -259,8 +259,8 @@ export async function updateContract(req: AuthRequest, res: Response): Promise<v
|
|||||||
const contract = await contractService.updateContract(contractId, sanitizedBody as any);
|
const contract = await contractService.updateContract(contractId, sanitizedBody as any);
|
||||||
|
|
||||||
// Wenn über das Formular ein (neues/geändertes) Kündigungsbestätigungs-
|
// Wenn über das Formular ein (neues/geändertes) Kündigungsbestätigungs-
|
||||||
// datum gesetzt wurde: Vertrag ACTIVE → CANCELLED + Vertragsende =
|
// datum gesetzt wurde: Vertrag → CANCELLED_CONFIRMED ("Gekündigt / bestätigt")
|
||||||
// Kündigungsdatum. Nur bei tatsächlicher Änderung des Datums auslösen,
|
// + Vertragsende = Kündigungsdatum. Nur bei tatsächlicher Änderung auslösen,
|
||||||
// damit manuelle Status-Korrekturen nicht sofort zurückgesetzt werden.
|
// damit manuelle Status-Korrekturen nicht sofort zurückgesetzt werden.
|
||||||
{
|
{
|
||||||
const asDay = (d: unknown) =>
|
const asDay = (d: unknown) =>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
} from '../utils/accessControl.js';
|
} from '../utils/accessControl.js';
|
||||||
import { validateOptionalIsoDate } from '../utils/sanitize.js';
|
import { validateOptionalIsoDate } from '../utils/sanitize.js';
|
||||||
import { validateUploadedFile } from '../middleware/uploadFileTypeValidator.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';
|
import { deleteFileAbsolute, deleteUploadByRelativePath } from '../utils/fileCleanup.js';
|
||||||
|
|
||||||
// Pentest 56.1 (HIGH, 2026-06-01): Upload-Endpoints prüften nur die
|
// Pentest 56.1 (HIGH, 2026-06-01): Upload-Endpoints prüften nur die
|
||||||
@@ -691,12 +691,17 @@ async function handleContractDocumentUpload(
|
|||||||
data: updateData,
|
data: updateData,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wenn eine Kündigungsbestätigung (nicht "Optionen") hochgeladen wurde:
|
// Kündigungsbestätigung (nicht "Optionen") hochgeladen → Vertrag auf
|
||||||
// Vertrag ACTIVE → CANCELLED und Vertragsende = Kündigungsdatum.
|
// "Gekündigt / bestätigt" (CANCELLED_CONFIRMED) + Vertragsende = Kündigungs-
|
||||||
// "Optionen" ist für Vertrags-Änderungen gedacht, nicht für echte Kündigungen.
|
// datum. "Optionen" ist für Vertrags-Änderungen gedacht, nicht für Kündigungen.
|
||||||
if (fieldName === 'cancellationConfirmationPath') {
|
if (fieldName === 'cancellationConfirmationPath') {
|
||||||
await maybeCancelOnCancellationConfirmation(contractId, req);
|
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({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ export async function getCockpitData(opts?: { customerIds?: number[] }): Promise
|
|||||||
const contracts = await prisma.contract.findMany({
|
const contracts = await prisma.contract.findMany({
|
||||||
where: {
|
where: {
|
||||||
status: {
|
status: {
|
||||||
in: ['ACTIVE', 'PENDING', 'DRAFT', 'CANCELLED', 'DEACTIVATED', 'EXPIRED', 'ONGOING'],
|
in: ['ACTIVE', 'PENDING', 'DRAFT', 'CANCELLED', 'CANCELLED_CONFIRMED', 'DEACTIVATED', 'EXPIRED', 'ONGOING'],
|
||||||
},
|
},
|
||||||
...customerScopeFilter,
|
...customerScopeFilter,
|
||||||
},
|
},
|
||||||
@@ -363,8 +363,10 @@ export async function getCockpitData(opts?: { customerIds?: number[] }): Promise
|
|||||||
// Fristen semantisch keinen Sinn ergeben:
|
// Fristen semantisch keinen Sinn ergeben:
|
||||||
// - ONGOING : "Läuft weiter auf unbestimmte Zeit" → kein Ende
|
// - ONGOING : "Läuft weiter auf unbestimmte Zeit" → kein Ende
|
||||||
// - DEACTIVATED : manuell beendet/archiviert → nichts mehr zu erinnern
|
// - DEACTIVATED : manuell beendet/archiviert → nichts mehr zu erinnern
|
||||||
// - CANCELLED : Kündigung schon eingereicht → Vertragsende ist
|
// - CANCELLED_CONFIRMED : Kündigung vom Anbieter bestätigt →
|
||||||
// die Konsequenz der Kündigung, keine Frist mehr
|
// 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
|
// Die Schlussrechnung-Fehlt-Warnung (Block 13a) bleibt aktiv, weil
|
||||||
// sie WEGEN dieser Status feuert (kein separater Guard). DSGVO-/
|
// sie WEGEN dieser Status feuert (kein separater Guard). DSGVO-/
|
||||||
// Consent-Checks (Block 8) sind pro Kunde, nicht pro Vertrag, und
|
// 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 =
|
const skipDeadlines =
|
||||||
contract.status === 'ONGOING'
|
contract.status === 'ONGOING'
|
||||||
|| contract.status === 'DEACTIVATED'
|
|| contract.status === 'DEACTIVATED'
|
||||||
|| contract.status === 'CANCELLED';
|
|| contract.status === 'CANCELLED_CONFIRMED';
|
||||||
|
|
||||||
// 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!
|
||||||
@@ -628,9 +630,11 @@ export async function getCockpitData(opts?: { customerIds?: number[] }): Promise
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
now.setHours(0, 0, 0, 0);
|
now.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
// 13a. SCHLUSSRECHNUNG FEHLT (nur wenn Vertrag gekündigt/deaktiviert ist)
|
// 13a. SCHLUSSRECHNUNG FEHLT (nur wenn Vertrag bestätigt gekündigt/deaktiviert)
|
||||||
// "Beendet" = CANCELLED oder DEACTIVATED (nicht nur Laufzeit abgelaufen!)
|
// "Beendet" = CANCELLED_CONFIRMED (Kündigung bestätigt) oder DEACTIVATED
|
||||||
const isContractTerminated = contract.status === 'CANCELLED' || contract.status === '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) {
|
if (isContractTerminated) {
|
||||||
const hasFinalInvoice = invoices.some(inv => inv.invoiceType === 'FINAL');
|
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. Eigene Liste, weil solche Verträge (v.a. bereits
|
||||||
// CANCELLED/EXPIRED) sonst mangels „Issue" nicht in der Cockpit-Liste
|
// CANCELLED/EXPIRED) sonst mangels „Issue" nicht in der Cockpit-Liste
|
||||||
// auftauchen.
|
// 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
|
const cancellationConfirmations: CockpitContract[] = contracts
|
||||||
.filter(
|
.filter(
|
||||||
(c) =>
|
(c) =>
|
||||||
|
|||||||
@@ -195,9 +195,13 @@ export async function maybeCancelOnCancellationConfirmation(
|
|||||||
const updateData: Record<string, unknown> = {};
|
const updateData: Record<string, unknown> = {};
|
||||||
const changes: Record<string, { vorher: unknown; nachher: unknown }> = {};
|
const changes: Record<string, { vorher: unknown; nachher: unknown }> = {};
|
||||||
|
|
||||||
if (c.status === 'ACTIVE') {
|
// Kündigungsbestätigung liegt vor (Datum/Dokument) → "Gekündigt / bestätigt".
|
||||||
updateData.status = 'CANCELLED';
|
// Von jedem noch "lebenden" Status heben – auch aus "Gekündigt / Bestätigung
|
||||||
changes.status = { vorher: 'ACTIVE', nachher: 'CANCELLED' };
|
// 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.
|
// 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<void> {
|
||||||
|
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(
|
export async function maybeActivateOnDeliveryConfirmation(
|
||||||
contractId: number,
|
contractId: number,
|
||||||
documentType: string,
|
documentType: string,
|
||||||
|
|||||||
@@ -97,6 +97,28 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ 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)
|
- [x] **🔒 Mass-Assignment-Schutz: Bankkarte/Adresse/Ausweis (Pentest R155)** (2026-08-13)
|
||||||
- Controller reichten rohen `req.body` an Prisma durch → `customerId` (Owner)
|
- Controller reichten rohen `req.body` an Prisma durch → `customerId` (Owner)
|
||||||
und `id` (PK) waren per Update mutierbar (staff-only, kein Cross-Tenant, aber
|
und `id` (PK) waren per Update mutierbar (staff-only, kein Cross-Tenant, aber
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ const statusLabels: Record<ContractStatus, string> = {
|
|||||||
DRAFT: 'Entwurf',
|
DRAFT: 'Entwurf',
|
||||||
PENDING: 'Ausstehend',
|
PENDING: 'Ausstehend',
|
||||||
ACTIVE: 'Aktiv',
|
ACTIVE: 'Aktiv',
|
||||||
CANCELLED: 'Gekündigt',
|
CANCELLED: 'Gekündigt / Bestätigung abwarten',
|
||||||
|
CANCELLED_CONFIRMED: 'Gekündigt / bestätigt',
|
||||||
EXPIRED: 'Abgelaufen',
|
EXPIRED: 'Abgelaufen',
|
||||||
DEACTIVATED: 'Deaktiviert',
|
DEACTIVATED: 'Deaktiviert',
|
||||||
ONGOING: 'Läuft weiter (unbestimmt)',
|
ONGOING: 'Läuft weiter (unbestimmt)',
|
||||||
@@ -30,7 +31,8 @@ const statusLabels: Record<ContractStatus, string> = {
|
|||||||
const statusVariants: Record<ContractStatus, 'success' | 'warning' | 'danger' | 'default'> = {
|
const statusVariants: Record<ContractStatus, 'success' | 'warning' | 'danger' | 'default'> = {
|
||||||
ACTIVE: 'success',
|
ACTIVE: 'success',
|
||||||
PENDING: 'warning',
|
PENDING: 'warning',
|
||||||
CANCELLED: 'danger',
|
CANCELLED: 'warning',
|
||||||
|
CANCELLED_CONFIRMED: 'danger',
|
||||||
EXPIRED: 'danger',
|
EXPIRED: 'danger',
|
||||||
DRAFT: 'default',
|
DRAFT: 'default',
|
||||||
DEACTIVATED: 'default',
|
DEACTIVATED: 'default',
|
||||||
|
|||||||
@@ -138,7 +138,8 @@ export default function AssignToContractModal({
|
|||||||
px-2 py-0.5 text-xs rounded-full
|
px-2 py-0.5 text-xs rounded-full
|
||||||
${contract.status === 'ACTIVE' ? 'bg-green-100 text-green-800' :
|
${contract.status === 'ACTIVE' ? 'bg-green-100 text-green-800' :
|
||||||
contract.status === 'PENDING' ? 'bg-yellow-100 text-yellow-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'}
|
'bg-gray-100 text-gray-800'}
|
||||||
`}>
|
`}>
|
||||||
{contract.status}
|
{contract.status}
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ const statusLabels: Record<ContractStatus, string> = {
|
|||||||
DRAFT: 'Entwurf',
|
DRAFT: 'Entwurf',
|
||||||
PENDING: 'Ausstehend',
|
PENDING: 'Ausstehend',
|
||||||
ACTIVE: 'Aktiv',
|
ACTIVE: 'Aktiv',
|
||||||
CANCELLED: 'Gekündigt',
|
CANCELLED: 'Gekündigt / Bestätigung abwarten',
|
||||||
|
CANCELLED_CONFIRMED: 'Gekündigt / bestätigt',
|
||||||
EXPIRED: 'Abgelaufen',
|
EXPIRED: 'Abgelaufen',
|
||||||
DEACTIVATED: 'Deaktiviert',
|
DEACTIVATED: 'Deaktiviert',
|
||||||
ONGOING: 'Läuft weiter (unbestimmt)',
|
ONGOING: 'Läuft weiter (unbestimmt)',
|
||||||
@@ -52,7 +53,8 @@ const statusLabels: Record<ContractStatus, string> = {
|
|||||||
const statusVariants: Record<ContractStatus, 'success' | 'warning' | 'danger' | 'default'> = {
|
const statusVariants: Record<ContractStatus, 'success' | 'warning' | 'danger' | 'default'> = {
|
||||||
ACTIVE: 'success',
|
ACTIVE: 'success',
|
||||||
PENDING: 'warning',
|
PENDING: 'warning',
|
||||||
CANCELLED: 'danger',
|
CANCELLED: 'warning',
|
||||||
|
CANCELLED_CONFIRMED: 'danger',
|
||||||
EXPIRED: 'danger',
|
EXPIRED: 'danger',
|
||||||
DRAFT: 'default',
|
DRAFT: 'default',
|
||||||
DEACTIVATED: 'default',
|
DEACTIVATED: 'default',
|
||||||
@@ -66,7 +68,8 @@ const statusDescriptions = [
|
|||||||
{ 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: '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 / 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' },
|
{ status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ const statusOptions = [
|
|||||||
{ value: 'PENDING', label: 'Ausstehend' },
|
{ value: 'PENDING', label: 'Ausstehend' },
|
||||||
{ value: 'ACTIVE', label: 'Aktiv' },
|
{ value: 'ACTIVE', label: 'Aktiv' },
|
||||||
{ value: 'ONGOING', label: 'Läuft weiter (unbestimmt)' },
|
{ 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: 'EXPIRED', label: 'Abgelaufen' },
|
||||||
{ value: 'DEACTIVATED', label: 'Deaktiviert' },
|
{ value: 'DEACTIVATED', label: 'Deaktiviert' },
|
||||||
];
|
];
|
||||||
@@ -60,7 +61,8 @@ const statusDescriptions = [
|
|||||||
{ 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: '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 / 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' },
|
{ status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ const statusLabels: Record<ContractStatus, string> = {
|
|||||||
DRAFT: 'Entwurf',
|
DRAFT: 'Entwurf',
|
||||||
PENDING: 'Ausstehend',
|
PENDING: 'Ausstehend',
|
||||||
ACTIVE: 'Aktiv',
|
ACTIVE: 'Aktiv',
|
||||||
CANCELLED: 'Gekündigt',
|
CANCELLED: 'Gekündigt / Bestätigung abwarten',
|
||||||
|
CANCELLED_CONFIRMED: 'Gekündigt / bestätigt',
|
||||||
EXPIRED: 'Abgelaufen',
|
EXPIRED: 'Abgelaufen',
|
||||||
DEACTIVATED: 'Deaktiviert',
|
DEACTIVATED: 'Deaktiviert',
|
||||||
ONGOING: 'Läuft weiter (unbestimmt)',
|
ONGOING: 'Läuft weiter (unbestimmt)',
|
||||||
@@ -40,7 +41,8 @@ const statusLabels: Record<ContractStatus, string> = {
|
|||||||
const statusVariants: Record<ContractStatus, 'success' | 'warning' | 'danger' | 'default'> = {
|
const statusVariants: Record<ContractStatus, 'success' | 'warning' | 'danger' | 'default'> = {
|
||||||
ACTIVE: 'success',
|
ACTIVE: 'success',
|
||||||
PENDING: 'warning',
|
PENDING: 'warning',
|
||||||
CANCELLED: 'danger',
|
CANCELLED: 'warning',
|
||||||
|
CANCELLED_CONFIRMED: 'danger',
|
||||||
EXPIRED: 'danger',
|
EXPIRED: 'danger',
|
||||||
DRAFT: 'default',
|
DRAFT: 'default',
|
||||||
DEACTIVATED: 'default',
|
DEACTIVATED: 'default',
|
||||||
@@ -54,7 +56,8 @@ const statusDescriptions = [
|
|||||||
{ 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: '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 / 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' },
|
{ status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1556,7 +1556,8 @@ function MetersTab({
|
|||||||
)}
|
)}
|
||||||
<Badge variant={
|
<Badge variant={
|
||||||
ct.status === 'ACTIVE' ? 'success'
|
ct.status === 'ACTIVE' ? 'success'
|
||||||
: ct.status === 'CANCELLED' ? 'danger'
|
: ct.status === 'CANCELLED' ? 'warning'
|
||||||
|
: ct.status === 'CANCELLED_CONFIRMED' ? 'danger'
|
||||||
: ct.status === 'EXPIRED' ? 'warning'
|
: ct.status === 'EXPIRED' ? 'warning'
|
||||||
: 'default'
|
: 'default'
|
||||||
}>{ct.status}</Badge>
|
}>{ct.status}</Badge>
|
||||||
@@ -1739,10 +1740,22 @@ function ContractsTab({
|
|||||||
CAR_INSURANCE: 'KFZ-Versicherung',
|
CAR_INSURANCE: 'KFZ-Versicherung',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const statusLabels: Record<string, string> = {
|
||||||
|
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<string, 'success' | 'warning' | 'danger' | 'default'> = {
|
const statusVariants: Record<string, 'success' | 'warning' | 'danger' | 'default'> = {
|
||||||
ACTIVE: 'success',
|
ACTIVE: 'success',
|
||||||
PENDING: 'warning',
|
PENDING: 'warning',
|
||||||
CANCELLED: 'danger',
|
CANCELLED: 'warning',
|
||||||
|
CANCELLED_CONFIRMED: 'danger',
|
||||||
EXPIRED: 'danger',
|
EXPIRED: 'danger',
|
||||||
DRAFT: 'default',
|
DRAFT: 'default',
|
||||||
DEACTIVATED: 'default',
|
DEACTIVATED: 'default',
|
||||||
@@ -1755,7 +1768,8 @@ function ContractsTab({
|
|||||||
{ 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: '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 / 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' },
|
{ status: 'DEACTIVATED', label: 'Deaktiviert', description: 'Manuell beendet/archiviert', color: 'text-gray-500' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -1818,7 +1832,7 @@ function ContractsTab({
|
|||||||
<CopyButton value={contract.contractNumber} />
|
<CopyButton value={contract.contractNumber} />
|
||||||
</Link>
|
</Link>
|
||||||
<Badge>{typeLabels[contract.type] || contract.type}</Badge>
|
<Badge>{typeLabels[contract.type] || contract.type}</Badge>
|
||||||
<Badge variant={statusVariants[contract.status] || 'default'}>{contract.status}</Badge>
|
<Badge variant={statusVariants[contract.status] || 'default'}>{statusLabels[contract.status] || contract.status}</Badge>
|
||||||
{depth === 0 && !isPredecessor && (
|
{depth === 0 && !isPredecessor && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); setShowStatusInfo(true); }}
|
onClick={(e) => { e.stopPropagation(); setShowStatusInfo(true); }}
|
||||||
|
|||||||
@@ -558,7 +558,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' | 'ONGOING';
|
export type ContractStatus = 'DRAFT' | 'PENDING' | 'ACTIVE' | 'CANCELLED' | 'CANCELLED_CONFIRMED' | 'EXPIRED' | 'DEACTIVATED' | 'ONGOING';
|
||||||
|
|
||||||
export interface Contract {
|
export interface Contract {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user