import { PrismaClient, ContractStatus } from '@prisma/client'; import * as appSettingService from './appSetting.service.js'; const prisma = new PrismaClient(); // Typen für das Cockpit export type UrgencyLevel = 'critical' | 'warning' | 'ok' | 'none'; export interface CockpitIssue { type: string; label: string; urgency: UrgencyLevel; daysRemaining?: number; details?: string; } export interface CockpitContract { id: number; contractNumber: string; type: string; status: ContractStatus; customer: { id: number; customerNumber: string; name: string; }; provider?: { id: number; name: string; }; tariff?: { id: number; name: string; }; providerName?: string; tariffName?: string; issues: CockpitIssue[]; highestUrgency: UrgencyLevel; } export interface CockpitSummary { totalContracts: number; criticalCount: number; warningCount: number; okCount: number; byCategory: { cancellationDeadlines: number; contractEnding: number; missingCredentials: number; missingData: number; openTasks: number; pendingContracts: number; }; } export interface CockpitResult { contracts: CockpitContract[]; summary: CockpitSummary; thresholds: { criticalDays: number; warningDays: number; okDays: number; }; } // Hilfsfunktion: Tage bis zu einem Datum berechnen function daysUntil(date: Date | null | undefined): number | null { if (!date) return null; const now = new Date(); now.setHours(0, 0, 0, 0); const target = new Date(date); target.setHours(0, 0, 0, 0); const diff = target.getTime() - now.getTime(); return Math.ceil(diff / (1000 * 60 * 60 * 24)); } // Hilfsfunktion: Urgency basierend auf Tagen bestimmen function getUrgencyByDays( daysRemaining: number | null, criticalDays: number, warningDays: number, okDays: number ): UrgencyLevel { if (daysRemaining === null) return 'none'; if (daysRemaining < 0) return 'critical'; // Bereits überfällig if (daysRemaining <= criticalDays) return 'critical'; if (daysRemaining <= warningDays) return 'warning'; if (daysRemaining <= okDays) return 'ok'; return 'none'; } // Hilfsfunktion: Höchste Dringlichkeit ermitteln function getHighestUrgency(issues: CockpitIssue[]): UrgencyLevel { const levels: UrgencyLevel[] = ['critical', 'warning', 'ok', 'none']; for (const level of levels) { if (issues.some(i => i.urgency === level)) { return level; } } return 'none'; } // Kündigungsfrist berechnen function calculateCancellationDeadline( endDate: Date | null | undefined, cancellationPeriodCode: string | null | undefined ): Date | null { if (!endDate || !cancellationPeriodCode) return null; const end = new Date(endDate); // Parse Kündigungsperiode (z.B. "1M" = 1 Monat, "6W" = 6 Wochen, "14D" = 14 Tage) const match = cancellationPeriodCode.match(/^(\d+)([DMWY])$/i); if (!match) return null; const amount = parseInt(match[1]); const unit = match[2].toUpperCase(); switch (unit) { case 'D': end.setDate(end.getDate() - amount); break; case 'W': end.setDate(end.getDate() - (amount * 7)); break; case 'M': end.setMonth(end.getMonth() - amount); break; case 'Y': end.setFullYear(end.getFullYear() - amount); break; } return end; } export async function getCockpitData(): Promise { // Lade Einstellungen const settings = await appSettingService.getAllSettings(); const criticalDays = parseInt(settings.deadlineCriticalDays) || 14; const warningDays = parseInt(settings.deadlineWarningDays) || 42; const okDays = parseInt(settings.deadlineOkDays) || 90; // Lade alle aktiven/pending Verträge mit allen relevanten Daten const contracts = await prisma.contract.findMany({ where: { status: { in: ['ACTIVE', 'PENDING', 'DRAFT'], }, }, include: { customer: { select: { id: true, customerNumber: true, firstName: true, lastName: true, companyName: true, }, }, provider: { select: { id: true, name: true, }, }, tariff: { select: { id: true, name: true, }, }, cancellationPeriod: { select: { code: true, }, }, address: true, bankCard: true, identityDocument: true, energyDetails: { include: { meter: true, }, }, internetDetails: { include: { phoneNumbers: true, }, }, mobileDetails: { include: { simCards: true, }, }, tasks: { where: { status: 'OPEN', }, }, // Folgevertrag laden um zu prüfen ob dieser aktiv ist followUpContract: { select: { id: true, status: true, }, }, }, orderBy: [ { endDate: 'asc' }, { createdAt: 'desc' }, ], }); const cockpitContracts: CockpitContract[] = []; const summary: CockpitSummary = { totalContracts: 0, criticalCount: 0, warningCount: 0, okCount: 0, byCategory: { cancellationDeadlines: 0, contractEnding: 0, missingCredentials: 0, missingData: 0, openTasks: 0, pendingContracts: 0, }, }; for (const contract of contracts) { const issues: CockpitIssue[] = []; // Prüfen ob aktiver Folgevertrag existiert - dann keine Kündigungswarnungen nötig const hasActiveFollowUp = contract.followUpContract?.status === 'ACTIVE'; // 1. KÜNDIGUNGSFRIST (nur wenn kein aktiver Folgevertrag) if (!hasActiveFollowUp) { const cancellationDeadline = calculateCancellationDeadline( contract.endDate, contract.cancellationPeriod?.code ); const daysToCancellation = daysUntil(cancellationDeadline); if (daysToCancellation !== null && daysToCancellation <= okDays) { const urgency = getUrgencyByDays(daysToCancellation, criticalDays, warningDays, okDays); if (urgency !== 'none') { issues.push({ type: 'cancellation_deadline', label: 'Kündigungsfrist', urgency, daysRemaining: daysToCancellation, details: daysToCancellation < 0 ? `Frist seit ${Math.abs(daysToCancellation)} Tagen überschritten!` : `Noch ${daysToCancellation} Tage bis zur Kündigungsfrist`, }); summary.byCategory.cancellationDeadlines++; } // 1a. KÜNDIGUNG NICHT GESENDET (wenn Frist naht) if (!contract.cancellationLetterPath) { issues.push({ type: 'missing_cancellation_letter', label: 'Kündigung nicht gesendet', urgency, details: 'Kündigungsschreiben wurde noch nicht hochgeladen', }); summary.byCategory.missingData++; } // 1b. KÜNDIGUNGSBESTÄTIGUNG FEHLT (wenn Kündigung gesendet aber keine Bestätigung) if (contract.cancellationLetterPath && !contract.cancellationConfirmationPath && !contract.cancellationConfirmationDate) { issues.push({ type: 'missing_cancellation_confirmation', label: 'Kündigungsbestätigung fehlt', urgency: urgency === 'critical' ? 'critical' : 'warning', details: 'Kündigungsbestätigung vom Anbieter wurde noch nicht erhalten', }); summary.byCategory.missingData++; } } } // 2. VERTRAGSENDE const daysToEnd = daysUntil(contract.endDate); if (daysToEnd !== null && daysToEnd <= okDays) { const urgency = getUrgencyByDays(daysToEnd, criticalDays, warningDays, okDays); if (urgency !== 'none') { issues.push({ type: 'contract_ending', label: 'Vertragsende', urgency, daysRemaining: daysToEnd, details: daysToEnd < 0 ? `Vertrag seit ${Math.abs(daysToEnd)} Tagen abgelaufen!` : `Noch ${daysToEnd} Tage bis Vertragsende`, }); summary.byCategory.contractEnding++; } } // 3. FEHLENDE PORTAL-ZUGANGSDATEN if (!contract.portalUsername || !contract.portalPasswordEncrypted) { issues.push({ type: 'missing_portal_credentials', label: 'Portal-Zugangsdaten fehlen', urgency: 'warning', details: 'Benutzername oder Passwort für das Anbieter-Portal fehlt', }); summary.byCategory.missingCredentials++; } // 4. KEINE KUNDENNUMMER BEIM ANBIETER if (!contract.customerNumberAtProvider) { issues.push({ type: 'missing_customer_number', label: 'Kundennummer fehlt', urgency: 'warning', details: 'Kundennummer beim Anbieter fehlt', }); summary.byCategory.missingData++; } // 5. KEIN ANBIETER/TARIF if (!contract.providerId && !contract.providerName) { issues.push({ type: 'missing_provider', label: 'Anbieter fehlt', urgency: 'warning', details: 'Kein Anbieter ausgewählt', }); summary.byCategory.missingData++; } // 6. KEINE ADRESSE if (!contract.addressId) { issues.push({ type: 'missing_address', label: 'Adresse fehlt', urgency: 'warning', details: 'Keine Lieferadresse verknüpft', }); summary.byCategory.missingData++; } // 7. KEINE BANKVERBINDUNG if (!contract.bankCardId) { issues.push({ type: 'missing_bank', label: 'Bankverbindung fehlt', urgency: 'warning', details: 'Keine Bankverbindung verknüpft', }); summary.byCategory.missingData++; } // 8. ENERGIE-SPEZIFISCH: KEIN ZÄHLER if (['ELECTRICITY', 'GAS'].includes(contract.type) && contract.energyDetails) { if (!contract.energyDetails.meterId) { issues.push({ type: 'missing_meter', label: 'Zähler fehlt', urgency: 'warning', details: 'Kein Zähler verknüpft', }); summary.byCategory.missingData++; } } // 9. MOBIL-SPEZIFISCH: SIM-KARTEN if (contract.type === 'MOBILE' && contract.mobileDetails) { if (!contract.mobileDetails.simCards || contract.mobileDetails.simCards.length === 0) { issues.push({ type: 'missing_sim', label: 'SIM-Karte fehlt', urgency: 'warning', details: 'Keine SIM-Karte eingetragen', }); summary.byCategory.missingData++; } } // 10. OFFENE AUFGABEN if (contract.tasks && contract.tasks.length > 0) { issues.push({ type: 'open_tasks', label: 'Offene Aufgaben', urgency: 'ok', details: `${contract.tasks.length} offene Aufgabe(n)`, }); summary.byCategory.openTasks++; } // 11. PENDING STATUS if (contract.status === 'PENDING') { issues.push({ type: 'pending_status', label: 'Warte auf Aktivierung', urgency: 'ok', details: 'Vertrag noch nicht aktiv', }); summary.byCategory.pendingContracts++; } // 12. DRAFT STATUS if (contract.status === 'DRAFT') { issues.push({ type: 'draft_status', label: 'Entwurf', urgency: 'warning', details: 'Vertrag ist noch ein Entwurf', }); summary.byCategory.pendingContracts++; } // Nur Verträge mit Issues hinzufügen if (issues.length > 0) { const highestUrgency = getHighestUrgency(issues); const customerName = contract.customer.companyName || `${contract.customer.firstName} ${contract.customer.lastName}`; cockpitContracts.push({ id: contract.id, contractNumber: contract.contractNumber, type: contract.type, status: contract.status, customer: { id: contract.customer.id, customerNumber: contract.customer.customerNumber, name: customerName, }, provider: contract.provider ? { id: contract.provider.id, name: contract.provider.name, } : undefined, tariff: contract.tariff ? { id: contract.tariff.id, name: contract.tariff.name, } : undefined, providerName: contract.providerName || undefined, tariffName: contract.tariffName || undefined, issues, highestUrgency, }); // Summary zählen summary.totalContracts++; if (highestUrgency === 'critical') summary.criticalCount++; else if (highestUrgency === 'warning') summary.warningCount++; else if (highestUrgency === 'ok') summary.okCount++; } } // Sortiere nach Dringlichkeit cockpitContracts.sort((a, b) => { const urgencyOrder: Record = { critical: 0, warning: 1, ok: 2, none: 3, }; return urgencyOrder[a.highestUrgency] - urgencyOrder[b.highestUrgency]; }); return { contracts: cockpitContracts, summary, thresholds: { criticalDays, warningDays, okDays, }, }; }