first commit
This commit is contained in:
@@ -0,0 +1,682 @@
|
||||
import { PrismaClient, CustomerType, ContractStatus } from '@prisma/client';
|
||||
import { generateCustomerNumber, paginate, buildPaginationResponse } from '../utils/helpers.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
export async function getAllCustomers(filters: CustomerFilters) {
|
||||
const { search, type, page = 1, limit = 20 } = filters;
|
||||
const { skip, take } = paginate(page, limit);
|
||||
|
||||
const where: Record<string, unknown> = {};
|
||||
|
||||
if (type) {
|
||||
where.type = type;
|
||||
}
|
||||
|
||||
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: {
|
||||
readings: {
|
||||
orderBy: { readingDate: 'desc' },
|
||||
},
|
||||
},
|
||||
},
|
||||
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;
|
||||
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.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: {
|
||||
readings: {
|
||||
orderBy: { readingDate: 'desc' },
|
||||
take: 5,
|
||||
},
|
||||
},
|
||||
orderBy: [{ isActive: 'desc' }, { createdAt: 'desc' }],
|
||||
});
|
||||
}
|
||||
|
||||
export async function createMeter(
|
||||
customerId: number,
|
||||
data: {
|
||||
meterNumber: string;
|
||||
type: 'ELECTRICITY' | 'GAS';
|
||||
location?: string;
|
||||
}
|
||||
) {
|
||||
return prisma.meter.create({
|
||||
data: {
|
||||
customerId,
|
||||
...data,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateMeter(
|
||||
id: number,
|
||||
data: {
|
||||
meterNumber?: string;
|
||||
type?: 'ELECTRICITY' | 'GAS';
|
||||
location?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
) {
|
||||
return prisma.meter.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteMeter(id: number) {
|
||||
return prisma.meter.delete({ where: { id } });
|
||||
}
|
||||
|
||||
export async function addMeterReading(
|
||||
meterId: number,
|
||||
data: {
|
||||
readingDate: Date;
|
||||
value: number;
|
||||
unit?: string;
|
||||
notes?: string;
|
||||
}
|
||||
) {
|
||||
return prisma.meterReading.create({
|
||||
data: {
|
||||
meterId,
|
||||
...data,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
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');
|
||||
}
|
||||
|
||||
return prisma.meterReading.update({
|
||||
where: { id: readingId },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user