gdpr audit implemented, email log, vollmachten, pdf delete cancel data privacy and vollmachten, removed message no id card in engergy car, and other contracts that are not telecom contracts, added insert counter for engery
This commit is contained in:
@@ -53,11 +53,54 @@ export interface CockpitSummary {
|
||||
openTasks: number;
|
||||
pendingContracts: number;
|
||||
reviewDue: number; // Erneute Prüfung fällig (Snooze abgelaufen)
|
||||
missingConsents: number; // Fehlende oder widerrufene Einwilligungen
|
||||
};
|
||||
}
|
||||
|
||||
export interface DocumentAlert {
|
||||
id: number;
|
||||
type: string; // ID_CARD, PASSPORT, DRIVERS_LICENSE, OTHER
|
||||
documentNumber: string;
|
||||
expiryDate: string;
|
||||
daysUntilExpiry: number;
|
||||
urgency: UrgencyLevel;
|
||||
customer: {
|
||||
id: number;
|
||||
customerNumber: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReportedMeterReading {
|
||||
id: number;
|
||||
readingDate: string;
|
||||
value: number;
|
||||
unit: string;
|
||||
notes?: string;
|
||||
reportedBy?: string;
|
||||
createdAt: string;
|
||||
meter: {
|
||||
id: number;
|
||||
meterNumber: string;
|
||||
type: string;
|
||||
};
|
||||
customer: {
|
||||
id: number;
|
||||
customerNumber: string;
|
||||
name: string;
|
||||
};
|
||||
// Anbieter-Info für Quick-Login
|
||||
providerPortal?: {
|
||||
providerName: string;
|
||||
portalUrl: string;
|
||||
portalUsername?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CockpitResult {
|
||||
contracts: CockpitContract[];
|
||||
documentAlerts: DocumentAlert[];
|
||||
reportedReadings: ReportedMeterReading[];
|
||||
summary: CockpitSummary;
|
||||
thresholds: {
|
||||
criticalDays: number;
|
||||
@@ -143,6 +186,8 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
||||
const criticalDays = parseInt(settings.deadlineCriticalDays) || 14;
|
||||
const warningDays = parseInt(settings.deadlineWarningDays) || 42;
|
||||
const okDays = parseInt(settings.deadlineOkDays) || 90;
|
||||
const docExpiryCriticalDays = parseInt(settings.documentExpiryCriticalDays) || 30;
|
||||
const docExpiryWarningDays = parseInt(settings.documentExpiryWarningDays) || 90;
|
||||
|
||||
// Lade alle relevanten Verträge (inkl. CANCELLED/DEACTIVATED für Schlussrechnung-Check)
|
||||
const contracts = await prisma.contract.findMany({
|
||||
@@ -231,9 +276,41 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
||||
openTasks: 0,
|
||||
pendingContracts: 0,
|
||||
reviewDue: 0,
|
||||
missingConsents: 0,
|
||||
},
|
||||
};
|
||||
|
||||
// Consent-Daten batch-laden für alle Kunden
|
||||
const allConsents = await prisma.customerConsent.findMany({
|
||||
where: { status: 'GRANTED' },
|
||||
select: { customerId: true, consentType: true },
|
||||
});
|
||||
|
||||
// Map: customerId → Set<consentType>
|
||||
const grantedConsentsMap = new Map<number, Set<string>>();
|
||||
for (const c of allConsents) {
|
||||
if (!grantedConsentsMap.has(c.customerId)) {
|
||||
grantedConsentsMap.set(c.customerId, new Set());
|
||||
}
|
||||
grantedConsentsMap.get(c.customerId)!.add(c.consentType);
|
||||
}
|
||||
|
||||
// Widerrufene Consents laden
|
||||
const withdrawnConsents = await prisma.customerConsent.findMany({
|
||||
where: { status: 'WITHDRAWN' },
|
||||
select: { customerId: true, consentType: true },
|
||||
});
|
||||
const withdrawnConsentsMap = new Map<number, Set<string>>();
|
||||
for (const c of withdrawnConsents) {
|
||||
if (!withdrawnConsentsMap.has(c.customerId)) {
|
||||
withdrawnConsentsMap.set(c.customerId, new Set());
|
||||
}
|
||||
withdrawnConsentsMap.get(c.customerId)!.add(c.consentType);
|
||||
}
|
||||
|
||||
// Track welche Kunden bereits eine Consent-Warnung bekommen haben (nur einmal pro Kunde)
|
||||
const customerConsentWarned = new Set<number>();
|
||||
|
||||
for (const contract of contracts) {
|
||||
const issues: CockpitIssue[] = [];
|
||||
|
||||
@@ -407,17 +484,43 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
||||
summary.byCategory.missingData++;
|
||||
}
|
||||
|
||||
// 7b. KEIN AUSWEIS (für DSL, FIBER, CABLE, MOBILE ist dies ein kritisches Problem)
|
||||
if (!contract.identityDocumentId) {
|
||||
// 7b. KEIN AUSWEIS (nur für Telekommunikationsprodukte relevant)
|
||||
const requiresIdentityDocument = ['DSL', 'FIBER', 'CABLE', 'MOBILE'].includes(contract.type);
|
||||
if (requiresIdentityDocument && !contract.identityDocumentId) {
|
||||
issues.push({
|
||||
type: 'missing_identity_document',
|
||||
label: 'Ausweis fehlt',
|
||||
urgency: requiresBankAndId ? 'critical' : 'warning',
|
||||
urgency: 'critical',
|
||||
details: 'Kein Ausweisdokument verknüpft',
|
||||
});
|
||||
summary.byCategory.missingData++;
|
||||
}
|
||||
|
||||
// 7c. AUSWEIS LÄUFT AB (nur aktive Ausweise prüfen)
|
||||
if (contract.identityDocument && contract.identityDocument.isActive && contract.identityDocument.expiryDate) {
|
||||
const expiryDate = new Date(contract.identityDocument.expiryDate);
|
||||
const today = new Date();
|
||||
const daysUntilExpiry = Math.ceil((expiryDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (daysUntilExpiry < 0) {
|
||||
issues.push({
|
||||
type: 'identity_document_expired',
|
||||
label: 'Ausweis abgelaufen',
|
||||
urgency: 'critical',
|
||||
details: `Ausweis seit ${Math.abs(daysUntilExpiry)} Tagen abgelaufen (${expiryDate.toLocaleDateString('de-DE')})`,
|
||||
});
|
||||
summary.byCategory.missingData++;
|
||||
} else if (daysUntilExpiry <= docExpiryWarningDays) {
|
||||
issues.push({
|
||||
type: 'identity_document_expiring',
|
||||
label: 'Ausweis läuft ab',
|
||||
urgency: daysUntilExpiry <= docExpiryCriticalDays ? 'critical' : 'warning',
|
||||
details: `Ausweis läuft in ${daysUntilExpiry} Tagen ab (${expiryDate.toLocaleDateString('de-DE')})`,
|
||||
});
|
||||
summary.byCategory.cancellationDeadlines++;
|
||||
}
|
||||
}
|
||||
|
||||
// 8. ENERGIE-SPEZIFISCH: KEIN ZÄHLER
|
||||
if (['ELECTRICITY', 'GAS'].includes(contract.type) && contract.energyDetails) {
|
||||
if (!contract.energyDetails.meterId) {
|
||||
@@ -546,6 +649,36 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
||||
}
|
||||
}
|
||||
|
||||
// #14 - Consent-Prüfung (nur für aktive Verträge, einmal pro Kunde)
|
||||
if (['ACTIVE', 'PENDING', 'DRAFT'].includes(contract.status) && !customerConsentWarned.has(contract.customer.id)) {
|
||||
const granted = grantedConsentsMap.get(contract.customer.id);
|
||||
const withdrawn = withdrawnConsentsMap.get(contract.customer.id);
|
||||
const requiredTypes = ['DATA_PROCESSING', 'MARKETING_EMAIL', 'MARKETING_PHONE', 'DATA_SHARING_PARTNER'];
|
||||
|
||||
if (withdrawn && withdrawn.size > 0) {
|
||||
// Mindestens eine Einwilligung widerrufen
|
||||
issues.push({
|
||||
type: 'consent_withdrawn',
|
||||
label: 'Einwilligung widerrufen',
|
||||
urgency: 'critical',
|
||||
details: `${withdrawn.size} Einwilligung(en) widerrufen`,
|
||||
});
|
||||
summary.byCategory.missingConsents++;
|
||||
customerConsentWarned.add(contract.customer.id);
|
||||
} else if (!granted || granted.size < requiredTypes.length) {
|
||||
// Nicht alle 4 Einwilligungen erteilt
|
||||
const missing = requiredTypes.length - (granted?.size || 0);
|
||||
issues.push({
|
||||
type: 'missing_consents',
|
||||
label: 'Fehlende Einwilligungen',
|
||||
urgency: 'critical',
|
||||
details: `${missing} von ${requiredTypes.length} Einwilligungen fehlen`,
|
||||
});
|
||||
summary.byCategory.missingConsents++;
|
||||
customerConsentWarned.add(contract.customer.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Nur Verträge mit Issues hinzufügen
|
||||
if (issues.length > 0) {
|
||||
const highestUrgency = getHighestUrgency(issues);
|
||||
@@ -596,8 +729,16 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
||||
return urgencyOrder[a.highestUrgency] - urgencyOrder[b.highestUrgency];
|
||||
});
|
||||
|
||||
// Vertragsunabhängige Ausweis-Warnungen
|
||||
const documentAlerts = await getDocumentExpiryAlerts(docExpiryCriticalDays, docExpiryWarningDays);
|
||||
|
||||
// Gemeldete Zählerstände (REPORTED Status)
|
||||
const reportedReadings = await getReportedMeterReadings();
|
||||
|
||||
return {
|
||||
contracts: cockpitContracts,
|
||||
documentAlerts,
|
||||
reportedReadings,
|
||||
summary,
|
||||
thresholds: {
|
||||
criticalDays,
|
||||
@@ -606,3 +747,111 @@ export async function getCockpitData(): Promise<CockpitResult> {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle aktiven Ausweise die ablaufen oder abgelaufen sind (vertragsunabhängig)
|
||||
*/
|
||||
async function getDocumentExpiryAlerts(criticalDays: number, warningDays: number): Promise<DocumentAlert[]> {
|
||||
const now = new Date();
|
||||
const inWarningDays = new Date(now.getTime() + warningDays * 24 * 60 * 60 * 1000);
|
||||
|
||||
const documents = await prisma.identityDocument.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
expiryDate: { lte: inWarningDays },
|
||||
},
|
||||
include: {
|
||||
customer: {
|
||||
select: { id: true, customerNumber: true, firstName: true, lastName: true },
|
||||
},
|
||||
},
|
||||
orderBy: { expiryDate: 'asc' },
|
||||
});
|
||||
|
||||
return documents.map((doc) => {
|
||||
const expiryDate = new Date(doc.expiryDate!);
|
||||
const daysUntilExpiry = Math.ceil((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
||||
|
||||
let urgency: UrgencyLevel = 'warning';
|
||||
if (daysUntilExpiry < 0) urgency = 'critical';
|
||||
else if (daysUntilExpiry <= criticalDays) urgency = 'critical';
|
||||
|
||||
return {
|
||||
id: doc.id,
|
||||
type: doc.type,
|
||||
documentNumber: doc.documentNumber,
|
||||
expiryDate: expiryDate.toISOString(),
|
||||
daysUntilExpiry,
|
||||
urgency,
|
||||
customer: {
|
||||
id: doc.customer.id,
|
||||
customerNumber: doc.customer.customerNumber,
|
||||
name: `${doc.customer.firstName} ${doc.customer.lastName}`,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Vom Kunden gemeldete Zählerstände die noch nicht übertragen wurden
|
||||
*/
|
||||
async function getReportedMeterReadings(): Promise<ReportedMeterReading[]> {
|
||||
const readings = await prisma.meterReading.findMany({
|
||||
where: { status: 'REPORTED' },
|
||||
include: {
|
||||
meter: {
|
||||
include: {
|
||||
customer: {
|
||||
select: { id: true, customerNumber: true, firstName: true, lastName: true },
|
||||
},
|
||||
// Energie-Verträge für diesen Zähler (um Provider-Portal-Daten zu bekommen)
|
||||
energyDetails: {
|
||||
include: {
|
||||
contract: {
|
||||
select: {
|
||||
id: true,
|
||||
portalUsername: true,
|
||||
provider: {
|
||||
select: { id: true, name: true, portalUrl: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
|
||||
return readings.map((r) => {
|
||||
const contract = r.meter.energyDetails?.[0]?.contract;
|
||||
const provider = contract?.provider;
|
||||
|
||||
return {
|
||||
id: r.id,
|
||||
readingDate: r.readingDate.toISOString(),
|
||||
value: r.value,
|
||||
unit: r.unit,
|
||||
notes: r.notes ?? undefined,
|
||||
reportedBy: r.reportedBy ?? undefined,
|
||||
createdAt: r.createdAt.toISOString(),
|
||||
meter: {
|
||||
id: r.meter.id,
|
||||
meterNumber: r.meter.meterNumber,
|
||||
type: r.meter.type,
|
||||
},
|
||||
customer: {
|
||||
id: r.meter.customer.id,
|
||||
customerNumber: r.meter.customer.customerNumber,
|
||||
name: `${r.meter.customer.firstName} ${r.meter.customer.lastName}`,
|
||||
},
|
||||
providerPortal: provider?.portalUrl ? {
|
||||
providerName: provider.name,
|
||||
portalUrl: provider.portalUrl,
|
||||
portalUsername: contract?.portalUsername ?? undefined,
|
||||
} : undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user