Files
opencrm/backend/src/services/customer.service.ts
T
duffyduck b4b0dbb004 Kundenakte → Zähler: Aufklapp-Liste der zugeordneten Verträge
Pro Zähler wird jetzt ein "Verträge (N)" Aufklapp-Bereich angezeigt,
der alle Verträge auflistet, die diesen Zähler nutzen – sowohl als
aktueller Hauptzähler (energyDetails.meterId) als auch über die
Folgezähler-Kette (ContractMeter). Dedupliziert auf contractId.

Jeder Eintrag ist Link auf den Vertrag im neuen Tab, mit
Vertragsnummer, Anbieter und Status-Badge. Folgezähler-Ketten-
Einträge werden mit "(über Folgezähler-Kette)" markiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 14:48:22 +02:00

1012 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { CustomerType, ContractStatus } from '@prisma/client';
import prisma from '../lib/prisma.js';
import { generateCustomerNumber, paginate, buildPaginationResponse } from '../utils/helpers.js';
import fs from 'fs';
import path from 'path';
// Helper zum Löschen von Dateien
function deleteFileIfExists(filePath: string | null) {
if (!filePath) return;
const absolutePath = path.join(process.cwd(), filePath);
if (fs.existsSync(absolutePath)) {
try {
fs.unlinkSync(absolutePath);
} catch (error) {
console.error('Fehler beim Löschen der Datei:', absolutePath, error);
}
}
}
export interface CustomerFilters {
search?: string;
type?: CustomerType;
page?: number;
limit?: number;
// Wenn gesetzt: nur Customer mit id in dieser Liste. Für Portal-User, damit
// weder Liste noch pagination.total die globale Kunden-Zahl preisgibt.
allowedIds?: number[];
}
export async function getAllCustomers(filters: CustomerFilters) {
const { search, type, page = 1, limit = 20, allowedIds } = filters;
const { skip, take } = paginate(page, limit);
const where: Record<string, unknown> = {};
if (type) {
where.type = type;
}
if (allowedIds) {
where.id = { in: allowedIds };
}
if (search) {
where.OR = [
{ firstName: { contains: search } },
{ lastName: { contains: search } },
{ companyName: { contains: search } },
{ email: { contains: search } },
{ customerNumber: { contains: search } },
];
}
const [customers, total] = await Promise.all([
prisma.customer.findMany({
where,
skip,
take,
orderBy: { createdAt: 'desc' },
include: {
addresses: { where: { isDefault: true }, take: 1 },
_count: {
select: { contracts: true },
},
},
}),
prisma.customer.count({ where }),
]);
return {
customers,
pagination: buildPaginationResponse(page, limit, total),
};
}
export async function getCustomerById(id: number) {
return prisma.customer.findUnique({
where: { id },
include: {
addresses: true,
bankCards: { orderBy: { isActive: 'desc' } },
identityDocuments: { orderBy: { isActive: 'desc' } },
meters: {
orderBy: { isActive: 'desc' },
include: {
address: true,
readings: {
orderBy: { readingDate: 'desc' },
},
// Verträge, die diesen Zähler aktuell als Hauptzähler nutzen
// (energyDetails.meterId === meter.id)
energyDetails: {
include: {
contract: { select: { id: true, contractNumber: true, status: true, type: true, providerName: true } },
},
},
// Verträge, in denen der Zähler in der ContractMeter-Kette steht
// (Vorgänger oder Nachfolger über Zählerwechsel)
contractMeters: {
include: {
energyContractDetails: {
include: {
contract: { select: { id: true, contractNumber: true, status: true, type: true, providerName: true } },
},
},
},
},
},
},
stressfreiEmails: { orderBy: { isActive: 'desc' } },
contracts: {
where: {
// Deaktivierte Verträge ausblenden
status: { not: ContractStatus.DEACTIVATED },
},
orderBy: [{ startDate: 'desc' }, { createdAt: 'desc' }],
include: {
address: true,
salesPlatform: true,
},
},
},
});
}
export async function getCustomersByIds(ids: number[]) {
return prisma.customer.findMany({
where: { id: { in: ids } },
select: {
id: true,
portalEmail: true,
},
});
}
export async function createCustomer(data: {
type?: CustomerType;
salutation?: string;
firstName: string;
lastName: string;
companyName?: string;
birthDate?: Date;
birthPlace?: string;
email?: string;
phone?: string;
mobile?: string;
taxNumber?: string;
businessRegistration?: string;
commercialRegister?: string;
notes?: string;
}) {
return prisma.customer.create({
data: {
...data,
customerNumber: generateCustomerNumber(),
},
});
}
export async function updateCustomer(
id: number,
data: {
type?: CustomerType;
salutation?: string;
useInformalAddress?: boolean;
firstName?: string;
lastName?: string;
companyName?: string;
birthDate?: Date;
birthPlace?: string;
email?: string;
phone?: string;
mobile?: string;
taxNumber?: string;
businessRegistration?: string;
commercialRegister?: string;
notes?: string;
autoBirthdayGreeting?: boolean;
autoBirthdayChannel?: string | null;
}
) {
return prisma.customer.update({
where: { id },
data,
});
}
export async function deleteCustomer(id: number) {
// Vor dem Löschen: Alle Dokumente (Dateien) des Kunden löschen
const customer = await prisma.customer.findUnique({
where: { id },
select: { businessRegistrationPath: true, commercialRegisterPath: true, privacyPolicyPath: true },
});
const bankCards = await prisma.bankCard.findMany({
where: { customerId: id },
select: { documentPath: true },
});
const identityDocs = await prisma.identityDocument.findMany({
where: { customerId: id },
select: { documentPath: true },
});
// Kundendokumente löschen
if (customer) {
deleteFileIfExists(customer.businessRegistrationPath);
deleteFileIfExists(customer.commercialRegisterPath);
deleteFileIfExists(customer.privacyPolicyPath);
}
// Bankkarten- und Ausweisdokumente löschen
for (const card of bankCards) {
deleteFileIfExists(card.documentPath);
}
for (const doc of identityDocs) {
deleteFileIfExists(doc.documentPath);
}
// Jetzt DB-Eintrag löschen (Cascade löscht die verknüpften Einträge)
return prisma.customer.delete({
where: { id },
});
}
// Address operations
export async function getCustomerAddresses(customerId: number) {
return prisma.address.findMany({
where: { customerId },
orderBy: [{ isDefault: 'desc' }, { createdAt: 'desc' }],
});
}
export async function createAddress(
customerId: number,
data: {
type: 'DELIVERY_RESIDENCE' | 'BILLING';
street: string;
houseNumber: string;
postalCode: string;
city: string;
country?: string;
isDefault?: boolean;
}
) {
// If this is set as default, unset other defaults of same type
if (data.isDefault) {
await prisma.address.updateMany({
where: { customerId, type: data.type },
data: { isDefault: false },
});
}
return prisma.address.create({
data: {
customerId,
...data,
},
});
}
export async function updateAddress(
id: number,
data: {
type?: 'DELIVERY_RESIDENCE' | 'BILLING';
street?: string;
houseNumber?: string;
postalCode?: string;
city?: string;
country?: string;
isDefault?: boolean;
}
) {
const address = await prisma.address.findUnique({ where: { id } });
if (!address) throw new Error('Adresse nicht gefunden');
if (data.isDefault) {
await prisma.address.updateMany({
where: {
customerId: address.customerId,
type: data.type || address.type,
id: { not: id },
},
data: { isDefault: false },
});
}
return prisma.address.update({
where: { id },
data,
});
}
export async function deleteAddress(id: number) {
return prisma.address.delete({ where: { id } });
}
// Bank card operations
export async function getCustomerBankCards(
customerId: number,
showInactive: boolean = false
) {
const where: Record<string, unknown> = { customerId };
if (!showInactive) {
where.isActive = true;
}
return prisma.bankCard.findMany({
where,
orderBy: [{ isActive: 'desc' }, { createdAt: 'desc' }],
});
}
export async function createBankCard(
customerId: number,
data: {
accountHolder: string;
iban: string;
bic?: string;
bankName?: string;
expiryDate?: Date;
}
) {
return prisma.bankCard.create({
data: {
customerId,
...data,
isActive: true,
},
});
}
export async function updateBankCard(
id: number,
data: {
accountHolder?: string;
iban?: string;
bic?: string;
bankName?: string;
expiryDate?: Date;
isActive?: boolean;
}
) {
return prisma.bankCard.update({
where: { id },
data,
});
}
export async function deleteBankCard(id: number) {
// Erst Datei-Pfad holen, dann Datei löschen, dann DB-Eintrag löschen
const bankCard = await prisma.bankCard.findUnique({ where: { id } });
if (bankCard?.documentPath) {
deleteFileIfExists(bankCard.documentPath);
}
return prisma.bankCard.delete({ where: { id } });
}
// Identity document operations
export async function getCustomerDocuments(
customerId: number,
showInactive: boolean = false
) {
const where: Record<string, unknown> = { customerId };
if (!showInactive) {
where.isActive = true;
}
return prisma.identityDocument.findMany({
where,
orderBy: [{ isActive: 'desc' }, { createdAt: 'desc' }],
});
}
export async function createDocument(
customerId: number,
data: {
type: 'ID_CARD' | 'PASSPORT' | 'DRIVERS_LICENSE' | 'OTHER';
documentNumber: string;
issuingAuthority?: string;
issueDate?: Date;
expiryDate?: Date;
licenseClasses?: string;
licenseIssueDate?: Date;
}
) {
return prisma.identityDocument.create({
data: {
customerId,
...data,
isActive: true,
},
});
}
export async function updateDocument(
id: number,
data: {
type?: 'ID_CARD' | 'PASSPORT' | 'DRIVERS_LICENSE' | 'OTHER';
documentNumber?: string;
issuingAuthority?: string;
issueDate?: Date;
expiryDate?: Date;
licenseClasses?: string;
licenseIssueDate?: Date;
isActive?: boolean;
}
) {
return prisma.identityDocument.update({
where: { id },
data,
});
}
export async function deleteDocument(id: number) {
// Erst Datei-Pfad holen, dann Datei löschen, dann DB-Eintrag löschen
const document = await prisma.identityDocument.findUnique({ where: { id } });
if (document?.documentPath) {
deleteFileIfExists(document.documentPath);
}
return prisma.identityDocument.delete({ where: { id } });
}
// Meter operations
export async function getCustomerMeters(
customerId: number,
showInactive: boolean = false
) {
const where: Record<string, unknown> = { customerId };
if (!showInactive) {
where.isActive = true;
}
return prisma.meter.findMany({
where,
include: {
address: true,
readings: {
orderBy: { readingDate: 'desc' },
take: 5,
},
},
orderBy: [{ isActive: 'desc' }, { createdAt: 'desc' }],
});
}
// Schreibt den Endstand des Vorgänger-Zählers beim Zählerwechsel als
// MeterReading. Wird beim Folgezähler-Anlegen aufgerufen (sowohl aus der
// Kundenakte als auch aus der Vertragsansicht). Idempotent: existiert am
// Wechseltag schon ein Reading, wird nichts angelegt. Validierung
// monoton-steigend wird durchgereicht wirft bei Konflikt.
export async function recordPredecessorFinalReading(
predecessorMeterId: number,
switchAt: Date,
value: number,
) {
const meter = await prisma.meter.findUnique({
where: { id: predecessorMeterId },
select: { type: true },
});
if (!meter) return;
const dayStart = new Date(switchAt);
dayStart.setHours(0, 0, 0, 0);
const dayEnd = new Date(dayStart);
dayEnd.setDate(dayEnd.getDate() + 1);
const existingSameDay = await prisma.meterReading.findFirst({
where: { meterId: predecessorMeterId, readingDate: { gte: dayStart, lt: dayEnd } },
});
if (existingSameDay) return;
await validateReadingValue(predecessorMeterId, switchAt, value, undefined, 'HT');
await prisma.meterReading.create({
data: {
meterId: predecessorMeterId,
readingDate: switchAt,
value,
unit: meter.type === 'GAS' ? 'm³' : 'kWh',
notes: 'Endstand bei Zählerwechsel (automatisch beim Folgezähler-Anlegen erfasst)',
},
});
}
// Lieferadresse muss zum Kunden gehören und vom Typ DELIVERY_RESIDENCE sein.
// Wirft eine sprechende Fehlermeldung, die der Controller dem User durchreicht.
async function assertDeliveryAddressBelongsToCustomer(addressId: number, customerId: number) {
const addr = await prisma.address.findUnique({ where: { id: addressId } });
if (!addr || addr.customerId !== customerId) {
throw new Error('Ungültige Lieferadresse');
}
if (addr.type !== 'DELIVERY_RESIDENCE') {
throw new Error('Nur Lieferadressen können einem Zähler zugeordnet werden');
}
}
export async function createMeter(
customerId: number,
data: {
meterNumber: string;
type: 'ELECTRICITY' | 'GAS';
tariffModel?: 'SINGLE' | 'DUAL';
location?: string;
addressId?: number | null;
// Optional: dieser Zähler ersetzt einen bestehenden (Folgezähler).
// Beim Create werden alle Verträge, die den Vorgänger als aktuellen
// Zähler nutzen, automatisch auf den neuen Zähler umgestellt
// (ContractMeter-Eintrag analog zu Vertragsansicht).
successorOf?: {
predecessorMeterId: number;
installedAt?: string;
finalReadingPrevious?: number;
// Default true im UI: alter Zähler wird nach dem Wechsel auf
// isActive=false gesetzt. Kann ausgeschaltet werden, wenn der alte
// Zähler aus irgendeinem Grund noch aktiv bleiben soll.
deactivatePredecessor?: boolean;
};
}
) {
if (data.addressId == null) {
throw new Error('Lieferadresse ist erforderlich');
}
await assertDeliveryAddressBelongsToCustomer(data.addressId, customerId);
// Vorgänger validieren (wenn Folgezähler)
let predecessor: { id: number; customerId: number; type: 'ELECTRICITY' | 'GAS' } | null = null;
if (data.successorOf) {
const pred = await prisma.meter.findUnique({
where: { id: data.successorOf.predecessorMeterId },
select: { id: true, customerId: true, type: true },
});
if (!pred || pred.customerId !== customerId) {
throw new Error('Vorgänger-Zähler nicht gefunden');
}
if (pred.type !== data.type) {
throw new Error('Vorgänger-Zähler muss denselben Typ haben (Strom/Gas)');
}
predecessor = pred;
// Endstand bereits hier validieren, damit kein verwaister Meter entsteht
// wenn der Wert mit bestehenden Zählerständen kollidiert.
if (data.successorOf.finalReadingPrevious != null) {
const switchAt = data.successorOf.installedAt
? new Date(data.successorOf.installedAt)
: new Date();
await validateReadingValue(
pred.id,
switchAt,
data.successorOf.finalReadingPrevious,
undefined,
'HT',
);
}
}
const created = await prisma.meter.create({
data: {
customerId,
meterNumber: data.meterNumber,
type: data.type,
tariffModel: data.tariffModel,
location: data.location,
addressId: data.addressId,
isActive: true,
predecessorMeterId: predecessor?.id,
},
include: { address: true, predecessor: true },
});
// Folgezähler-Propagation: alle Verträge, die den Vorgänger als aktuellen
// Zähler nutzen, bekommen den neuen Zähler als Nachfolger angehängt
// (analog zu addSuccessorMeter im contract.controller).
if (predecessor && data.successorOf) {
const installedAt = data.successorOf.installedAt
? new Date(data.successorOf.installedAt)
: new Date();
const finalReading = data.successorOf.finalReadingPrevious;
const affectedContracts = await prisma.energyContractDetails.findMany({
where: { meterId: predecessor.id },
include: { contractMeters: { orderBy: { position: 'asc' } } },
});
for (const ecd of affectedContracts) {
// Vorhandenen ContractMeter für den Vorgänger als gewechselt markieren.
// Falls noch kein ContractMeter für den Vorgänger existiert (Single-Meter-
// Vertrag vor Multi-Meter-Refactor), legen wir ihn als position 0 an,
// damit die Kette lückenlos ist.
let predCM = ecd.contractMeters.find((cm) => cm.meterId === predecessor!.id);
if (!predCM) {
predCM = await prisma.contractMeter.create({
data: {
energyContractDetailsId: ecd.id,
meterId: predecessor.id,
position: 0,
installedAt: null,
},
});
ecd.contractMeters.push(predCM);
}
await prisma.contractMeter.update({
where: { id: predCM.id },
data: {
removedAt: installedAt,
finalReading: finalReading != null ? finalReading : predCM.finalReading,
},
});
const nextPosition = ecd.contractMeters.length > 0
? Math.max(...ecd.contractMeters.map((cm) => cm.position)) + 1
: 0;
// Idempotenz: falls (durch Doppel-Klick o.ä.) schon ein ContractMeter
// mit dem neuen Zähler existiert, nicht doppelt anlegen.
const existsForNew = await prisma.contractMeter.findUnique({
where: {
energyContractDetailsId_meterId: {
energyContractDetailsId: ecd.id,
meterId: created.id,
},
},
});
if (!existsForNew) {
await prisma.contractMeter.create({
data: {
energyContractDetailsId: ecd.id,
meterId: created.id,
position: nextPosition,
installedAt,
},
});
}
// Aktuellen Zähler am Vertrag aktualisieren
await prisma.energyContractDetails.update({
where: { id: ecd.id },
data: { meterId: created.id },
});
}
// Endstand des Vorgängers als regulären Zählerstand erfassen, damit er
// in die Verbrauchsberechnung einfließt und in der Zählerstände-Liste
// sichtbar ist. Idempotent gegen Doppel-Submit.
if (data.successorOf.finalReadingPrevious != null) {
await recordPredecessorFinalReading(
predecessor.id,
installedAt,
data.successorOf.finalReadingPrevious,
);
}
// Alten Zähler deaktivieren (Default), sofern der Aufrufer das nicht
// explizit auf false setzt. Macht den typischen Zählerwechsel-Workflow
// ein-klick-fähig.
if (data.successorOf.deactivatePredecessor !== false) {
await prisma.meter.update({
where: { id: predecessor.id },
data: { isActive: false },
});
}
}
return created;
}
export async function updateMeter(
id: number,
data: {
meterNumber?: string;
type?: 'ELECTRICITY' | 'GAS';
tariffModel?: 'SINGLE' | 'DUAL';
location?: string;
isActive?: boolean;
addressId?: number | null;
}
) {
if (data.addressId !== undefined && data.addressId !== null) {
const meter = await prisma.meter.findUnique({ where: { id }, select: { customerId: true } });
if (!meter) throw new Error('Zähler nicht gefunden');
await assertDeliveryAddressBelongsToCustomer(data.addressId, meter.customerId);
}
return prisma.meter.update({
where: { id },
data,
include: { address: true },
});
}
export async function deleteMeter(id: number) {
// Prüfen ob der Zähler noch an Verträgen hängt
const linkedContracts = await prisma.contractMeter.findMany({
where: { meterId: id },
include: { energyContractDetails: { include: { contract: { select: { contractNumber: true } } } } },
});
if (linkedContracts.length > 0) {
const contractNumbers = linkedContracts
.map(cm => cm.energyContractDetails.contract.contractNumber)
.join(', ');
throw new Error(`Zähler kann nicht gelöscht werden noch an Vertrag/Verträgen zugeordnet: ${contractNumbers}`);
}
// Auch direkte meterId-Referenz auf EnergyContractDetails prüfen
const directLinks = await prisma.energyContractDetails.findMany({
where: { meterId: id },
include: { contract: { select: { contractNumber: true } } },
});
if (directLinks.length > 0) {
const contractNumbers = directLinks.map(d => d.contract.contractNumber).join(', ');
throw new Error(`Zähler kann nicht gelöscht werden noch an Vertrag/Verträgen zugeordnet: ${contractNumbers}`);
}
return prisma.meter.delete({ where: { id } });
}
export async function addMeterReading(
meterId: number,
data: {
readingDate: Date;
value: number;
valueNt?: number;
unit?: string;
notes?: string;
}
) {
// Validierung: Zählerstand muss monoton steigend sein
await validateReadingValue(meterId, data.readingDate, data.value, undefined, 'HT');
if (data.valueNt !== undefined) {
await validateReadingValue(meterId, data.readingDate, data.valueNt, undefined, 'NT');
}
return prisma.meterReading.create({
data: {
meterId,
readingDate: data.readingDate,
value: data.value,
valueNt: data.valueNt,
unit: data.unit,
notes: data.notes,
},
});
}
export async function getMeterReadings(meterId: number) {
return prisma.meterReading.findMany({
where: { meterId },
orderBy: { readingDate: 'desc' },
});
}
export async function updateMeterReading(
meterId: number,
readingId: number,
data: {
readingDate?: Date;
value?: number;
valueNt?: number | null;
unit?: string;
notes?: string;
}
) {
// Verify the reading belongs to the meter
const reading = await prisma.meterReading.findFirst({
where: { id: readingId, meterId },
});
if (!reading) {
throw new Error('Zählerstand nicht gefunden');
}
// Validierung bei Wertänderung
if (data.value !== undefined || data.readingDate !== undefined) {
await validateReadingValue(
meterId,
data.readingDate || reading.readingDate,
data.value ?? reading.value,
readingId,
'HT'
);
}
if (data.valueNt !== undefined || data.readingDate !== undefined) {
const ntVal = data.valueNt ?? reading.valueNt;
if (ntVal !== undefined && ntVal !== null) {
await validateReadingValue(
meterId,
data.readingDate || reading.readingDate,
ntVal,
readingId,
'NT'
);
}
}
return prisma.meterReading.update({
where: { id: readingId },
data,
});
}
/**
* Validiert, dass ein Zählerstand monoton steigend ist.
* tariffLabel: 'HT' für Hochtarif/Eintarif, 'NT' für Niedertarif
*/
async function validateReadingValue(meterId: number, readingDate: Date, value: number, excludeReadingId?: number, tariffLabel: 'HT' | 'NT' = 'HT') {
const existing = await prisma.meterReading.findMany({
where: { meterId, ...(excludeReadingId ? { id: { not: excludeReadingId } } : {}) },
orderBy: { readingDate: 'asc' },
});
const fmtDate = (d: Date) => d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
const fmtVal = (v: number) => v.toLocaleString('de-DE');
const label = tariffLabel === 'NT' ? 'NT-Zählerstand' : 'Zählerstand';
// Vergleichswert aus bestehendem Reading extrahieren
const getVal = (r: typeof existing[0]) => tariffLabel === 'NT' ? (r.valueNt ?? 0) : r.value;
// Stand vor dem neuen Datum
const before = [...existing].filter(r => r.readingDate <= readingDate).pop();
if (before && value < getVal(before)) {
throw new Error(`${label} (${fmtVal(value)}) darf nicht kleiner sein als der Stand vom ${fmtDate(before.readingDate)} (${fmtVal(getVal(before))})`);
}
// Stand nach dem neuen Datum
const after = existing.find(r => r.readingDate > readingDate);
if (after && value > getVal(after)) {
throw new Error(`${label} (${fmtVal(value)}) darf nicht größer sein als der spätere Stand vom ${fmtDate(after.readingDate)} (${fmtVal(getVal(after))})`);
}
}
export async function deleteMeterReading(meterId: number, readingId: number) {
// Verify the reading belongs to the meter
const reading = await prisma.meterReading.findFirst({
where: { id: readingId, meterId },
});
if (!reading) {
throw new Error('Zählerstand nicht gefunden');
}
return prisma.meterReading.delete({
where: { id: readingId },
});
}
// ==================== PORTAL SETTINGS ====================
export async function updatePortalSettings(
customerId: number,
data: {
portalEnabled?: boolean;
portalEmail?: string | null;
}
) {
// Wenn Portal deaktiviert wird, Passwort-Hash nicht löschen (für spätere Reaktivierung)
return prisma.customer.update({
where: { id: customerId },
data: {
portalEnabled: data.portalEnabled,
portalEmail: data.portalEmail,
},
select: {
id: true,
portalEnabled: true,
portalEmail: true,
portalLastLogin: true,
},
});
}
export async function getPortalSettings(customerId: number) {
return prisma.customer.findUnique({
where: { id: customerId },
select: {
id: true,
portalEnabled: true,
portalEmail: true,
portalLastLogin: true,
portalPasswordHash: true, // Nur um zu prüfen ob Passwort gesetzt (wird als boolean zurückgegeben)
},
});
}
// ==================== REPRESENTATIVE MANAGEMENT ====================
export async function getCustomerRepresentatives(customerId: number) {
// Holt alle Kunden, die der angegebene Kunde vertreten kann (dieser ist der Vertreter)
return prisma.customerRepresentative.findMany({
where: { representativeId: customerId, isActive: true },
include: {
customer: {
select: {
id: true,
customerNumber: true,
firstName: true,
lastName: true,
companyName: true,
type: true,
},
},
},
orderBy: { createdAt: 'desc' },
});
}
export async function getRepresentedByList(customerId: number) {
// Holt alle Kunden, die den angegebenen Kunden vertreten können
return prisma.customerRepresentative.findMany({
where: { customerId: customerId, isActive: true },
include: {
representative: {
select: {
id: true,
customerNumber: true,
firstName: true,
lastName: true,
companyName: true,
type: true,
},
},
},
orderBy: { createdAt: 'desc' },
});
}
export async function addRepresentative(
customerId: number, // Der Kunde, dessen Verträge eingesehen werden dürfen
representativeId: number, // Der Kunde, der einsehen darf
notes?: string
) {
// Prüfen, ob beide Kunden existieren
const [customer, representative] = await Promise.all([
prisma.customer.findUnique({ where: { id: customerId } }),
prisma.customer.findUnique({ where: { id: representativeId } }),
]);
if (!customer) {
throw new Error('Kunde nicht gefunden');
}
if (!representative) {
throw new Error('Vertreter-Kunde nicht gefunden');
}
if (customerId === representativeId) {
throw new Error('Ein Kunde kann sich nicht selbst vertreten');
}
// Prüfen ob der Vertreter ein Portal-Konto hat
if (!representative.portalEnabled) {
throw new Error('Der Vertreter-Kunde muss ein aktiviertes Portal-Konto haben');
}
return prisma.customerRepresentative.upsert({
where: {
customerId_representativeId: { customerId, representativeId },
},
create: {
customerId,
representativeId,
notes,
isActive: true,
},
update: {
isActive: true,
notes,
},
include: {
representative: {
select: {
id: true,
customerNumber: true,
firstName: true,
lastName: true,
companyName: true,
type: true,
},
},
},
});
}
export async function removeRepresentative(customerId: number, representativeId: number) {
// Anstatt zu löschen, setzen wir isActive auf false
return prisma.customerRepresentative.update({
where: {
customerId_representativeId: { customerId, representativeId },
},
data: { isActive: false },
});
}
export async function searchCustomersForRepresentative(search: string, excludeCustomerId: number) {
// Sucht Kunden, die als Vertreter hinzugefügt werden können
// Nur Kunden mit aktiviertem Portal
return prisma.customer.findMany({
where: {
id: { not: excludeCustomerId },
portalEnabled: true,
OR: [
{ firstName: { contains: search } },
{ lastName: { contains: search } },
{ companyName: { contains: search } },
{ customerNumber: { contains: search } },
],
},
select: {
id: true,
customerNumber: true,
firstName: true,
lastName: true,
companyName: true,
type: true,
},
take: 10,
});
}