"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.getCockpitData = getCockpitData; const client_1 = require("@prisma/client"); const appSettingService = __importStar(require("./appSetting.service.js")); const prisma = new client_1.PrismaClient(); // Hilfsfunktion: Tage bis zu einem Datum berechnen function daysUntil(date) { 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, criticalDays, warningDays, okDays) { 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) { const levels = ['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, cancellationPeriodCode) { 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; } async function getCockpitData() { // 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 relevanten Verträge (inkl. CANCELLED/DEACTIVATED für Schlussrechnung-Check) const contracts = await prisma.contract.findMany({ where: { status: { in: ['ACTIVE', 'PENDING', 'DRAFT', 'CANCELLED', 'DEACTIVATED', 'EXPIRED'], }, }, 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, invoices: 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 = []; const summary = { totalContracts: 0, criticalCount: 0, warningCount: 0, okCount: 0, byCategory: { cancellationDeadlines: 0, contractEnding: 0, missingCredentials: 0, missingData: 0, missingInvoices: 0, openTasks: 0, pendingContracts: 0, reviewDue: 0, }, }; for (const contract of contracts) { const issues = []; // SNOOZE-LOGIK: Prüfen ob Snooze aktiv ist (für Fristen-Unterdrückung) let snoozeActive = false; if (contract.nextReviewDate) { const reviewDate = new Date(contract.nextReviewDate); const now = new Date(); now.setHours(0, 0, 0, 0); reviewDate.setHours(0, 0, 0, 0); if (reviewDate > now) { // Snooze aktiv → NUR Fristen-Warnungen unterdrücken, andere Prüfungen laufen weiter snoozeActive = true; } else { // Snooze abgelaufen → "Erneute Prüfung fällig" Warnung const daysSince = Math.floor((now.getTime() - reviewDate.getTime()) / (1000 * 60 * 60 * 24)); issues.push({ type: 'review_due', label: 'Erneute Prüfung fällig', urgency: daysSince > 30 ? 'critical' : 'warning', daysRemaining: -daysSince, details: daysSince === 0 ? 'Heute zur Prüfung fällig' : `Zur Prüfung seit ${daysSince} Tagen fällig`, }); summary.byCategory.reviewDue++; } } // 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 UND Snooze nicht aktiv) // Snooze unterdrückt NUR Fristen-bezogene Warnungen! if (!hasActiveFollowUp && !snoozeActive) { 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 (nur wenn Snooze nicht aktiv) // Snooze unterdrückt NUR Fristen-bezogene Warnungen! if (!snoozeActive) { 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 // Benutzername kann entweder manuell (portalUsername) oder via Stressfrei-Wechseln E-Mail (stressfreiEmailId) gesetzt sein const hasUsername = contract.portalUsername || contract.stressfreiEmailId; const hasPassword = contract.portalPasswordEncrypted; if (!hasUsername || !hasPassword) { 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++; } // 4b. KEINE VERTRAGSNUMMER BEIM ANBIETER if (!contract.contractNumberAtProvider) { issues.push({ type: 'missing_contract_number', label: 'Vertragsnummer fehlt', urgency: 'warning', details: 'Vertragsnummer 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 // Für DSL, FIBER, CABLE, MOBILE ist dies ein kritisches Problem const requiresBankAndId = ['DSL', 'FIBER', 'CABLE', 'MOBILE'].includes(contract.type); if (!contract.bankCardId) { issues.push({ type: 'missing_bank', label: 'Bankverbindung fehlt', urgency: requiresBankAndId ? 'critical' : 'warning', details: 'Keine Bankverbindung verknüpft', }); summary.byCategory.missingData++; } // 7b. KEIN AUSWEIS (für DSL, FIBER, CABLE, MOBILE ist dies ein kritisches Problem) if (!contract.identityDocumentId) { issues.push({ type: 'missing_identity_document', label: 'Ausweis fehlt', urgency: requiresBankAndId ? 'critical' : 'warning', details: 'Kein Ausweisdokument 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++; } // 13. ENERGIE-RECHNUNGEN (nur für ELECTRICITY und GAS) if (['ELECTRICITY', 'GAS'].includes(contract.type) && contract.energyDetails) { const invoices = contract.energyDetails.invoices || []; 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'; if (isContractTerminated) { const hasFinalInvoice = invoices.some(inv => inv.invoiceType === 'FINAL'); const hasNotAvailable = invoices.some(inv => inv.invoiceType === 'NOT_AVAILABLE'); if (!hasFinalInvoice && !hasNotAvailable) { issues.push({ type: 'missing_final_invoice', label: 'Schlussrechnung fehlt', urgency: 'warning', details: 'Vertrag gekündigt/deaktiviert, aber keine Schlussrechnung vorhanden', }); summary.byCategory.missingInvoices++; } } // 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) { const startDate = new Date(contract.startDate); startDate.setHours(0, 0, 0, 0); const daysSinceStart = Math.floor((now.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)); if (daysSinceStart > 365) { // Vertrag läuft > 12 Monate if (invoices.length === 0) { // Keine Rechnungen vorhanden issues.push({ type: 'missing_interim_invoice', label: 'Zwischenrechnung fehlt', urgency: 'warning', details: 'Vertrag läuft über 12 Monate ohne Rechnung', }); summary.byCategory.missingInvoices++; } else { // Prüfen ob letzte Rechnung > 12 Monate alt const latestInvoice = invoices .filter(inv => inv.invoiceType !== 'NOT_AVAILABLE') .sort((a, b) => new Date(b.invoiceDate).getTime() - new Date(a.invoiceDate).getTime())[0]; if (latestInvoice) { const invoiceDate = new Date(latestInvoice.invoiceDate); const daysSinceInvoice = Math.floor((now.getTime() - invoiceDate.getTime()) / (1000 * 60 * 60 * 24)); if (daysSinceInvoice > 365) { issues.push({ type: 'overdue_interim_invoice', label: 'Zwischenrechnung überfällig', urgency: 'warning', details: `Letzte Rechnung vor ${Math.floor(daysSinceInvoice / 30)} Monaten`, }); summary.byCategory.missingInvoices++; } } } } } } // 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 = { critical: 0, warning: 1, ok: 2, none: 3, }; return urgencyOrder[a.highestUrgency] - urgencyOrder[b.highestUrgency]; }); return { contracts: cockpitContracts, summary, thresholds: { criticalDays, warningDays, okDays, }, }; } //# sourceMappingURL=contractCockpit.service.js.map