first commit
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { Response } from 'express';
|
||||
import * as appSettingService from '../services/appSetting.service.js';
|
||||
import { ApiResponse, AuthRequest } from '../types/index.js';
|
||||
|
||||
export async function getAllSettings(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const settings = await appSettingService.getAllSettings();
|
||||
res.json({ success: true, data: settings } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Einstellungen',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPublicSettings(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const settings = await appSettingService.getPublicSettings();
|
||||
res.json({ success: true, data: settings } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Einstellungen',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateSetting(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const { key } = req.params;
|
||||
const { value } = req.body;
|
||||
|
||||
if (value === undefined) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Wert ist erforderlich',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
await appSettingService.setSetting(key, String(value));
|
||||
res.json({ success: true, message: 'Einstellung gespeichert' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Speichern der Einstellung',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateSettings(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const settings = req.body;
|
||||
|
||||
if (!settings || typeof settings !== 'object') {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Einstellungen sind erforderlich',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(settings)) {
|
||||
await appSettingService.setSetting(key, String(value));
|
||||
}
|
||||
|
||||
res.json({ success: true, message: 'Einstellungen gespeichert' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Speichern der Einstellungen',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as authService from '../services/auth.service.js';
|
||||
import { AuthRequest, ApiResponse } from '../types/index.js';
|
||||
|
||||
// Mitarbeiter-Login
|
||||
export async function login(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'E-Mail und Passwort erforderlich',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await authService.login(email, password);
|
||||
res.json({ success: true, data: result } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Anmeldung fehlgeschlagen',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// Kundenportal-Login
|
||||
export async function customerLogin(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'E-Mail und Passwort erforderlich',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await authService.customerLogin(email, password);
|
||||
res.json({ success: true, data: result } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Anmeldung fehlgeschlagen',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function me(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
if (!req.user) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: 'Nicht authentifiziert',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Kundenportal-Login
|
||||
if (req.user.isCustomerPortal && req.user.customerId) {
|
||||
const customer = await authService.getCustomerPortalUser(req.user.customerId);
|
||||
if (!customer) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Kunde nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: customer } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mitarbeiter-Login
|
||||
if (!req.user.userId) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: 'Ungültige Authentifizierung',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await authService.getUserById(req.user.userId);
|
||||
if (!user) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Benutzer nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true, data: user } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Benutzerdaten',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function register(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { email, password, firstName, lastName, roleIds } = req.body;
|
||||
|
||||
if (!email || !password || !firstName || !lastName) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Alle Pflichtfelder müssen ausgefüllt sein',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await authService.createUser({
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
roleIds: roleIds || [2], // Default to employee role
|
||||
});
|
||||
|
||||
res.status(201).json({ success: true, data: user } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Benutzer konnte nicht erstellt werden',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as cancellationPeriodService from '../services/cancellation-period.service.js';
|
||||
import { ApiResponse } from '../types/index.js';
|
||||
|
||||
export async function getCancellationPeriods(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const includeInactive = req.query.includeInactive === 'true';
|
||||
const periods = await cancellationPeriodService.getAllCancellationPeriods(includeInactive);
|
||||
res.json({ success: true, data: periods } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Kündigungsfristen',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCancellationPeriod(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const period = await cancellationPeriodService.getCancellationPeriodById(parseInt(req.params.id));
|
||||
if (!period) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Kündigungsfrist nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: period } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Kündigungsfrist',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCancellationPeriod(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const period = await cancellationPeriodService.createCancellationPeriod(req.body);
|
||||
res.status(201).json({ success: true, data: period } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Kündigungsfrist',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateCancellationPeriod(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const period = await cancellationPeriodService.updateCancellationPeriod(parseInt(req.params.id), req.body);
|
||||
res.json({ success: true, data: period } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Kündigungsfrist',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCancellationPeriod(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await cancellationPeriodService.deleteCancellationPeriod(parseInt(req.params.id));
|
||||
res.json({ success: true, message: 'Kündigungsfrist gelöscht' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Kündigungsfrist',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as contractDurationService from '../services/contract-duration.service.js';
|
||||
import { ApiResponse } from '../types/index.js';
|
||||
|
||||
export async function getContractDurations(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const includeInactive = req.query.includeInactive === 'true';
|
||||
const durations = await contractDurationService.getAllContractDurations(includeInactive);
|
||||
res.json({ success: true, data: durations } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Laufzeiten',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getContractDuration(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const duration = await contractDurationService.getContractDurationById(parseInt(req.params.id));
|
||||
if (!duration) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Laufzeit nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: duration } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Laufzeit',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createContractDuration(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const duration = await contractDurationService.createContractDuration(req.body);
|
||||
res.status(201).json({ success: true, data: duration } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Laufzeit',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateContractDuration(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const duration = await contractDurationService.updateContractDuration(parseInt(req.params.id), req.body);
|
||||
res.json({ success: true, data: duration } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Laufzeit',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteContractDuration(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await contractDurationService.deleteContractDuration(parseInt(req.params.id));
|
||||
res.json({ success: true, message: 'Laufzeit gelöscht' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Laufzeit',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as contractService from '../services/contract.service.js';
|
||||
import * as contractCockpitService from '../services/contractCockpit.service.js';
|
||||
import { ApiResponse, AuthRequest } from '../types/index.js';
|
||||
|
||||
export async function getContracts(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const { customerId, type, status, search, page, limit } = req.query;
|
||||
|
||||
// Für Kundenportal-Benutzer: nur eigene + vertretene Kunden-Verträge anzeigen
|
||||
let customerIds: number[] | undefined;
|
||||
if (req.user?.isCustomerPortal && req.user.customerId) {
|
||||
// Eigene Customer-ID + alle vertretenen Kunden-IDs
|
||||
customerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])];
|
||||
}
|
||||
|
||||
const result = await contractService.getAllContracts({
|
||||
customerId: customerId ? parseInt(customerId as string) : undefined,
|
||||
customerIds, // Wird nur für Kundenportal-Benutzer gesetzt
|
||||
type: type as any,
|
||||
status: status as any,
|
||||
search: search as string,
|
||||
page: page ? parseInt(page as string) : undefined,
|
||||
limit: limit ? parseInt(limit as string) : undefined,
|
||||
});
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.contracts,
|
||||
pagination: result.pagination,
|
||||
} as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Verträge',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getContract(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const contract = await contractService.getContractById(parseInt(req.params.id));
|
||||
if (!contract) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Vertrag nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Für Kundenportal-Benutzer: Zugriff nur auf eigene + vertretene Kunden-Verträge
|
||||
if (req.user?.isCustomerPortal && req.user.customerId) {
|
||||
const allowedCustomerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])];
|
||||
if (!allowedCustomerIds.includes(contract.customerId)) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: 'Kein Zugriff auf diesen Vertrag',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ success: true, data: contract } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden des Vertrags',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createContract(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const contract = await contractService.createContract(req.body);
|
||||
res.status(201).json({ success: true, data: contract } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Vertrags',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateContract(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const contract = await contractService.updateContract(parseInt(req.params.id), req.body);
|
||||
res.json({ success: true, data: contract } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Vertrags',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteContract(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await contractService.deleteContract(parseInt(req.params.id));
|
||||
res.json({ success: true, message: 'Vertrag gelöscht' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Vertrags',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createFollowUp(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const contract = await contractService.createFollowUpContract(parseInt(req.params.id));
|
||||
res.status(201).json({ success: true, data: contract } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Folgevertrags',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getContractPassword(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const password = await contractService.getContractPassword(parseInt(req.params.id));
|
||||
if (password === null) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Kein Passwort hinterlegt',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: { password } } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Entschlüsseln des Passworts',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSimCardCredentials(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const credentials = await contractService.getSimCardCredentials(parseInt(req.params.simCardId));
|
||||
res.json({ success: true, data: credentials } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Entschlüsseln der SIM-Karten-Daten',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getInternetCredentials(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const credentials = await contractService.getInternetCredentials(parseInt(req.params.id));
|
||||
res.json({ success: true, data: credentials } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Entschlüsseln des Internet-Passworts',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSipCredentials(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const credentials = await contractService.getSipCredentials(parseInt(req.params.phoneNumberId));
|
||||
res.json({ success: true, data: credentials } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Entschlüsseln des SIP-Passworts',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== VERTRAGS-COCKPIT ====================
|
||||
|
||||
export async function getCockpit(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const cockpitData = await contractCockpitService.getCockpitData();
|
||||
res.json({ success: true, data: cockpitData } as ApiResponse);
|
||||
} catch (error) {
|
||||
console.error('Cockpit error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden des Vertrags-Cockpits',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as contractCategoryService from '../services/contractCategory.service.js';
|
||||
import { ApiResponse } from '../types/index.js';
|
||||
|
||||
export async function getContractCategories(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const includeInactive = req.query.includeInactive === 'true';
|
||||
const categories = await contractCategoryService.getAllContractCategories(includeInactive);
|
||||
res.json({ success: true, data: categories } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Vertragskategorien',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getContractCategory(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const category = await contractCategoryService.getContractCategoryById(parseInt(req.params.id));
|
||||
if (!category) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Vertragskategorie nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: category } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Vertragskategorie',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createContractCategory(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const category = await contractCategoryService.createContractCategory(req.body);
|
||||
res.status(201).json({ success: true, data: category } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Vertragskategorie',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateContractCategory(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const category = await contractCategoryService.updateContractCategory(parseInt(req.params.id), req.body);
|
||||
res.json({ success: true, data: category } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Vertragskategorie',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteContractCategory(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await contractCategoryService.deleteContractCategory(parseInt(req.params.id));
|
||||
res.json({ success: true, message: 'Vertragskategorie gelöscht' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Vertragskategorie',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
import { Response } from 'express';
|
||||
import * as contractTaskService from '../services/contractTask.service.js';
|
||||
import * as contractService from '../services/contract.service.js';
|
||||
import * as customerService from '../services/customer.service.js';
|
||||
import * as appSettingService from '../services/appSetting.service.js';
|
||||
import { ApiResponse, AuthRequest } from '../types/index.js';
|
||||
|
||||
// ==================== ALL TASKS (Dashboard & Task List) ====================
|
||||
|
||||
export async function getAllTasks(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const { status, customerId } = req.query;
|
||||
|
||||
// Für Kundenportal: Filter auf erlaubte Kunden
|
||||
let customerPortalCustomerIds: number[] | undefined;
|
||||
let customerPortalEmails: string[] | undefined;
|
||||
|
||||
if (req.user?.isCustomerPortal && req.user.customerId) {
|
||||
customerPortalCustomerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])];
|
||||
const customers = await customerService.getCustomersByIds(customerPortalCustomerIds);
|
||||
customerPortalEmails = customers
|
||||
.map((c: { id: number; portalEmail: string | null }) => c.portalEmail)
|
||||
.filter((email: string | null): email is string => !!email);
|
||||
}
|
||||
|
||||
const tasks = await contractTaskService.getAllTasks({
|
||||
status: status as 'OPEN' | 'COMPLETED' | undefined,
|
||||
customerId: customerId ? parseInt(customerId as string) : undefined,
|
||||
customerPortalCustomerIds,
|
||||
customerPortalEmails,
|
||||
});
|
||||
|
||||
res.json({ success: true, data: tasks } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Aufgaben',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTaskStats(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
// Für Kundenportal: Filter auf erlaubte Kunden
|
||||
let customerPortalCustomerIds: number[] | undefined;
|
||||
let customerPortalEmails: string[] | undefined;
|
||||
|
||||
if (req.user?.isCustomerPortal && req.user.customerId) {
|
||||
customerPortalCustomerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])];
|
||||
const customers = await customerService.getCustomersByIds(customerPortalCustomerIds);
|
||||
customerPortalEmails = customers
|
||||
.map((c: { id: number; portalEmail: string | null }) => c.portalEmail)
|
||||
.filter((email: string | null): email is string => !!email);
|
||||
}
|
||||
|
||||
const stats = await contractTaskService.getTaskStats({
|
||||
customerPortalCustomerIds,
|
||||
customerPortalEmails,
|
||||
});
|
||||
|
||||
res.json({ success: true, data: stats } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Statistik',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== TASKS BY CONTRACT ====================
|
||||
|
||||
export async function getTasks(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const contractId = parseInt(req.params.contractId);
|
||||
const { status } = req.query;
|
||||
|
||||
// Prüfe Zugriff auf den Vertrag
|
||||
const contract = await contractService.getContractById(contractId);
|
||||
if (!contract) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Vertrag nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Für Kundenportal: Zugriffsprüfung
|
||||
if (req.user?.isCustomerPortal && req.user.customerId) {
|
||||
const allowedCustomerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])];
|
||||
if (!allowedCustomerIds.includes(contract.customerId)) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: 'Kein Zugriff auf diesen Vertrag',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Für Kundenportal-Benutzer: Lade E-Mails der erlaubten Kunden
|
||||
let customerPortalEmails: string[] | undefined;
|
||||
if (req.user?.isCustomerPortal && req.user.customerId) {
|
||||
const allowedCustomerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])];
|
||||
const customers = await customerService.getCustomersByIds(allowedCustomerIds);
|
||||
customerPortalEmails = customers
|
||||
.map((c: { id: number; portalEmail: string | null }) => c.portalEmail)
|
||||
.filter((email: string | null): email is string => !!email);
|
||||
}
|
||||
|
||||
const tasks = await contractTaskService.getTasksByContract({
|
||||
contractId,
|
||||
status: status as 'OPEN' | 'COMPLETED' | undefined,
|
||||
customerPortalEmails,
|
||||
});
|
||||
|
||||
res.json({ success: true, data: tasks } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Aufgaben',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createTask(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const contractId = parseInt(req.params.contractId);
|
||||
const { title, description, visibleInPortal } = req.body;
|
||||
|
||||
if (!title) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Titel ist erforderlich',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const createdBy = req.user?.email;
|
||||
|
||||
// Für Kundenportal-Benutzer: visibleInPortal wird automatisch auf true gesetzt
|
||||
const finalVisibleInPortal = req.user?.isCustomerPortal ? true : visibleInPortal;
|
||||
|
||||
const task = await contractTaskService.createTask({
|
||||
contractId,
|
||||
title,
|
||||
description,
|
||||
visibleInPortal: finalVisibleInPortal,
|
||||
createdBy,
|
||||
});
|
||||
|
||||
res.status(201).json({ success: true, data: task } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Aufgabe',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// Für Kundenportal-Benutzer: Support-Anfrage erstellen (ohne contracts:update Permission)
|
||||
export async function createSupportTicket(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
// Prüfe ob Support-Tickets aktiviert sind
|
||||
const supportEnabled = await appSettingService.getSettingBool('customerSupportTicketsEnabled');
|
||||
if (!supportEnabled) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: 'Support-Anfragen sind nicht aktiviert',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const contractId = parseInt(req.params.contractId);
|
||||
const { title, description } = req.body;
|
||||
|
||||
if (!title) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Titel ist erforderlich',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfe Zugriff auf den Vertrag
|
||||
const contract = await contractService.getContractById(contractId);
|
||||
if (!contract) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Vertrag nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Zugriffsprüfung für Kundenportal
|
||||
if (req.user?.customerId) {
|
||||
const allowedCustomerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])];
|
||||
if (!allowedCustomerIds.includes(contract.customerId)) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: 'Kein Zugriff auf diesen Vertrag',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const createdBy = req.user?.email;
|
||||
|
||||
const task = await contractTaskService.createTask({
|
||||
contractId,
|
||||
title,
|
||||
description,
|
||||
visibleInPortal: true, // Immer sichtbar im Portal
|
||||
createdBy,
|
||||
});
|
||||
|
||||
res.status(201).json({ success: true, data: task } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Support-Anfrage',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateTask(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const taskId = parseInt(req.params.taskId);
|
||||
const { title, description, visibleInPortal } = req.body;
|
||||
|
||||
const task = await contractTaskService.updateTask(taskId, {
|
||||
title,
|
||||
description,
|
||||
visibleInPortal,
|
||||
});
|
||||
|
||||
res.json({ success: true, data: task } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Aufgabe',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function completeTask(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const taskId = parseInt(req.params.taskId);
|
||||
const task = await contractTaskService.completeTask(taskId);
|
||||
res.json({ success: true, data: task } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Abschließen der Aufgabe',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function reopenTask(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const taskId = parseInt(req.params.taskId);
|
||||
const task = await contractTaskService.reopenTask(taskId);
|
||||
res.json({ success: true, data: task } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Wiedereröffnen der Aufgabe',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteTask(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const taskId = parseInt(req.params.taskId);
|
||||
await contractTaskService.deleteTask(taskId);
|
||||
res.json({ success: true, message: 'Aufgabe gelöscht' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Aufgabe',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== SUBTASKS ====================
|
||||
|
||||
export async function createSubtask(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const taskId = parseInt(req.params.taskId);
|
||||
const { title } = req.body;
|
||||
|
||||
if (!title) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Titel ist erforderlich',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const createdBy = req.user?.email;
|
||||
|
||||
const subtask = await contractTaskService.createSubtask({
|
||||
taskId,
|
||||
title,
|
||||
createdBy,
|
||||
});
|
||||
|
||||
res.status(201).json({ success: true, data: subtask } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Unteraufgabe',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// Kundenportal: Antwort auf eigenes Ticket erstellen
|
||||
export async function createCustomerReply(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const taskId = parseInt(req.params.taskId);
|
||||
const { title } = req.body;
|
||||
|
||||
if (!title) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Antwort ist erforderlich',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Hole den Task
|
||||
const task = await contractTaskService.getTaskById(taskId);
|
||||
if (!task) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Anfrage nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfe ob der Kunde berechtigt ist (eigenes Ticket oder freigegebener Kunde)
|
||||
if (req.user?.isCustomerPortal && req.user.customerId) {
|
||||
const allowedCustomerIds = [req.user.customerId, ...(req.user.representedCustomerIds || [])];
|
||||
const customers = await customerService.getCustomersByIds(allowedCustomerIds);
|
||||
const allowedEmails = customers
|
||||
.map((c: { id: number; portalEmail: string | null }) => c.portalEmail)
|
||||
.filter((email: string | null): email is string => !!email);
|
||||
|
||||
// Task muss entweder visibleInPortal sein ODER vom Kunden erstellt worden sein
|
||||
const isOwnTask = task.createdBy && allowedEmails.includes(task.createdBy);
|
||||
if (!task.visibleInPortal && !isOwnTask) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: 'Kein Zugriff auf diese Anfrage',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: 'Nur für Kundenportal-Benutzer',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const createdBy = req.user?.email;
|
||||
|
||||
const subtask = await contractTaskService.createSubtask({
|
||||
taskId,
|
||||
title,
|
||||
createdBy,
|
||||
});
|
||||
|
||||
res.status(201).json({ success: true, data: subtask } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Antwort',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateSubtask(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const subtaskId = parseInt(req.params.subtaskId);
|
||||
const { title } = req.body;
|
||||
|
||||
if (!title) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Titel ist erforderlich',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const subtask = await contractTaskService.updateSubtask(subtaskId, { title });
|
||||
res.json({ success: true, data: subtask } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Unteraufgabe',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function completeSubtask(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const subtaskId = parseInt(req.params.subtaskId);
|
||||
const subtask = await contractTaskService.completeSubtask(subtaskId);
|
||||
res.json({ success: true, data: subtask } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Abschließen der Unteraufgabe',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function reopenSubtask(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const subtaskId = parseInt(req.params.subtaskId);
|
||||
const subtask = await contractTaskService.reopenSubtask(subtaskId);
|
||||
res.json({ success: true, data: subtask } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Wiedereröffnen der Unteraufgabe',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteSubtask(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const subtaskId = parseInt(req.params.subtaskId);
|
||||
await contractTaskService.deleteSubtask(subtaskId);
|
||||
res.json({ success: true, message: 'Unteraufgabe gelöscht' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Unteraufgabe',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as customerService from '../services/customer.service.js';
|
||||
import * as authService from '../services/auth.service.js';
|
||||
import { ApiResponse } from '../types/index.js';
|
||||
|
||||
// Customer CRUD
|
||||
export async function getCustomers(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { search, type, page, limit } = req.query;
|
||||
const result = await customerService.getAllCustomers({
|
||||
search: search as string,
|
||||
type: type as 'PRIVATE' | 'BUSINESS',
|
||||
page: page ? parseInt(page as string) : undefined,
|
||||
limit: limit ? parseInt(limit as string) : undefined,
|
||||
});
|
||||
res.json({ success: true, data: result.customers, pagination: result.pagination } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Kunden',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCustomer(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const customer = await customerService.getCustomerById(parseInt(req.params.id));
|
||||
if (!customer) {
|
||||
res.status(404).json({ success: false, error: 'Kunde nicht gefunden' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: customer } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden des Kunden' } as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCustomer(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const data = { ...req.body };
|
||||
// Convert birthDate string to Date if present
|
||||
if (data.birthDate) {
|
||||
data.birthDate = new Date(data.birthDate);
|
||||
}
|
||||
const customer = await customerService.createCustomer(data);
|
||||
res.status(201).json({ success: true, data: customer } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Kunden',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateCustomer(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const data = { ...req.body };
|
||||
// Convert birthDate string to Date if present
|
||||
if (data.birthDate) {
|
||||
data.birthDate = new Date(data.birthDate);
|
||||
}
|
||||
const customer = await customerService.updateCustomer(parseInt(req.params.id), data);
|
||||
res.json({ success: true, data: customer } as ApiResponse);
|
||||
} catch (error) {
|
||||
console.error('Update customer error:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Kunden',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCustomer(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await customerService.deleteCustomer(parseInt(req.params.id));
|
||||
res.json({ success: true, message: 'Kunde gelöscht' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Kunden',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// Addresses
|
||||
export async function getAddresses(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const addresses = await customerService.getCustomerAddresses(parseInt(req.params.customerId));
|
||||
res.json({ success: true, data: addresses } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden der Adressen' } as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createAddress(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const address = await customerService.createAddress(parseInt(req.params.customerId), req.body);
|
||||
res.status(201).json({ success: true, data: address } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Adresse',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateAddress(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const address = await customerService.updateAddress(parseInt(req.params.id), req.body);
|
||||
res.json({ success: true, data: address } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Adresse',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteAddress(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await customerService.deleteAddress(parseInt(req.params.id));
|
||||
res.json({ success: true, message: 'Adresse gelöscht' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Adresse',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// Bank Cards
|
||||
export async function getBankCards(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const showInactive = req.query.showInactive === 'true';
|
||||
const cards = await customerService.getCustomerBankCards(
|
||||
parseInt(req.params.customerId),
|
||||
showInactive
|
||||
);
|
||||
res.json({ success: true, data: cards } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden der Bankkarten' } as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createBankCard(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const card = await customerService.createBankCard(parseInt(req.params.customerId), req.body);
|
||||
res.status(201).json({ success: true, data: card } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Bankkarte',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateBankCard(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const card = await customerService.updateBankCard(parseInt(req.params.id), req.body);
|
||||
res.json({ success: true, data: card } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Bankkarte',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteBankCard(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await customerService.deleteBankCard(parseInt(req.params.id));
|
||||
res.json({ success: true, message: 'Bankkarte gelöscht' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Bankkarte',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// Identity Documents
|
||||
export async function getDocuments(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const showInactive = req.query.showInactive === 'true';
|
||||
const docs = await customerService.getCustomerDocuments(
|
||||
parseInt(req.params.customerId),
|
||||
showInactive
|
||||
);
|
||||
res.json({ success: true, data: docs } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden der Ausweise' } as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createDocument(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const doc = await customerService.createDocument(parseInt(req.params.customerId), req.body);
|
||||
res.status(201).json({ success: true, data: doc } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Ausweises',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateDocument(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const doc = await customerService.updateDocument(parseInt(req.params.id), req.body);
|
||||
res.json({ success: true, data: doc } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Ausweises',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteDocument(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await customerService.deleteDocument(parseInt(req.params.id));
|
||||
res.json({ success: true, message: 'Ausweis gelöscht' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Ausweises',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// Meters
|
||||
export async function getMeters(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const showInactive = req.query.showInactive === 'true';
|
||||
const meters = await customerService.getCustomerMeters(
|
||||
parseInt(req.params.customerId),
|
||||
showInactive
|
||||
);
|
||||
res.json({ success: true, data: meters } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden der Zähler' } as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createMeter(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const meter = await customerService.createMeter(parseInt(req.params.customerId), req.body);
|
||||
res.status(201).json({ success: true, data: meter } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Zählers',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateMeter(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const meter = await customerService.updateMeter(parseInt(req.params.id), req.body);
|
||||
res.json({ success: true, data: meter } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Zählers',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteMeter(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await customerService.deleteMeter(parseInt(req.params.id));
|
||||
res.json({ success: true, message: 'Zähler gelöscht' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Zählers',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// Meter Readings
|
||||
export async function getMeterReadings(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const readings = await customerService.getMeterReadings(parseInt(req.params.meterId));
|
||||
res.json({ success: true, data: readings } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden der Zählerstände' } as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function addMeterReading(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const reading = await customerService.addMeterReading(parseInt(req.params.meterId), req.body);
|
||||
res.status(201).json({ success: true, data: reading } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Hinzufügen des Zählerstands',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateMeterReading(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const reading = await customerService.updateMeterReading(
|
||||
parseInt(req.params.meterId),
|
||||
parseInt(req.params.readingId),
|
||||
req.body
|
||||
);
|
||||
res.json({ success: true, data: reading } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Zählerstands',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteMeterReading(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await customerService.deleteMeterReading(
|
||||
parseInt(req.params.meterId),
|
||||
parseInt(req.params.readingId)
|
||||
);
|
||||
res.json({ success: true, data: null } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Zählerstands',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PORTAL SETTINGS ====================
|
||||
|
||||
export async function getPortalSettings(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const settings = await customerService.getPortalSettings(parseInt(req.params.customerId));
|
||||
if (!settings) {
|
||||
res.status(404).json({ success: false, error: 'Kunde nicht gefunden' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
// Passwort-Hash nicht zurückgeben, nur ob ein Passwort gesetzt ist
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
id: settings.id,
|
||||
portalEnabled: settings.portalEnabled,
|
||||
portalEmail: settings.portalEmail,
|
||||
portalLastLogin: settings.portalLastLogin,
|
||||
hasPassword: !!settings.portalPasswordHash,
|
||||
},
|
||||
} as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Portal-Einstellungen',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updatePortalSettings(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { portalEnabled, portalEmail } = req.body;
|
||||
const settings = await customerService.updatePortalSettings(parseInt(req.params.customerId), {
|
||||
portalEnabled,
|
||||
portalEmail,
|
||||
});
|
||||
res.json({ success: true, data: settings } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Portal-Einstellungen',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setPortalPassword(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { password } = req.body;
|
||||
if (!password || password.length < 6) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Passwort muss mindestens 6 Zeichen lang sein',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
await authService.setCustomerPortalPassword(parseInt(req.params.customerId), password);
|
||||
res.json({ success: true, message: 'Passwort gesetzt' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Setzen des Passworts',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPortalPassword(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const password = await authService.getCustomerPortalPassword(parseInt(req.params.customerId));
|
||||
res.json({ success: true, data: { password } } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Abrufen des Passworts',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== REPRESENTATIVE MANAGEMENT ====================
|
||||
|
||||
export async function getRepresentatives(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
// Wer kann diesen Kunden vertreten (representedBy)?
|
||||
const representedBy = await customerService.getRepresentedByList(parseInt(req.params.customerId));
|
||||
res.json({ success: true, data: representedBy } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Vertreter',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function addRepresentative(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { representativeId, notes } = req.body;
|
||||
const representative = await customerService.addRepresentative(
|
||||
parseInt(req.params.customerId),
|
||||
parseInt(representativeId),
|
||||
notes
|
||||
);
|
||||
res.status(201).json({ success: true, data: representative } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Hinzufügen des Vertreters',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeRepresentative(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await customerService.removeRepresentative(
|
||||
parseInt(req.params.customerId),
|
||||
parseInt(req.params.representativeId)
|
||||
);
|
||||
res.json({ success: true, message: 'Vertreter entfernt' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Entfernen des Vertreters',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchForRepresentative(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { search } = req.query;
|
||||
if (!search || typeof search !== 'string' || search.length < 2) {
|
||||
res.json({ success: true, data: [] } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
const customers = await customerService.searchCustomersForRepresentative(
|
||||
search,
|
||||
parseInt(req.params.customerId)
|
||||
);
|
||||
res.json({ success: true, data: customers } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler bei der Suche',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
// ==================== EMAIL PROVIDER CONTROLLER ====================
|
||||
|
||||
import { Request, Response } from 'express';
|
||||
import * as emailProviderService from '../services/emailProvider/emailProviderService.js';
|
||||
import { ApiResponse } from '../types/index.js';
|
||||
|
||||
// ==================== CONFIG CRUD ====================
|
||||
|
||||
export async function getProviderConfigs(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const configs = await emailProviderService.getAllProviderConfigs();
|
||||
res.json({ success: true, data: configs } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Email-Provider',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProviderConfig(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const config = await emailProviderService.getProviderConfigById(id);
|
||||
|
||||
if (!config) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Email-Provider nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true, data: config } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden des Email-Providers',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createProviderConfig(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const config = await emailProviderService.createProviderConfig(req.body);
|
||||
res.status(201).json({ success: true, data: config } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Email-Providers',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateProviderConfig(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const config = await emailProviderService.updateProviderConfig(id, req.body);
|
||||
res.json({ success: true, data: config } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Email-Providers',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteProviderConfig(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
await emailProviderService.deleteProviderConfig(id);
|
||||
res.json({ success: true, message: 'Email-Provider gelöscht' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Email-Providers',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== EMAIL OPERATIONS ====================
|
||||
|
||||
export async function testConnection(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
// Option 1: Provider-ID für gespeicherten Provider
|
||||
const id = req.body?.id ? parseInt(req.body.id) : undefined;
|
||||
|
||||
// Option 2: Testdaten aus Body (für Test im Modal mit ungespeicherten Daten)
|
||||
const testData = req.body && req.body.type ? {
|
||||
type: req.body.type as 'PLESK' | 'CPANEL' | 'DIRECTADMIN',
|
||||
apiUrl: req.body.apiUrl,
|
||||
apiKey: req.body.apiKey || undefined,
|
||||
username: req.body.username || undefined,
|
||||
password: req.body.password || undefined,
|
||||
domain: req.body.domain,
|
||||
} : undefined;
|
||||
|
||||
const result = await emailProviderService.testProviderConnection({ id, testData });
|
||||
res.json({ success: result.success, data: result } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Verbindungstest fehlgeschlagen',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkEmailExists(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { localPart } = req.params;
|
||||
const result = await emailProviderService.checkEmailExists(localPart);
|
||||
res.json({ success: true, data: result } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler bei der E-Mail-Prüfung',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function provisionEmail(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { localPart, customerEmail } = req.body;
|
||||
|
||||
if (!localPart || !customerEmail) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'localPart und customerEmail sind erforderlich',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await emailProviderService.provisionEmail(localPart, customerEmail);
|
||||
res.json({ success: result.success, data: result } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler bei der E-Mail-Provisionierung',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deprovisionEmail(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { localPart } = req.params;
|
||||
const result = await emailProviderService.deprovisionEmail(localPart);
|
||||
res.json({ success: result.success, data: result } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen der E-Mail',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProviderDomain(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const domain = await emailProviderService.getProviderDomain();
|
||||
res.json({ success: true, data: { domain } } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Domain',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as platformService from '../services/platform.service.js';
|
||||
import { ApiResponse } from '../types/index.js';
|
||||
|
||||
export async function getPlatforms(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const includeInactive = req.query.includeInactive === 'true';
|
||||
const platforms = await platformService.getAllPlatforms(includeInactive);
|
||||
res.json({ success: true, data: platforms } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Vertriebsplattformen',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPlatform(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const platform = await platformService.getPlatformById(parseInt(req.params.id));
|
||||
if (!platform) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Vertriebsplattform nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: platform } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Vertriebsplattform',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createPlatform(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const platform = await platformService.createPlatform(req.body);
|
||||
res.status(201).json({ success: true, data: platform } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Vertriebsplattform',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updatePlatform(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const platform = await platformService.updatePlatform(parseInt(req.params.id), req.body);
|
||||
res.json({ success: true, data: platform } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Vertriebsplattform',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deletePlatform(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await platformService.deletePlatform(parseInt(req.params.id));
|
||||
res.json({ success: true, message: 'Vertriebsplattform gelöscht' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Vertriebsplattform',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as providerService from '../services/provider.service.js';
|
||||
import { ApiResponse } from '../types/index.js';
|
||||
|
||||
export async function getProviders(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const includeInactive = req.query.includeInactive === 'true';
|
||||
const providers = await providerService.getAllProviders(includeInactive);
|
||||
res.json({ success: true, data: providers } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Anbieter',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProvider(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const provider = await providerService.getProviderById(parseInt(req.params.id));
|
||||
if (!provider) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Anbieter nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: provider } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden des Anbieters',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createProvider(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const provider = await providerService.createProvider(req.body);
|
||||
res.status(201).json({ success: true, data: provider } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Anbieters',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateProvider(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const provider = await providerService.updateProvider(parseInt(req.params.id), req.body);
|
||||
res.json({ success: true, data: provider } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Anbieters',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteProvider(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await providerService.deleteProvider(parseInt(req.params.id));
|
||||
res.json({ success: true, message: 'Anbieter gelöscht' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Anbieters',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as stressfreiEmailService from '../services/stressfreiEmail.service.js';
|
||||
import { ApiResponse } from '../types/index.js';
|
||||
|
||||
export async function getEmailsByCustomer(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const customerId = parseInt(req.params.customerId);
|
||||
const includeInactive = req.query.includeInactive === 'true';
|
||||
const emails = await stressfreiEmailService.getEmailsByCustomerId(customerId, includeInactive);
|
||||
res.json({ success: true, data: emails } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Stressfrei-Wechseln Adressen',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getEmail(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const email = await stressfreiEmailService.getEmailById(parseInt(req.params.id));
|
||||
if (!email) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Stressfrei-Wechseln Adresse nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: email } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Stressfrei-Wechseln Adresse',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createEmail(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const customerId = parseInt(req.params.customerId);
|
||||
const email = await stressfreiEmailService.createEmail({
|
||||
...req.body,
|
||||
customerId,
|
||||
});
|
||||
res.status(201).json({ success: true, data: email } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Stressfrei-Wechseln Adresse',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateEmail(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const email = await stressfreiEmailService.updateEmail(parseInt(req.params.id), req.body);
|
||||
res.json({ success: true, data: email } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Stressfrei-Wechseln Adresse',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteEmail(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await stressfreiEmailService.deleteEmail(parseInt(req.params.id));
|
||||
res.json({ success: true, message: 'Stressfrei-Wechseln Adresse gelöscht' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Stressfrei-Wechseln Adresse',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as tariffService from '../services/tariff.service.js';
|
||||
import { ApiResponse } from '../types/index.js';
|
||||
|
||||
export async function getTariffs(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const providerId = parseInt(req.params.providerId);
|
||||
const includeInactive = req.query.includeInactive === 'true';
|
||||
const tariffs = await tariffService.getTariffsByProvider(providerId, includeInactive);
|
||||
res.json({ success: true, data: tariffs } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Tarife',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTariff(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const tariff = await tariffService.getTariffById(parseInt(req.params.id));
|
||||
if (!tariff) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Tarif nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: tariff } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden des Tarifs',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createTariff(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const providerId = parseInt(req.params.providerId);
|
||||
const tariff = await tariffService.createTariff({ ...req.body, providerId });
|
||||
res.status(201).json({ success: true, data: tariff } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Tarifs',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateTariff(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const tariff = await tariffService.updateTariff(parseInt(req.params.id), req.body);
|
||||
res.json({ success: true, data: tariff } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Tarifs',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteTariff(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await tariffService.deleteTariff(parseInt(req.params.id));
|
||||
res.json({ success: true, message: 'Tarif gelöscht' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Tarifs',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as userService from '../services/user.service.js';
|
||||
import { ApiResponse } from '../types/index.js';
|
||||
|
||||
// Users
|
||||
export async function getUsers(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { search, isActive, roleId, page, limit } = req.query;
|
||||
const result = await userService.getAllUsers({
|
||||
search: search as string,
|
||||
isActive: isActive !== undefined ? isActive === 'true' : undefined,
|
||||
roleId: roleId ? parseInt(roleId as string) : undefined,
|
||||
page: page ? parseInt(page as string) : undefined,
|
||||
limit: limit ? parseInt(limit as string) : undefined,
|
||||
});
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.users,
|
||||
pagination: result.pagination,
|
||||
} as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Benutzer',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUser(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const user = await userService.getUserById(parseInt(req.params.id));
|
||||
if (!user) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Benutzer nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: user } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden des Benutzers',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createUser(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const user = await userService.createUser(req.body);
|
||||
res.status(201).json({ success: true, data: user } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen des Benutzers',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateUser(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const user = await userService.updateUser(parseInt(req.params.id), req.body);
|
||||
res.json({ success: true, data: user } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren des Benutzers',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteUser(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await userService.deleteUser(parseInt(req.params.id));
|
||||
res.json({ success: true, message: 'Benutzer gelöscht' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen des Benutzers',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// Roles
|
||||
export async function getRoles(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const roles = await userService.getAllRoles();
|
||||
res.json({ success: true, data: roles } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Rollen',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRole(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const role = await userService.getRoleById(parseInt(req.params.id));
|
||||
if (!role) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Rolle nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: role } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Rolle',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createRole(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const role = await userService.createRole(req.body);
|
||||
res.status(201).json({ success: true, data: role } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen der Rolle',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateRole(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const role = await userService.updateRole(parseInt(req.params.id), req.body);
|
||||
res.json({ success: true, data: role } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Rolle',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteRole(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
await userService.deleteRole(parseInt(req.params.id));
|
||||
res.json({ success: true, message: 'Rolle gelöscht' } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Rolle',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// Permissions
|
||||
export async function getPermissions(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const permissions = await userService.getAllPermissions();
|
||||
res.json({ success: true, data: permissions } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Berechtigungen',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import path from 'path';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
import authRoutes from './routes/auth.routes.js';
|
||||
import customerRoutes from './routes/customer.routes.js';
|
||||
import addressRoutes from './routes/address.routes.js';
|
||||
import bankcardRoutes from './routes/bankcard.routes.js';
|
||||
import documentRoutes from './routes/document.routes.js';
|
||||
import meterRoutes from './routes/meter.routes.js';
|
||||
import stressfreiEmailRoutes from './routes/stressfreiEmail.routes.js';
|
||||
import contractRoutes from './routes/contract.routes.js';
|
||||
import platformRoutes from './routes/platform.routes.js';
|
||||
import cancellationPeriodRoutes from './routes/cancellation-period.routes.js';
|
||||
import contractDurationRoutes from './routes/contract-duration.routes.js';
|
||||
import providerRoutes from './routes/provider.routes.js';
|
||||
import tariffRoutes from './routes/tariff.routes.js';
|
||||
import userRoutes from './routes/user.routes.js';
|
||||
import uploadRoutes from './routes/upload.routes.js';
|
||||
import developerRoutes from './routes/developer.routes.js';
|
||||
import contractCategoryRoutes from './routes/contractCategory.routes.js';
|
||||
import contractTaskRoutes from './routes/contractTask.routes.js';
|
||||
import appSettingRoutes from './routes/appSetting.routes.js';
|
||||
import emailProviderRoutes from './routes/emailProvider.routes.js';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Statische Dateien für Uploads
|
||||
app.use('/api/uploads', express.static(path.join(process.cwd(), 'uploads')));
|
||||
|
||||
// Routes
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/customers', customerRoutes);
|
||||
app.use('/api/addresses', addressRoutes);
|
||||
app.use('/api/bank-cards', bankcardRoutes);
|
||||
app.use('/api/documents', documentRoutes);
|
||||
app.use('/api/meters', meterRoutes);
|
||||
app.use('/api/stressfrei-emails', stressfreiEmailRoutes);
|
||||
app.use('/api/contracts', contractRoutes);
|
||||
app.use('/api/platforms', platformRoutes);
|
||||
app.use('/api/cancellation-periods', cancellationPeriodRoutes);
|
||||
app.use('/api/contract-durations', contractDurationRoutes);
|
||||
app.use('/api/providers', providerRoutes);
|
||||
app.use('/api/tariffs', tariffRoutes);
|
||||
app.use('/api/users', userRoutes);
|
||||
app.use('/api/upload', uploadRoutes);
|
||||
app.use('/api/developer', developerRoutes);
|
||||
app.use('/api/contract-categories', contractCategoryRoutes);
|
||||
app.use('/api', contractTaskRoutes);
|
||||
app.use('/api/settings', appSettingRoutes);
|
||||
app.use('/api/email-providers', emailProviderRoutes);
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Error handling
|
||||
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
console.error(err.stack);
|
||||
res.status(500).json({ success: false, error: 'Interner Serverfehler' });
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server läuft auf Port ${PORT}`);
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { AuthRequest, JwtPayload } from '../types/index.js';
|
||||
|
||||
export function authenticate(
|
||||
req: AuthRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): void {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
res.status(401).json({ success: false, error: 'Nicht authentifiziert' });
|
||||
return;
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(
|
||||
token,
|
||||
process.env.JWT_SECRET || 'fallback-secret'
|
||||
) as JwtPayload;
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({ success: false, error: 'Ungültiger Token' });
|
||||
}
|
||||
}
|
||||
|
||||
export function requirePermission(...requiredPermissions: string[]) {
|
||||
return (req: AuthRequest, res: Response, next: NextFunction): void => {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ success: false, error: 'Nicht authentifiziert' });
|
||||
return;
|
||||
}
|
||||
|
||||
const userPermissions = req.user.permissions || [];
|
||||
|
||||
// Check if user has any of the required permissions
|
||||
const hasPermission = requiredPermissions.some((perm) =>
|
||||
userPermissions.includes(perm)
|
||||
);
|
||||
|
||||
if (!hasPermission) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: 'Keine Berechtigung für diese Aktion',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
// Middleware to check if user can access specific customer data
|
||||
export function requireCustomerAccess(
|
||||
req: AuthRequest,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): void {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ success: false, error: 'Nicht authentifiziert' });
|
||||
return;
|
||||
}
|
||||
|
||||
const userPermissions = req.user.permissions || [];
|
||||
|
||||
// Admins and employees can access all customers
|
||||
if (
|
||||
userPermissions.includes('customers:read') ||
|
||||
userPermissions.includes('customers:update')
|
||||
) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Customers can only access their own data
|
||||
const customerId = parseInt(req.params.customerId || req.params.id);
|
||||
if (req.user.customerId && req.user.customerId === customerId) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: 'Kein Zugriff auf diese Kundendaten',
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Router } from 'express';
|
||||
import * as customerController from '../controllers/customer.controller.js';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.put('/:id', authenticate, requirePermission('customers:update'), customerController.updateAddress);
|
||||
router.delete('/:id', authenticate, requirePermission('customers:delete'), customerController.deleteAddress);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Router } from 'express';
|
||||
import * as appSettingController from '../controllers/appSetting.controller.js';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Öffentliche Einstellungen (für alle authentifizierten Benutzer, inkl. Kunden)
|
||||
router.get('/public', authenticate, appSettingController.getPublicSettings);
|
||||
|
||||
// Alle Einstellungen (nur Admin)
|
||||
router.get('/', authenticate, requirePermission('settings:read'), appSettingController.getAllSettings);
|
||||
|
||||
// Einzelne Einstellung aktualisieren (nur Admin)
|
||||
router.put(
|
||||
'/:key',
|
||||
authenticate,
|
||||
requirePermission('settings:update'),
|
||||
appSettingController.updateSetting
|
||||
);
|
||||
|
||||
// Mehrere Einstellungen aktualisieren (nur Admin)
|
||||
router.put(
|
||||
'/',
|
||||
authenticate,
|
||||
requirePermission('settings:update'),
|
||||
appSettingController.updateSettings
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Router } from 'express';
|
||||
import * as authController from '../controllers/auth.controller.js';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.post('/login', authController.login);
|
||||
router.post('/customer-login', authController.customerLogin); // Kundenportal-Login
|
||||
router.get('/me', authenticate, authController.me);
|
||||
router.post('/register', authenticate, requirePermission('users:create'), authController.register);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Router } from 'express';
|
||||
import * as customerController from '../controllers/customer.controller.js';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.put('/:id', authenticate, requirePermission('customers:update'), customerController.updateBankCard);
|
||||
router.delete('/:id', authenticate, requirePermission('customers:delete'), customerController.deleteBankCard);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Router } from 'express';
|
||||
import * as cancellationPeriodController from '../controllers/cancellation-period.controller.js';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', authenticate, cancellationPeriodController.getCancellationPeriods);
|
||||
router.post('/', authenticate, requirePermission('platforms:create'), cancellationPeriodController.createCancellationPeriod);
|
||||
router.get('/:id', authenticate, cancellationPeriodController.getCancellationPeriod);
|
||||
router.put('/:id', authenticate, requirePermission('platforms:update'), cancellationPeriodController.updateCancellationPeriod);
|
||||
router.delete('/:id', authenticate, requirePermission('platforms:delete'), cancellationPeriodController.deleteCancellationPeriod);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Router } from 'express';
|
||||
import * as contractDurationController from '../controllers/contract-duration.controller.js';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', authenticate, contractDurationController.getContractDurations);
|
||||
router.post('/', authenticate, requirePermission('platforms:create'), contractDurationController.createContractDuration);
|
||||
router.get('/:id', authenticate, contractDurationController.getContractDuration);
|
||||
router.put('/:id', authenticate, requirePermission('platforms:update'), contractDurationController.updateContractDuration);
|
||||
router.delete('/:id', authenticate, requirePermission('platforms:delete'), contractDurationController.deleteContractDuration);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Router } from 'express';
|
||||
import * as contractController from '../controllers/contract.controller.js';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', authenticate, requirePermission('contracts:read'), contractController.getContracts);
|
||||
router.post('/', authenticate, requirePermission('contracts:create'), contractController.createContract);
|
||||
|
||||
// Vertrags-Cockpit (muss VOR /:id stehen!)
|
||||
router.get('/cockpit', authenticate, requirePermission('contracts:read'), contractController.getCockpit);
|
||||
|
||||
router.get('/:id', authenticate, requirePermission('contracts:read'), contractController.getContract);
|
||||
router.put('/:id', authenticate, requirePermission('contracts:update'), contractController.updateContract);
|
||||
router.delete('/:id', authenticate, requirePermission('contracts:delete'), contractController.deleteContract);
|
||||
|
||||
// Follow-up contract
|
||||
router.post('/:id/follow-up', authenticate, requirePermission('contracts:create'), contractController.createFollowUp);
|
||||
|
||||
// Get decrypted password
|
||||
router.get('/:id/password', authenticate, requirePermission('contracts:read'), contractController.getContractPassword);
|
||||
|
||||
// Get decrypted SimCard PIN/PUK
|
||||
router.get('/simcard/:simCardId/credentials', authenticate, requirePermission('contracts:read'), contractController.getSimCardCredentials);
|
||||
|
||||
// Get decrypted Internet password
|
||||
router.get('/:id/internet-credentials', authenticate, requirePermission('contracts:read'), contractController.getInternetCredentials);
|
||||
|
||||
// Get decrypted SIP password
|
||||
router.get('/phonenumber/:phoneNumberId/sip-credentials', authenticate, requirePermission('contracts:read'), contractController.getSipCredentials);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Router } from 'express';
|
||||
import * as contractCategoryController from '../controllers/contractCategory.controller.js';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', authenticate, contractCategoryController.getContractCategories);
|
||||
router.post('/', authenticate, requirePermission('platforms:create'), contractCategoryController.createContractCategory);
|
||||
router.get('/:id', authenticate, contractCategoryController.getContractCategory);
|
||||
router.put('/:id', authenticate, requirePermission('platforms:update'), contractCategoryController.updateContractCategory);
|
||||
router.delete('/:id', authenticate, requirePermission('platforms:delete'), contractCategoryController.deleteContractCategory);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,133 @@
|
||||
import { Router } from 'express';
|
||||
import * as contractTaskController from '../controllers/contractTask.controller.js';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// ==================== ALL TASKS (vertragsübergreifend) ====================
|
||||
|
||||
// Alle Aufgaben über alle Verträge (für Dashboard & Task-Liste)
|
||||
router.get(
|
||||
'/tasks',
|
||||
authenticate,
|
||||
requirePermission('contracts:read'),
|
||||
contractTaskController.getAllTasks
|
||||
);
|
||||
|
||||
// Task-Statistik (offene Aufgaben)
|
||||
router.get(
|
||||
'/tasks/stats',
|
||||
authenticate,
|
||||
requirePermission('contracts:read'),
|
||||
contractTaskController.getTaskStats
|
||||
);
|
||||
|
||||
// ==================== TASKS BY CONTRACT ====================
|
||||
|
||||
// Alle Aufgaben eines Vertrags abrufen (auch für Kundenportal, aber nur sichtbare)
|
||||
router.get(
|
||||
'/contracts/:contractId/tasks',
|
||||
authenticate,
|
||||
requirePermission('contracts:read'),
|
||||
contractTaskController.getTasks
|
||||
);
|
||||
|
||||
// Neue Aufgabe erstellen (nur für Mitarbeiter mit contracts:update)
|
||||
router.post(
|
||||
'/contracts/:contractId/tasks',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
contractTaskController.createTask
|
||||
);
|
||||
|
||||
// Support-Anfrage erstellen (für Kundenportal-Benutzer, nur contracts:read erforderlich)
|
||||
router.post(
|
||||
'/contracts/:contractId/support-ticket',
|
||||
authenticate,
|
||||
requirePermission('contracts:read'),
|
||||
contractTaskController.createSupportTicket
|
||||
);
|
||||
|
||||
// Aufgabe aktualisieren
|
||||
router.put(
|
||||
'/tasks/:taskId',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
contractTaskController.updateTask
|
||||
);
|
||||
|
||||
// Aufgabe als erledigt markieren
|
||||
router.post(
|
||||
'/tasks/:taskId/complete',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
contractTaskController.completeTask
|
||||
);
|
||||
|
||||
// Aufgabe wieder öffnen
|
||||
router.post(
|
||||
'/tasks/:taskId/reopen',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
contractTaskController.reopenTask
|
||||
);
|
||||
|
||||
// Aufgabe löschen
|
||||
router.delete(
|
||||
'/tasks/:taskId',
|
||||
authenticate,
|
||||
requirePermission('contracts:delete'),
|
||||
contractTaskController.deleteTask
|
||||
);
|
||||
|
||||
// ==================== SUBTASKS ====================
|
||||
|
||||
// Neue Unteraufgabe erstellen (Mitarbeiter)
|
||||
router.post(
|
||||
'/tasks/:taskId/subtasks',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
contractTaskController.createSubtask
|
||||
);
|
||||
|
||||
// Antwort auf eigenes Ticket erstellen (Kundenportal)
|
||||
router.post(
|
||||
'/tasks/:taskId/reply',
|
||||
authenticate,
|
||||
requirePermission('contracts:read'),
|
||||
contractTaskController.createCustomerReply
|
||||
);
|
||||
|
||||
// Unteraufgabe als erledigt markieren
|
||||
router.post(
|
||||
'/subtasks/:subtaskId/complete',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
contractTaskController.completeSubtask
|
||||
);
|
||||
|
||||
// Unteraufgabe wieder öffnen
|
||||
router.post(
|
||||
'/subtasks/:subtaskId/reopen',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
contractTaskController.reopenSubtask
|
||||
);
|
||||
|
||||
// Unteraufgabe aktualisieren
|
||||
router.put(
|
||||
'/subtasks/:subtaskId',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
contractTaskController.updateSubtask
|
||||
);
|
||||
|
||||
// Unteraufgabe löschen
|
||||
router.delete(
|
||||
'/subtasks/:subtaskId',
|
||||
authenticate,
|
||||
requirePermission('contracts:delete'),
|
||||
contractTaskController.deleteSubtask
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Router } from 'express';
|
||||
import * as customerController from '../controllers/customer.controller.js';
|
||||
import * as stressfreiEmailController from '../controllers/stressfreiEmail.controller.js';
|
||||
import { authenticate, requirePermission, requireCustomerAccess } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Customers
|
||||
router.get('/', authenticate, requirePermission('customers:read'), customerController.getCustomers);
|
||||
router.post('/', authenticate, requirePermission('customers:create'), customerController.createCustomer);
|
||||
router.get('/:id', authenticate, requireCustomerAccess, customerController.getCustomer);
|
||||
router.put('/:id', authenticate, requirePermission('customers:update'), customerController.updateCustomer);
|
||||
router.delete('/:id', authenticate, requirePermission('customers:delete'), customerController.deleteCustomer);
|
||||
|
||||
// Addresses
|
||||
router.get('/:customerId/addresses', authenticate, requireCustomerAccess, customerController.getAddresses);
|
||||
router.post('/:customerId/addresses', authenticate, requirePermission('customers:update'), customerController.createAddress);
|
||||
|
||||
// Bank Cards
|
||||
router.get('/:customerId/bank-cards', authenticate, requireCustomerAccess, customerController.getBankCards);
|
||||
router.post('/:customerId/bank-cards', authenticate, requirePermission('customers:update'), customerController.createBankCard);
|
||||
|
||||
// Identity Documents
|
||||
router.get('/:customerId/documents', authenticate, requireCustomerAccess, customerController.getDocuments);
|
||||
router.post('/:customerId/documents', authenticate, requirePermission('customers:update'), customerController.createDocument);
|
||||
|
||||
// Meters
|
||||
router.get('/:customerId/meters', authenticate, requireCustomerAccess, customerController.getMeters);
|
||||
router.post('/:customerId/meters', authenticate, requirePermission('customers:update'), customerController.createMeter);
|
||||
|
||||
// Stressfrei-Wechseln E-Mail-Adressen
|
||||
router.get('/:customerId/stressfrei-emails', authenticate, requireCustomerAccess, stressfreiEmailController.getEmailsByCustomer);
|
||||
router.post('/:customerId/stressfrei-emails', authenticate, requirePermission('customers:update'), stressfreiEmailController.createEmail);
|
||||
|
||||
// Portal Settings
|
||||
router.get('/:customerId/portal', authenticate, requirePermission('customers:update'), customerController.getPortalSettings);
|
||||
router.put('/:customerId/portal', authenticate, requirePermission('customers:update'), customerController.updatePortalSettings);
|
||||
router.post('/:customerId/portal/password', authenticate, requirePermission('customers:update'), customerController.setPortalPassword);
|
||||
router.get('/:customerId/portal/password', authenticate, requirePermission('customers:update'), customerController.getPortalPassword);
|
||||
|
||||
// Representatives (Vertreter)
|
||||
router.get('/:customerId/representatives', authenticate, requirePermission('customers:read'), customerController.getRepresentatives);
|
||||
router.post('/:customerId/representatives', authenticate, requirePermission('customers:update'), customerController.addRepresentative);
|
||||
router.delete('/:customerId/representatives/:representativeId', authenticate, requirePermission('customers:update'), customerController.removeRepresentative);
|
||||
router.get('/:customerId/representatives/search', authenticate, requirePermission('customers:read'), customerController.searchForRepresentative);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,512 @@
|
||||
import { Router, Response } from 'express';
|
||||
import { PrismaClient, Prisma } from '@prisma/client';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
import { AuthRequest } from '../types/index.js';
|
||||
|
||||
const router = Router();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// Setup-Endpunkt: Erstellt die developer:access Permission und fügt sie der Admin-Rolle hinzu
|
||||
// Dieser Endpunkt erfordert keine Authentifizierung, da er nur einmalig zum Setup verwendet wird
|
||||
router.post('/setup', async (req, res: Response) => {
|
||||
try {
|
||||
// Create or get the developer:access permission
|
||||
const developerPerm = await prisma.permission.upsert({
|
||||
where: { resource_action: { resource: 'developer', action: 'access' } },
|
||||
update: {},
|
||||
create: { resource: 'developer', action: 'access' },
|
||||
});
|
||||
|
||||
// Get the Admin role
|
||||
const adminRole = await prisma.role.findUnique({
|
||||
where: { name: 'Admin' },
|
||||
include: { permissions: true },
|
||||
});
|
||||
|
||||
if (!adminRole) {
|
||||
res.status(404).json({ success: false, error: 'Admin-Rolle nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Admin already has this permission
|
||||
const hasPermission = adminRole.permissions.some(
|
||||
(rp) => rp.permissionId === developerPerm.id
|
||||
);
|
||||
|
||||
if (!hasPermission) {
|
||||
await prisma.rolePermission.create({
|
||||
data: {
|
||||
roleId: adminRole.id,
|
||||
permissionId: developerPerm.id,
|
||||
},
|
||||
});
|
||||
res.json({ success: true, message: 'developer:access Permission wurde zur Admin-Rolle hinzugefügt. Bitte neu einloggen!' });
|
||||
} else {
|
||||
res.json({ success: true, message: 'Admin-Rolle hat bereits die developer:access Permission' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Setup error:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Setup' });
|
||||
}
|
||||
});
|
||||
|
||||
// Tabellen-Metadaten mit Beziehungen
|
||||
const tableMetadata: Record<string, {
|
||||
model: string;
|
||||
primaryKey: string;
|
||||
readonlyFields: string[];
|
||||
requiredFields: string[];
|
||||
relations: { field: string; targetTable: string; type: 'one' | 'many' }[];
|
||||
foreignKeys: { field: string; targetTable: string }[];
|
||||
}> = {
|
||||
User: {
|
||||
model: 'user',
|
||||
primaryKey: 'id',
|
||||
readonlyFields: ['id', 'createdAt', 'updatedAt', 'password'],
|
||||
requiredFields: ['email', 'firstName', 'lastName'],
|
||||
relations: [
|
||||
{ field: 'customer', targetTable: 'Customer', type: 'one' },
|
||||
{ field: 'roles', targetTable: 'UserRole', type: 'many' },
|
||||
],
|
||||
foreignKeys: [{ field: 'customerId', targetTable: 'Customer' }],
|
||||
},
|
||||
Role: {
|
||||
model: 'role',
|
||||
primaryKey: 'id',
|
||||
readonlyFields: ['id', 'createdAt', 'updatedAt'],
|
||||
requiredFields: ['name'],
|
||||
relations: [
|
||||
{ field: 'permissions', targetTable: 'RolePermission', type: 'many' },
|
||||
{ field: 'users', targetTable: 'UserRole', type: 'many' },
|
||||
],
|
||||
foreignKeys: [],
|
||||
},
|
||||
Permission: {
|
||||
model: 'permission',
|
||||
primaryKey: 'id',
|
||||
readonlyFields: ['id'],
|
||||
requiredFields: ['resource', 'action'],
|
||||
relations: [{ field: 'roles', targetTable: 'RolePermission', type: 'many' }],
|
||||
foreignKeys: [],
|
||||
},
|
||||
RolePermission: {
|
||||
model: 'rolePermission',
|
||||
primaryKey: 'roleId,permissionId',
|
||||
readonlyFields: [],
|
||||
requiredFields: ['roleId', 'permissionId'],
|
||||
relations: [],
|
||||
foreignKeys: [
|
||||
{ field: 'roleId', targetTable: 'Role' },
|
||||
{ field: 'permissionId', targetTable: 'Permission' },
|
||||
],
|
||||
},
|
||||
UserRole: {
|
||||
model: 'userRole',
|
||||
primaryKey: 'userId,roleId',
|
||||
readonlyFields: [],
|
||||
requiredFields: ['userId', 'roleId'],
|
||||
relations: [],
|
||||
foreignKeys: [
|
||||
{ field: 'userId', targetTable: 'User' },
|
||||
{ field: 'roleId', targetTable: 'Role' },
|
||||
],
|
||||
},
|
||||
Customer: {
|
||||
model: 'customer',
|
||||
primaryKey: 'id',
|
||||
readonlyFields: ['id', 'createdAt', 'updatedAt', 'customerNumber'],
|
||||
requiredFields: ['firstName', 'lastName'],
|
||||
relations: [
|
||||
{ field: 'user', targetTable: 'User', type: 'one' },
|
||||
{ field: 'addresses', targetTable: 'Address', type: 'many' },
|
||||
{ field: 'bankCards', targetTable: 'BankCard', type: 'many' },
|
||||
{ field: 'identityDocuments', targetTable: 'IdentityDocument', type: 'many' },
|
||||
{ field: 'meters', targetTable: 'Meter', type: 'many' },
|
||||
{ field: 'contracts', targetTable: 'Contract', type: 'many' },
|
||||
],
|
||||
foreignKeys: [],
|
||||
},
|
||||
Address: {
|
||||
model: 'address',
|
||||
primaryKey: 'id',
|
||||
readonlyFields: ['id', 'createdAt', 'updatedAt'],
|
||||
requiredFields: ['customerId', 'street', 'houseNumber', 'postalCode', 'city'],
|
||||
relations: [
|
||||
{ field: 'customer', targetTable: 'Customer', type: 'one' },
|
||||
{ field: 'contracts', targetTable: 'Contract', type: 'many' },
|
||||
],
|
||||
foreignKeys: [{ field: 'customerId', targetTable: 'Customer' }],
|
||||
},
|
||||
BankCard: {
|
||||
model: 'bankCard',
|
||||
primaryKey: 'id',
|
||||
readonlyFields: ['id', 'createdAt', 'updatedAt'],
|
||||
requiredFields: ['customerId', 'accountHolder', 'iban'],
|
||||
relations: [
|
||||
{ field: 'customer', targetTable: 'Customer', type: 'one' },
|
||||
{ field: 'contracts', targetTable: 'Contract', type: 'many' },
|
||||
],
|
||||
foreignKeys: [{ field: 'customerId', targetTable: 'Customer' }],
|
||||
},
|
||||
IdentityDocument: {
|
||||
model: 'identityDocument',
|
||||
primaryKey: 'id',
|
||||
readonlyFields: ['id', 'createdAt', 'updatedAt'],
|
||||
requiredFields: ['customerId', 'documentNumber'],
|
||||
relations: [
|
||||
{ field: 'customer', targetTable: 'Customer', type: 'one' },
|
||||
{ field: 'contracts', targetTable: 'Contract', type: 'many' },
|
||||
],
|
||||
foreignKeys: [{ field: 'customerId', targetTable: 'Customer' }],
|
||||
},
|
||||
Meter: {
|
||||
model: 'meter',
|
||||
primaryKey: 'id',
|
||||
readonlyFields: ['id', 'createdAt', 'updatedAt'],
|
||||
requiredFields: ['customerId', 'meterNumber', 'type'],
|
||||
relations: [
|
||||
{ field: 'customer', targetTable: 'Customer', type: 'one' },
|
||||
{ field: 'readings', targetTable: 'MeterReading', type: 'many' },
|
||||
{ field: 'energyDetails', targetTable: 'EnergyContractDetails', type: 'many' },
|
||||
],
|
||||
foreignKeys: [{ field: 'customerId', targetTable: 'Customer' }],
|
||||
},
|
||||
MeterReading: {
|
||||
model: 'meterReading',
|
||||
primaryKey: 'id',
|
||||
readonlyFields: ['id', 'createdAt'],
|
||||
requiredFields: ['meterId', 'readingDate', 'value'],
|
||||
relations: [{ field: 'meter', targetTable: 'Meter', type: 'one' }],
|
||||
foreignKeys: [{ field: 'meterId', targetTable: 'Meter' }],
|
||||
},
|
||||
SalesPlatform: {
|
||||
model: 'salesPlatform',
|
||||
primaryKey: 'id',
|
||||
readonlyFields: ['id', 'createdAt', 'updatedAt'],
|
||||
requiredFields: ['name'],
|
||||
relations: [{ field: 'contracts', targetTable: 'Contract', type: 'many' }],
|
||||
foreignKeys: [],
|
||||
},
|
||||
Contract: {
|
||||
model: 'contract',
|
||||
primaryKey: 'id',
|
||||
readonlyFields: ['id', 'createdAt', 'updatedAt', 'contractNumber'],
|
||||
requiredFields: ['customerId', 'type'],
|
||||
relations: [
|
||||
{ field: 'customer', targetTable: 'Customer', type: 'one' },
|
||||
{ field: 'address', targetTable: 'Address', type: 'one' },
|
||||
{ field: 'bankCard', targetTable: 'BankCard', type: 'one' },
|
||||
{ field: 'identityDocument', targetTable: 'IdentityDocument', type: 'one' },
|
||||
{ field: 'salesPlatform', targetTable: 'SalesPlatform', type: 'one' },
|
||||
{ field: 'previousContract', targetTable: 'Contract', type: 'one' },
|
||||
{ field: 'followUpContract', targetTable: 'Contract', type: 'one' },
|
||||
{ field: 'energyDetails', targetTable: 'EnergyContractDetails', type: 'one' },
|
||||
{ field: 'internetDetails', targetTable: 'InternetContractDetails', type: 'one' },
|
||||
{ field: 'mobileDetails', targetTable: 'MobileContractDetails', type: 'one' },
|
||||
{ field: 'tvDetails', targetTable: 'TvContractDetails', type: 'one' },
|
||||
{ field: 'carInsuranceDetails', targetTable: 'CarInsuranceDetails', type: 'one' },
|
||||
],
|
||||
foreignKeys: [
|
||||
{ field: 'customerId', targetTable: 'Customer' },
|
||||
{ field: 'addressId', targetTable: 'Address' },
|
||||
{ field: 'bankCardId', targetTable: 'BankCard' },
|
||||
{ field: 'identityDocumentId', targetTable: 'IdentityDocument' },
|
||||
{ field: 'salesPlatformId', targetTable: 'SalesPlatform' },
|
||||
{ field: 'previousContractId', targetTable: 'Contract' },
|
||||
],
|
||||
},
|
||||
EnergyContractDetails: {
|
||||
model: 'energyContractDetails',
|
||||
primaryKey: 'id',
|
||||
readonlyFields: ['id'],
|
||||
requiredFields: ['contractId'],
|
||||
relations: [
|
||||
{ field: 'contract', targetTable: 'Contract', type: 'one' },
|
||||
{ field: 'meter', targetTable: 'Meter', type: 'one' },
|
||||
],
|
||||
foreignKeys: [
|
||||
{ field: 'contractId', targetTable: 'Contract' },
|
||||
{ field: 'meterId', targetTable: 'Meter' },
|
||||
],
|
||||
},
|
||||
InternetContractDetails: {
|
||||
model: 'internetContractDetails',
|
||||
primaryKey: 'id',
|
||||
readonlyFields: ['id'],
|
||||
requiredFields: ['contractId'],
|
||||
relations: [
|
||||
{ field: 'contract', targetTable: 'Contract', type: 'one' },
|
||||
{ field: 'phoneNumbers', targetTable: 'PhoneNumber', type: 'many' },
|
||||
],
|
||||
foreignKeys: [{ field: 'contractId', targetTable: 'Contract' }],
|
||||
},
|
||||
PhoneNumber: {
|
||||
model: 'phoneNumber',
|
||||
primaryKey: 'id',
|
||||
readonlyFields: ['id'],
|
||||
requiredFields: ['internetContractDetailsId', 'phoneNumber'],
|
||||
relations: [{ field: 'internetDetails', targetTable: 'InternetContractDetails', type: 'one' }],
|
||||
foreignKeys: [{ field: 'internetContractDetailsId', targetTable: 'InternetContractDetails' }],
|
||||
},
|
||||
MobileContractDetails: {
|
||||
model: 'mobileContractDetails',
|
||||
primaryKey: 'id',
|
||||
readonlyFields: ['id'],
|
||||
requiredFields: ['contractId'],
|
||||
relations: [{ field: 'contract', targetTable: 'Contract', type: 'one' }],
|
||||
foreignKeys: [{ field: 'contractId', targetTable: 'Contract' }],
|
||||
},
|
||||
TvContractDetails: {
|
||||
model: 'tvContractDetails',
|
||||
primaryKey: 'id',
|
||||
readonlyFields: ['id'],
|
||||
requiredFields: ['contractId'],
|
||||
relations: [{ field: 'contract', targetTable: 'Contract', type: 'one' }],
|
||||
foreignKeys: [{ field: 'contractId', targetTable: 'Contract' }],
|
||||
},
|
||||
CarInsuranceDetails: {
|
||||
model: 'carInsuranceDetails',
|
||||
primaryKey: 'id',
|
||||
readonlyFields: ['id'],
|
||||
requiredFields: ['contractId'],
|
||||
relations: [{ field: 'contract', targetTable: 'Contract', type: 'one' }],
|
||||
foreignKeys: [{ field: 'contractId', targetTable: 'Contract' }],
|
||||
},
|
||||
};
|
||||
|
||||
// Schema-Informationen abrufen
|
||||
router.get(
|
||||
'/schema',
|
||||
authenticate,
|
||||
requirePermission('developer:access'),
|
||||
async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const tables = Object.entries(tableMetadata).map(([name, meta]) => ({
|
||||
name,
|
||||
...meta,
|
||||
}));
|
||||
|
||||
res.json({ success: true, data: tables });
|
||||
} catch (error) {
|
||||
console.error('Schema error:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden des Schemas' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Tabellen-Daten abrufen
|
||||
router.get(
|
||||
'/table/:tableName',
|
||||
authenticate,
|
||||
requirePermission('developer:access'),
|
||||
async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { tableName } = req.params;
|
||||
const { page = '1', limit = '50' } = req.query;
|
||||
|
||||
const meta = tableMetadata[tableName];
|
||||
if (!meta) {
|
||||
res.status(404).json({ success: false, error: 'Tabelle nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
const skip = (parseInt(page as string) - 1) * parseInt(limit as string);
|
||||
const take = parseInt(limit as string);
|
||||
|
||||
const model = (prisma as any)[meta.model];
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
model.findMany({
|
||||
skip,
|
||||
take,
|
||||
orderBy: meta.primaryKey.includes(',') ? undefined : { [meta.primaryKey.split(',')[0]]: 'desc' },
|
||||
}),
|
||||
model.count(),
|
||||
]);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data,
|
||||
meta: {
|
||||
...meta,
|
||||
tableName,
|
||||
},
|
||||
pagination: {
|
||||
page: parseInt(page as string),
|
||||
limit: parseInt(limit as string),
|
||||
total,
|
||||
totalPages: Math.ceil(total / parseInt(limit as string)),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Table data error:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden der Daten' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Einzelne Zeile aktualisieren
|
||||
router.put(
|
||||
'/table/:tableName/:id',
|
||||
authenticate,
|
||||
requirePermission('developer:access'),
|
||||
async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { tableName, id } = req.params;
|
||||
const updates = req.body;
|
||||
|
||||
const meta = tableMetadata[tableName];
|
||||
if (!meta) {
|
||||
res.status(404).json({ success: false, error: 'Tabelle nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Readonly-Felder aus Updates entfernen
|
||||
const filteredUpdates: Record<string, any> = {};
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
if (!meta.readonlyFields.includes(key)) {
|
||||
filteredUpdates[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfen ob required-Felder nicht auf null/leer gesetzt werden
|
||||
for (const field of meta.requiredFields) {
|
||||
if (field in filteredUpdates && (filteredUpdates[field] === null || filteredUpdates[field] === '')) {
|
||||
res.status(400).json({ success: false, error: `Feld '${field}' ist erforderlich` });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const model = (prisma as any)[meta.model];
|
||||
|
||||
// Composite Primary Key Handling
|
||||
let where: any;
|
||||
if (meta.primaryKey.includes(',')) {
|
||||
const keys = meta.primaryKey.split(',');
|
||||
const idParts = id.split('-');
|
||||
where = {};
|
||||
keys.forEach((key, idx) => {
|
||||
where[key] = parseInt(idParts[idx]);
|
||||
});
|
||||
} else {
|
||||
where = { [meta.primaryKey]: parseInt(id) };
|
||||
}
|
||||
|
||||
const updated = await model.update({
|
||||
where,
|
||||
data: filteredUpdates,
|
||||
});
|
||||
|
||||
res.json({ success: true, data: updated });
|
||||
} catch (error: any) {
|
||||
console.error('Update error:', error);
|
||||
if (error.code === 'P2003') {
|
||||
res.status(400).json({ success: false, error: 'Fremdschlüssel-Verletzung: Referenzierter Datensatz existiert nicht' });
|
||||
} else if (error.code === 'P2002') {
|
||||
res.status(400).json({ success: false, error: 'Unique-Constraint-Verletzung: Wert existiert bereits' });
|
||||
} else {
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Aktualisieren' });
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Zeile löschen (nur wenn keine abhängigen Daten)
|
||||
router.delete(
|
||||
'/table/:tableName/:id',
|
||||
authenticate,
|
||||
requirePermission('developer:access'),
|
||||
async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { tableName, id } = req.params;
|
||||
|
||||
const meta = tableMetadata[tableName];
|
||||
if (!meta) {
|
||||
res.status(404).json({ success: false, error: 'Tabelle nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
const model = (prisma as any)[meta.model];
|
||||
|
||||
// Composite Primary Key Handling
|
||||
let where: any;
|
||||
if (meta.primaryKey.includes(',')) {
|
||||
const keys = meta.primaryKey.split(',');
|
||||
const idParts = id.split('-');
|
||||
where = {};
|
||||
keys.forEach((key, idx) => {
|
||||
where[key] = parseInt(idParts[idx]);
|
||||
});
|
||||
} else {
|
||||
where = { [meta.primaryKey]: parseInt(id) };
|
||||
}
|
||||
|
||||
// Prüfen ob abhängige Daten existieren (nur "many"-Relations)
|
||||
const record = await model.findUnique({
|
||||
where,
|
||||
include: meta.relations
|
||||
.filter((r) => r.type === 'many')
|
||||
.reduce((acc, r) => ({ ...acc, [r.field]: { take: 1 } }), {}),
|
||||
});
|
||||
|
||||
if (!record) {
|
||||
res.status(404).json({ success: false, error: 'Datensatz nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfen auf abhängige Daten
|
||||
for (const rel of meta.relations.filter((r) => r.type === 'many')) {
|
||||
if (record[rel.field] && record[rel.field].length > 0) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `Kann nicht gelöscht werden: Es existieren abhängige Daten in '${rel.targetTable}'`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await model.delete({ where });
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error: any) {
|
||||
console.error('Delete error:', error);
|
||||
if (error.code === 'P2003') {
|
||||
res.status(400).json({ success: false, error: 'Kann nicht gelöscht werden: Fremdschlüssel-Abhängigkeit' });
|
||||
} else {
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Löschen' });
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Referenzierte Daten für Dropdowns abrufen
|
||||
router.get(
|
||||
'/reference/:tableName',
|
||||
authenticate,
|
||||
requirePermission('developer:access'),
|
||||
async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { tableName } = req.params;
|
||||
const { search = '', limit = '50' } = req.query;
|
||||
|
||||
const meta = tableMetadata[tableName];
|
||||
if (!meta) {
|
||||
res.status(404).json({ success: false, error: 'Tabelle nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
const model = (prisma as any)[meta.model];
|
||||
|
||||
const data = await model.findMany({
|
||||
take: parseInt(limit as string),
|
||||
});
|
||||
|
||||
res.json({ success: true, data });
|
||||
} catch (error) {
|
||||
console.error('Reference error:', error);
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden der Referenzdaten' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Router } from 'express';
|
||||
import * as customerController from '../controllers/customer.controller.js';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.put('/:id', authenticate, requirePermission('customers:update'), customerController.updateDocument);
|
||||
router.delete('/:id', authenticate, requirePermission('customers:delete'), customerController.deleteDocument);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,23 @@
|
||||
// ==================== EMAIL PROVIDER ROUTES ====================
|
||||
|
||||
import { Router } from 'express';
|
||||
import * as emailProviderController from '../controllers/emailProvider.controller.js';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Provider Config CRUD (Admin-only)
|
||||
router.get('/configs', authenticate, requirePermission('settings:read'), emailProviderController.getProviderConfigs);
|
||||
router.get('/configs/:id', authenticate, requirePermission('settings:read'), emailProviderController.getProviderConfig);
|
||||
router.post('/configs', authenticate, requirePermission('settings:update'), emailProviderController.createProviderConfig);
|
||||
router.put('/configs/:id', authenticate, requirePermission('settings:update'), emailProviderController.updateProviderConfig);
|
||||
router.delete('/configs/:id', authenticate, requirePermission('settings:update'), emailProviderController.deleteProviderConfig);
|
||||
|
||||
// Email Operations
|
||||
router.post('/test-connection', authenticate, requirePermission('settings:update'), emailProviderController.testConnection);
|
||||
router.get('/domain', authenticate, emailProviderController.getProviderDomain);
|
||||
router.get('/check/:localPart', authenticate, requirePermission('customers:read'), emailProviderController.checkEmailExists);
|
||||
router.post('/provision', authenticate, requirePermission('customers:update'), emailProviderController.provisionEmail);
|
||||
router.delete('/deprovision/:localPart', authenticate, requirePermission('customers:update'), emailProviderController.deprovisionEmail);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Router } from 'express';
|
||||
import * as customerController from '../controllers/customer.controller.js';
|
||||
import { authenticate, requirePermission, requireCustomerAccess } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.put('/:id', authenticate, requirePermission('customers:update'), customerController.updateMeter);
|
||||
router.delete('/:id', authenticate, requirePermission('customers:delete'), customerController.deleteMeter);
|
||||
|
||||
// Meter readings
|
||||
router.get('/:meterId/readings', authenticate, customerController.getMeterReadings);
|
||||
router.post('/:meterId/readings', authenticate, requirePermission('customers:update'), customerController.addMeterReading);
|
||||
router.put('/:meterId/readings/:readingId', authenticate, requirePermission('customers:update'), customerController.updateMeterReading);
|
||||
router.delete('/:meterId/readings/:readingId', authenticate, requirePermission('customers:delete'), customerController.deleteMeterReading);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Router } from 'express';
|
||||
import * as platformController from '../controllers/platform.controller.js';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', authenticate, platformController.getPlatforms);
|
||||
router.post('/', authenticate, requirePermission('platforms:create'), platformController.createPlatform);
|
||||
router.get('/:id', authenticate, platformController.getPlatform);
|
||||
router.put('/:id', authenticate, requirePermission('platforms:update'), platformController.updatePlatform);
|
||||
router.delete('/:id', authenticate, requirePermission('platforms:delete'), platformController.deletePlatform);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Router } from 'express';
|
||||
import * as providerController from '../controllers/provider.controller.js';
|
||||
import * as tariffController from '../controllers/tariff.controller.js';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Provider routes
|
||||
router.get('/', authenticate, providerController.getProviders);
|
||||
router.post('/', authenticate, requirePermission('providers:create'), providerController.createProvider);
|
||||
router.get('/:id', authenticate, providerController.getProvider);
|
||||
router.put('/:id', authenticate, requirePermission('providers:update'), providerController.updateProvider);
|
||||
router.delete('/:id', authenticate, requirePermission('providers:delete'), providerController.deleteProvider);
|
||||
|
||||
// Nested tariff routes
|
||||
router.get('/:providerId/tariffs', authenticate, tariffController.getTariffs);
|
||||
router.post('/:providerId/tariffs', authenticate, requirePermission('providers:create'), tariffController.createTariff);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Router } from 'express';
|
||||
import * as stressfreiEmailController from '../controllers/stressfreiEmail.controller.js';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Einzelne Stressfrei-Email verwalten
|
||||
router.get('/:id', authenticate, requirePermission('customers:read'), stressfreiEmailController.getEmail);
|
||||
router.put('/:id', authenticate, requirePermission('customers:update'), stressfreiEmailController.updateEmail);
|
||||
router.delete('/:id', authenticate, requirePermission('customers:delete'), stressfreiEmailController.deleteEmail);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Router } from 'express';
|
||||
import * as tariffController from '../controllers/tariff.controller.js';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Standalone tariff routes (for update/delete by tariff id)
|
||||
router.get('/:id', authenticate, tariffController.getTariff);
|
||||
router.put('/:id', authenticate, requirePermission('providers:update'), tariffController.updateTariff);
|
||||
router.delete('/:id', authenticate, requirePermission('providers:delete'), tariffController.deleteTariff);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,661 @@
|
||||
import { Router, Response } from 'express';
|
||||
import multer from 'multer';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
import { AuthRequest } from '../types/index.js';
|
||||
|
||||
const router = Router();
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// Uploads-Verzeichnis erstellen falls nicht vorhanden
|
||||
const uploadsDir = path.join(process.cwd(), 'uploads');
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Multer-Konfiguration
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
const subDir = (req as any).uploadSubDir || 'misc';
|
||||
const targetDir = path.join(uploadsDir, subDir);
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
}
|
||||
cb(null, targetDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||
const ext = path.extname(file.originalname);
|
||||
cb(null, `${uniqueSuffix}${ext}`);
|
||||
},
|
||||
});
|
||||
|
||||
const fileFilter = (
|
||||
req: Express.Request,
|
||||
file: Express.Multer.File,
|
||||
cb: multer.FileFilterCallback
|
||||
) => {
|
||||
// Nur PDFs und Bilder erlauben
|
||||
const allowedTypes = ['application/pdf', 'image/jpeg', 'image/png', 'image/jpg'];
|
||||
if (allowedTypes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Nur PDF, JPG und PNG Dateien sind erlaubt'));
|
||||
}
|
||||
};
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
fileFilter,
|
||||
limits: {
|
||||
fileSize: 10 * 1024 * 1024, // 10MB max
|
||||
},
|
||||
});
|
||||
|
||||
// Middleware um Subdirectory zu setzen
|
||||
function setUploadDir(subDir: string) {
|
||||
return (req: AuthRequest, res: Response, next: Function) => {
|
||||
(req as any).uploadSubDir = subDir;
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
// Upload für Bankkarten-Dokumente
|
||||
router.post(
|
||||
'/bank-cards/:id',
|
||||
authenticate,
|
||||
requirePermission('customers:update'),
|
||||
setUploadDir('bank-cards'),
|
||||
upload.single('document'),
|
||||
async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' });
|
||||
return;
|
||||
}
|
||||
|
||||
const bankCardId = parseInt(req.params.id);
|
||||
const relativePath = `/uploads/bank-cards/${req.file.filename}`;
|
||||
|
||||
// Bankkarte in der DB aktualisieren
|
||||
await prisma.bankCard.update({
|
||||
where: { id: bankCardId },
|
||||
data: { documentPath: relativePath },
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
path: relativePath,
|
||||
filename: req.file.filename,
|
||||
originalName: req.file.originalname,
|
||||
size: req.file.size,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Upload für Ausweis-Dokumente
|
||||
router.post(
|
||||
'/documents/:id',
|
||||
authenticate,
|
||||
requirePermission('customers:update'),
|
||||
setUploadDir('documents'),
|
||||
upload.single('document'),
|
||||
async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' });
|
||||
return;
|
||||
}
|
||||
|
||||
const documentId = parseInt(req.params.id);
|
||||
const relativePath = `/uploads/documents/${req.file.filename}`;
|
||||
|
||||
// Ausweis in der DB aktualisieren
|
||||
await prisma.identityDocument.update({
|
||||
where: { id: documentId },
|
||||
data: { documentPath: relativePath },
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
path: relativePath,
|
||||
filename: req.file.filename,
|
||||
originalName: req.file.originalname,
|
||||
size: req.file.size,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Löschen von Bankkarten-Dokumenten
|
||||
router.delete(
|
||||
'/bank-cards/:id',
|
||||
authenticate,
|
||||
requirePermission('customers:update'),
|
||||
async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const bankCardId = parseInt(req.params.id);
|
||||
|
||||
// Bankkarte aus DB holen um Dateipfad zu bekommen
|
||||
const bankCard = await prisma.bankCard.findUnique({
|
||||
where: { id: bankCardId },
|
||||
});
|
||||
|
||||
if (!bankCard) {
|
||||
res.status(404).json({ success: false, error: 'Bankkarte nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!bankCard.documentPath) {
|
||||
res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Datei von Festplatte löschen
|
||||
const filePath = path.join(process.cwd(), bankCard.documentPath);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
// documentPath in DB auf null setzen
|
||||
await prisma.bankCard.update({
|
||||
where: { id: bankCardId },
|
||||
data: { documentPath: null },
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error);
|
||||
res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Löschen von Ausweis-Dokumenten
|
||||
router.delete(
|
||||
'/documents/:id',
|
||||
authenticate,
|
||||
requirePermission('customers:update'),
|
||||
async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const documentId = parseInt(req.params.id);
|
||||
|
||||
// Ausweis aus DB holen um Dateipfad zu bekommen
|
||||
const document = await prisma.identityDocument.findUnique({
|
||||
where: { id: documentId },
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
res.status(404).json({ success: false, error: 'Ausweis nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!document.documentPath) {
|
||||
res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Datei von Festplatte löschen
|
||||
const filePath = path.join(process.cwd(), document.documentPath);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
// documentPath in DB auf null setzen
|
||||
await prisma.identityDocument.update({
|
||||
where: { id: documentId },
|
||||
data: { documentPath: null },
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error);
|
||||
res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== FIRMEN-DOKUMENTE ====================
|
||||
|
||||
// Upload für Gewerbeanmeldung
|
||||
router.post(
|
||||
'/customers/:id/business-registration',
|
||||
authenticate,
|
||||
requirePermission('customers:update'),
|
||||
setUploadDir('business-registrations'),
|
||||
upload.single('document'),
|
||||
async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' });
|
||||
return;
|
||||
}
|
||||
|
||||
const customerId = parseInt(req.params.id);
|
||||
const relativePath = `/uploads/business-registrations/${req.file.filename}`;
|
||||
|
||||
// Alte Datei löschen falls vorhanden
|
||||
const customer = await prisma.customer.findUnique({ where: { id: customerId } });
|
||||
if (customer?.businessRegistrationPath) {
|
||||
const oldPath = path.join(process.cwd(), customer.businessRegistrationPath);
|
||||
if (fs.existsSync(oldPath)) {
|
||||
fs.unlinkSync(oldPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Kunde in der DB aktualisieren
|
||||
await prisma.customer.update({
|
||||
where: { id: customerId },
|
||||
data: { businessRegistrationPath: relativePath },
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
path: relativePath,
|
||||
filename: req.file.filename,
|
||||
originalName: req.file.originalname,
|
||||
size: req.file.size,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Upload für Handelsregisterauszug
|
||||
router.post(
|
||||
'/customers/:id/commercial-register',
|
||||
authenticate,
|
||||
requirePermission('customers:update'),
|
||||
setUploadDir('commercial-registers'),
|
||||
upload.single('document'),
|
||||
async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' });
|
||||
return;
|
||||
}
|
||||
|
||||
const customerId = parseInt(req.params.id);
|
||||
const relativePath = `/uploads/commercial-registers/${req.file.filename}`;
|
||||
|
||||
// Alte Datei löschen falls vorhanden
|
||||
const customer = await prisma.customer.findUnique({ where: { id: customerId } });
|
||||
if (customer?.commercialRegisterPath) {
|
||||
const oldPath = path.join(process.cwd(), customer.commercialRegisterPath);
|
||||
if (fs.existsSync(oldPath)) {
|
||||
fs.unlinkSync(oldPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Kunde in der DB aktualisieren
|
||||
await prisma.customer.update({
|
||||
where: { id: customerId },
|
||||
data: { commercialRegisterPath: relativePath },
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
path: relativePath,
|
||||
filename: req.file.filename,
|
||||
originalName: req.file.originalname,
|
||||
size: req.file.size,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Löschen der Gewerbeanmeldung
|
||||
router.delete(
|
||||
'/customers/:id/business-registration',
|
||||
authenticate,
|
||||
requirePermission('customers:update'),
|
||||
async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const customerId = parseInt(req.params.id);
|
||||
|
||||
const customer = await prisma.customer.findUnique({ where: { id: customerId } });
|
||||
if (!customer) {
|
||||
res.status(404).json({ success: false, error: 'Kunde nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!customer.businessRegistrationPath) {
|
||||
res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Datei löschen
|
||||
const filePath = path.join(process.cwd(), customer.businessRegistrationPath);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
// Pfad in DB auf null setzen
|
||||
await prisma.customer.update({
|
||||
where: { id: customerId },
|
||||
data: { businessRegistrationPath: null },
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error);
|
||||
res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Löschen des Handelsregisterauszugs
|
||||
router.delete(
|
||||
'/customers/:id/commercial-register',
|
||||
authenticate,
|
||||
requirePermission('customers:update'),
|
||||
async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const customerId = parseInt(req.params.id);
|
||||
|
||||
const customer = await prisma.customer.findUnique({ where: { id: customerId } });
|
||||
if (!customer) {
|
||||
res.status(404).json({ success: false, error: 'Kunde nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!customer.commercialRegisterPath) {
|
||||
res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Datei löschen
|
||||
const filePath = path.join(process.cwd(), customer.commercialRegisterPath);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
// Pfad in DB auf null setzen
|
||||
await prisma.customer.update({
|
||||
where: { id: customerId },
|
||||
data: { commercialRegisterPath: null },
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error);
|
||||
res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== DATENSCHUTZERKLÄRUNG (für alle Kunden) ====================
|
||||
|
||||
// Upload für Datenschutzerklärung
|
||||
router.post(
|
||||
'/customers/:id/privacy-policy',
|
||||
authenticate,
|
||||
requirePermission('customers:update'),
|
||||
setUploadDir('privacy-policies'),
|
||||
upload.single('document'),
|
||||
async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' });
|
||||
return;
|
||||
}
|
||||
|
||||
const customerId = parseInt(req.params.id);
|
||||
const relativePath = `/uploads/privacy-policies/${req.file.filename}`;
|
||||
|
||||
// Alte Datei löschen falls vorhanden
|
||||
const customer = await prisma.customer.findUnique({ where: { id: customerId } });
|
||||
if (customer?.privacyPolicyPath) {
|
||||
const oldPath = path.join(process.cwd(), customer.privacyPolicyPath);
|
||||
if (fs.existsSync(oldPath)) {
|
||||
fs.unlinkSync(oldPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Kunde in der DB aktualisieren
|
||||
await prisma.customer.update({
|
||||
where: { id: customerId },
|
||||
data: { privacyPolicyPath: relativePath },
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
path: relativePath,
|
||||
filename: req.file.filename,
|
||||
originalName: req.file.originalname,
|
||||
size: req.file.size,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Löschen der Datenschutzerklärung
|
||||
router.delete(
|
||||
'/customers/:id/privacy-policy',
|
||||
authenticate,
|
||||
requirePermission('customers:update'),
|
||||
async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const customerId = parseInt(req.params.id);
|
||||
|
||||
const customer = await prisma.customer.findUnique({ where: { id: customerId } });
|
||||
if (!customer) {
|
||||
res.status(404).json({ success: false, error: 'Kunde nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!customer.privacyPolicyPath) {
|
||||
res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Datei löschen
|
||||
const filePath = path.join(process.cwd(), customer.privacyPolicyPath);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
// Pfad in DB auf null setzen
|
||||
await prisma.customer.update({
|
||||
where: { id: customerId },
|
||||
data: { privacyPolicyPath: null },
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error);
|
||||
res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== VERTRAGS-DOKUMENTE ====================
|
||||
|
||||
// Generische Funktion für Vertrags-Dokument Upload
|
||||
async function handleContractDocumentUpload(
|
||||
req: AuthRequest,
|
||||
res: Response,
|
||||
fieldName: 'cancellationLetterPath' | 'cancellationConfirmationPath' | 'cancellationLetterOptionsPath' | 'cancellationConfirmationOptionsPath',
|
||||
subDir: string
|
||||
) {
|
||||
try {
|
||||
if (!req.file) {
|
||||
res.status(400).json({ success: false, error: 'Keine Datei hochgeladen' });
|
||||
return;
|
||||
}
|
||||
|
||||
const contractId = parseInt(req.params.id);
|
||||
const relativePath = `/uploads/${subDir}/${req.file.filename}`;
|
||||
|
||||
// Alte Datei löschen falls vorhanden
|
||||
const contract = await prisma.contract.findUnique({ where: { id: contractId } });
|
||||
if (!contract) {
|
||||
res.status(404).json({ success: false, error: 'Vertrag nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
const oldPath = contract[fieldName];
|
||||
if (oldPath) {
|
||||
const fullPath = path.join(process.cwd(), oldPath);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
fs.unlinkSync(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Vertrag in der DB aktualisieren
|
||||
await prisma.contract.update({
|
||||
where: { id: contractId },
|
||||
data: { [fieldName]: relativePath },
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
path: relativePath,
|
||||
filename: req.file.filename,
|
||||
originalName: req.file.originalname,
|
||||
size: req.file.size,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
res.status(500).json({ success: false, error: 'Upload fehlgeschlagen' });
|
||||
}
|
||||
}
|
||||
|
||||
// Generische Funktion für Vertrags-Dokument Löschen
|
||||
async function handleContractDocumentDelete(
|
||||
req: AuthRequest,
|
||||
res: Response,
|
||||
fieldName: 'cancellationLetterPath' | 'cancellationConfirmationPath' | 'cancellationLetterOptionsPath' | 'cancellationConfirmationOptionsPath'
|
||||
) {
|
||||
try {
|
||||
const contractId = parseInt(req.params.id);
|
||||
|
||||
const contract = await prisma.contract.findUnique({ where: { id: contractId } });
|
||||
if (!contract) {
|
||||
res.status(404).json({ success: false, error: 'Vertrag nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
|
||||
const documentPath = contract[fieldName];
|
||||
if (!documentPath) {
|
||||
res.status(400).json({ success: false, error: 'Kein Dokument vorhanden' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Datei löschen
|
||||
const filePath = path.join(process.cwd(), documentPath);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
// Pfad in DB auf null setzen
|
||||
await prisma.contract.update({
|
||||
where: { id: contractId },
|
||||
data: { [fieldName]: null },
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error);
|
||||
res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' });
|
||||
}
|
||||
}
|
||||
|
||||
// Kündigungsschreiben
|
||||
router.post(
|
||||
'/contracts/:id/cancellation-letter',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
setUploadDir('cancellation-letters'),
|
||||
upload.single('document'),
|
||||
(req: AuthRequest, res: Response) => handleContractDocumentUpload(req, res, 'cancellationLetterPath', 'cancellation-letters')
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/contracts/:id/cancellation-letter',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
(req: AuthRequest, res: Response) => handleContractDocumentDelete(req, res, 'cancellationLetterPath')
|
||||
);
|
||||
|
||||
// Kündigungsbestätigung
|
||||
router.post(
|
||||
'/contracts/:id/cancellation-confirmation',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
setUploadDir('cancellation-confirmations'),
|
||||
upload.single('document'),
|
||||
(req: AuthRequest, res: Response) => handleContractDocumentUpload(req, res, 'cancellationConfirmationPath', 'cancellation-confirmations')
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/contracts/:id/cancellation-confirmation',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
(req: AuthRequest, res: Response) => handleContractDocumentDelete(req, res, 'cancellationConfirmationPath')
|
||||
);
|
||||
|
||||
// Kündigungsschreiben Optionen
|
||||
router.post(
|
||||
'/contracts/:id/cancellation-letter-options',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
setUploadDir('cancellation-letters-options'),
|
||||
upload.single('document'),
|
||||
(req: AuthRequest, res: Response) => handleContractDocumentUpload(req, res, 'cancellationLetterOptionsPath', 'cancellation-letters-options')
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/contracts/:id/cancellation-letter-options',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
(req: AuthRequest, res: Response) => handleContractDocumentDelete(req, res, 'cancellationLetterOptionsPath')
|
||||
);
|
||||
|
||||
// Kündigungsbestätigung Optionen
|
||||
router.post(
|
||||
'/contracts/:id/cancellation-confirmation-options',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
setUploadDir('cancellation-confirmations-options'),
|
||||
upload.single('document'),
|
||||
(req: AuthRequest, res: Response) => handleContractDocumentUpload(req, res, 'cancellationConfirmationOptionsPath', 'cancellation-confirmations-options')
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/contracts/:id/cancellation-confirmation-options',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
(req: AuthRequest, res: Response) => handleContractDocumentDelete(req, res, 'cancellationConfirmationOptionsPath')
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Router } from 'express';
|
||||
import * as userController from '../controllers/user.controller.js';
|
||||
import { authenticate, requirePermission } from '../middleware/auth.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Users (Admin only)
|
||||
router.get('/', authenticate, requirePermission('users:read'), userController.getUsers);
|
||||
router.post('/', authenticate, requirePermission('users:create'), userController.createUser);
|
||||
router.get('/:id', authenticate, requirePermission('users:read'), userController.getUser);
|
||||
router.put('/:id', authenticate, requirePermission('users:update'), userController.updateUser);
|
||||
router.delete('/:id', authenticate, requirePermission('users:delete'), userController.deleteUser);
|
||||
|
||||
// Roles
|
||||
router.get('/roles/list', authenticate, requirePermission('users:read'), userController.getRoles);
|
||||
router.post('/roles', authenticate, requirePermission('users:create'), userController.createRole);
|
||||
router.get('/roles/:id', authenticate, requirePermission('users:read'), userController.getRole);
|
||||
router.put('/roles/:id', authenticate, requirePermission('users:update'), userController.updateRole);
|
||||
router.delete('/roles/:id', authenticate, requirePermission('users:delete'), userController.deleteRole);
|
||||
|
||||
// Permissions
|
||||
router.get('/permissions/list', authenticate, requirePermission('users:read'), userController.getPermissions);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,66 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// Default settings
|
||||
const DEFAULT_SETTINGS: Record<string, string> = {
|
||||
customerSupportTicketsEnabled: 'false',
|
||||
// Vertrags-Cockpit: Fristenschwellen (in Tagen)
|
||||
deadlineCriticalDays: '14', // Rot: Kritisch
|
||||
deadlineWarningDays: '42', // Gelb: Warnung (6 Wochen)
|
||||
deadlineOkDays: '90', // Grün: OK (3 Monate)
|
||||
};
|
||||
|
||||
export async function getSetting(key: string): Promise<string | null> {
|
||||
const setting = await prisma.appSetting.findUnique({
|
||||
where: { key },
|
||||
});
|
||||
|
||||
if (setting) {
|
||||
return setting.value;
|
||||
}
|
||||
|
||||
// Return default if exists
|
||||
return DEFAULT_SETTINGS[key] ?? null;
|
||||
}
|
||||
|
||||
export async function getSettingBool(key: string): Promise<boolean> {
|
||||
const value = await getSetting(key);
|
||||
return value === 'true';
|
||||
}
|
||||
|
||||
export async function setSetting(key: string, value: string): Promise<void> {
|
||||
await prisma.appSetting.upsert({
|
||||
where: { key },
|
||||
update: { value },
|
||||
create: { key, value },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAllSettings(): Promise<Record<string, string>> {
|
||||
const settings = await prisma.appSetting.findMany();
|
||||
|
||||
// Start with defaults, then override with stored values
|
||||
const result = { ...DEFAULT_SETTINGS };
|
||||
|
||||
for (const setting of settings) {
|
||||
result[setting.key] = setting.value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getPublicSettings(): Promise<Record<string, string>> {
|
||||
// Settings that should be available to all authenticated users (including customers)
|
||||
const publicKeys = ['customerSupportTicketsEnabled'];
|
||||
const allSettings = await getAllSettings();
|
||||
|
||||
const result: Record<string, string> = {};
|
||||
for (const key of publicKeys) {
|
||||
if (key in allSettings) {
|
||||
result[key] = allSettings[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { JwtPayload } from '../types/index.js';
|
||||
import { encrypt, decrypt } from '../utils/encryption.js';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// Mitarbeiter-Login
|
||||
export async function login(email: string, password: string) {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
include: {
|
||||
roles: {
|
||||
include: {
|
||||
role: {
|
||||
include: {
|
||||
permissions: {
|
||||
include: {
|
||||
permission: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
throw new Error('Ungültige Anmeldedaten');
|
||||
}
|
||||
|
||||
const isValid = await bcrypt.compare(password, user.password);
|
||||
if (!isValid) {
|
||||
throw new Error('Ungültige Anmeldedaten');
|
||||
}
|
||||
|
||||
// Collect all permissions from all roles
|
||||
const permissions = new Set<string>();
|
||||
for (const userRole of user.roles) {
|
||||
for (const rolePerm of userRole.role.permissions) {
|
||||
permissions.add(
|
||||
`${rolePerm.permission.resource}:${rolePerm.permission.action}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const payload: JwtPayload = {
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
permissions: Array.from(permissions),
|
||||
customerId: user.customerId ?? undefined,
|
||||
isCustomerPortal: false,
|
||||
};
|
||||
|
||||
const token = jwt.sign(payload, process.env.JWT_SECRET || 'fallback-secret', {
|
||||
expiresIn: (process.env.JWT_EXPIRES_IN || '7d') as jwt.SignOptions['expiresIn'],
|
||||
});
|
||||
|
||||
return {
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
permissions: Array.from(permissions),
|
||||
customerId: user.customerId,
|
||||
isCustomerPortal: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Kundenportal-Login
|
||||
export async function customerLogin(email: string, password: string) {
|
||||
console.log('[CustomerLogin] Versuch mit E-Mail:', email);
|
||||
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { portalEmail: email },
|
||||
include: {
|
||||
// Kunden, die dieser Kunde vertreten kann
|
||||
representingFor: {
|
||||
where: { isActive: true },
|
||||
include: {
|
||||
customer: {
|
||||
select: {
|
||||
id: true,
|
||||
customerNumber: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
companyName: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log('[CustomerLogin] Kunde gefunden:', customer ? `ID ${customer.id}, portalEnabled: ${customer.portalEnabled}, hasPasswordHash: ${!!customer.portalPasswordHash}` : 'NEIN');
|
||||
|
||||
if (!customer || !customer.portalEnabled || !customer.portalPasswordHash) {
|
||||
console.log('[CustomerLogin] Abbruch: Kunde nicht gefunden oder Portal nicht aktiviert');
|
||||
throw new Error('Ungültige Anmeldedaten');
|
||||
}
|
||||
|
||||
const isValid = await bcrypt.compare(password, customer.portalPasswordHash);
|
||||
console.log('[CustomerLogin] Passwort-Check:', isValid ? 'OK' : 'FALSCH');
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error('Ungültige Anmeldedaten');
|
||||
}
|
||||
|
||||
// Letzte Anmeldung aktualisieren
|
||||
await prisma.customer.update({
|
||||
where: { id: customer.id },
|
||||
data: { portalLastLogin: new Date() },
|
||||
});
|
||||
|
||||
// IDs der Kunden sammeln, die dieser Kunde vertreten kann
|
||||
const representedCustomerIds = customer.representingFor.map(
|
||||
(rep) => rep.customer.id
|
||||
);
|
||||
|
||||
// Kundenportal-Berechtigungen (eingeschränkt)
|
||||
const customerPermissions = [
|
||||
'contracts:read', // Eigene Verträge lesen
|
||||
'customers:read', // Eigene Kundendaten lesen
|
||||
];
|
||||
|
||||
const payload: JwtPayload = {
|
||||
email: customer.portalEmail!,
|
||||
permissions: customerPermissions,
|
||||
customerId: customer.id,
|
||||
isCustomerPortal: true,
|
||||
representedCustomerIds,
|
||||
};
|
||||
|
||||
const token = jwt.sign(payload, process.env.JWT_SECRET || 'fallback-secret', {
|
||||
expiresIn: (process.env.JWT_EXPIRES_IN || '7d') as jwt.SignOptions['expiresIn'],
|
||||
});
|
||||
|
||||
return {
|
||||
token,
|
||||
user: {
|
||||
id: customer.id,
|
||||
email: customer.portalEmail,
|
||||
firstName: customer.firstName,
|
||||
lastName: customer.lastName,
|
||||
permissions: customerPermissions,
|
||||
customerId: customer.id,
|
||||
isCustomerPortal: true,
|
||||
representedCustomers: customer.representingFor.map((rep) => ({
|
||||
id: rep.customer.id,
|
||||
customerNumber: rep.customer.customerNumber,
|
||||
firstName: rep.customer.firstName,
|
||||
lastName: rep.customer.lastName,
|
||||
companyName: rep.customer.companyName,
|
||||
type: rep.customer.type,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Kundenportal-Passwort setzen/ändern
|
||||
export async function setCustomerPortalPassword(customerId: number, password: string) {
|
||||
console.log('[SetPortalPassword] Setze Passwort für Kunde:', customerId);
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const encryptedPassword = encrypt(password);
|
||||
|
||||
console.log('[SetPortalPassword] Hash erstellt, Länge:', hashedPassword.length);
|
||||
|
||||
await prisma.customer.update({
|
||||
where: { id: customerId },
|
||||
data: {
|
||||
portalPasswordHash: hashedPassword,
|
||||
portalPasswordEncrypted: encryptedPassword,
|
||||
},
|
||||
});
|
||||
|
||||
console.log('[SetPortalPassword] Passwort gespeichert');
|
||||
}
|
||||
|
||||
// Kundenportal-Passwort im Klartext abrufen
|
||||
export async function getCustomerPortalPassword(customerId: number): Promise<string | null> {
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
select: { portalPasswordEncrypted: true },
|
||||
});
|
||||
|
||||
if (!customer?.portalPasswordEncrypted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return decrypt(customer.portalPasswordEncrypted);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Entschlüsseln des Passworts:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createUser(data: {
|
||||
email: string;
|
||||
password: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
roleIds: number[];
|
||||
customerId?: number;
|
||||
}) {
|
||||
const hashedPassword = await bcrypt.hash(data.password, 10);
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email: data.email,
|
||||
password: hashedPassword,
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
customerId: data.customerId,
|
||||
roles: {
|
||||
create: data.roleIds.map((roleId) => ({ roleId })),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
roles: {
|
||||
include: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
roles: user.roles.map((ur) => ur.role.name),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getUserById(id: number) {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
roles: {
|
||||
include: {
|
||||
role: {
|
||||
include: {
|
||||
permissions: {
|
||||
include: {
|
||||
permission: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
console.log('auth.getUserById - user roles:', user.roles.map(ur => ur.role.name));
|
||||
|
||||
const permissions = new Set<string>();
|
||||
for (const userRole of user.roles) {
|
||||
for (const rolePerm of userRole.role.permissions) {
|
||||
permissions.add(
|
||||
`${rolePerm.permission.resource}:${rolePerm.permission.action}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('auth.getUserById - permissions:', Array.from(permissions));
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
isActive: user.isActive,
|
||||
customerId: user.customerId,
|
||||
roles: user.roles.map((ur) => ur.role.name),
|
||||
permissions: Array.from(permissions),
|
||||
isCustomerPortal: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Kundenportal-Benutzer laden (für /me Endpoint)
|
||||
export async function getCustomerPortalUser(customerId: number) {
|
||||
const customer = await prisma.customer.findUnique({
|
||||
where: { id: customerId },
|
||||
include: {
|
||||
representingFor: {
|
||||
where: { isActive: true },
|
||||
include: {
|
||||
customer: {
|
||||
select: {
|
||||
id: true,
|
||||
customerNumber: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
companyName: true,
|
||||
type: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!customer || !customer.portalEnabled) return null;
|
||||
|
||||
const customerPermissions = [
|
||||
'contracts:read',
|
||||
'customers:read',
|
||||
];
|
||||
|
||||
return {
|
||||
id: customer.id,
|
||||
email: customer.portalEmail,
|
||||
firstName: customer.firstName,
|
||||
lastName: customer.lastName,
|
||||
isActive: customer.portalEnabled,
|
||||
customerId: customer.id,
|
||||
permissions: customerPermissions,
|
||||
isCustomerPortal: true,
|
||||
representedCustomers: customer.representingFor.map((rep) => ({
|
||||
id: rep.customer.id,
|
||||
customerNumber: rep.customer.customerNumber,
|
||||
firstName: rep.customer.firstName,
|
||||
lastName: rep.customer.lastName,
|
||||
companyName: rep.customer.companyName,
|
||||
type: rep.customer.type,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function getAllCancellationPeriods(includeInactive = false) {
|
||||
const where = includeInactive ? {} : { isActive: true };
|
||||
return prisma.cancellationPeriod.findMany({
|
||||
where,
|
||||
orderBy: { code: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCancellationPeriodById(id: number) {
|
||||
return prisma.cancellationPeriod.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
_count: {
|
||||
select: { contracts: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function createCancellationPeriod(data: {
|
||||
code: string;
|
||||
description: string;
|
||||
}) {
|
||||
return prisma.cancellationPeriod.create({
|
||||
data: {
|
||||
...data,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateCancellationPeriod(
|
||||
id: number,
|
||||
data: {
|
||||
code?: string;
|
||||
description?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
) {
|
||||
return prisma.cancellationPeriod.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteCancellationPeriod(id: number) {
|
||||
// Check if cancellation period is used by any contracts
|
||||
const count = await prisma.contract.count({
|
||||
where: { cancellationPeriodId: id },
|
||||
});
|
||||
|
||||
if (count > 0) {
|
||||
throw new Error(
|
||||
`Kündigungsfrist kann nicht gelöscht werden, da sie von ${count} Verträgen verwendet wird`
|
||||
);
|
||||
}
|
||||
|
||||
return prisma.cancellationPeriod.delete({ where: { id } });
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function getAllContractDurations(includeInactive = false) {
|
||||
const where = includeInactive ? {} : { isActive: true };
|
||||
return prisma.contractDuration.findMany({
|
||||
where,
|
||||
orderBy: { code: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getContractDurationById(id: number) {
|
||||
return prisma.contractDuration.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
_count: {
|
||||
select: { contracts: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function createContractDuration(data: {
|
||||
code: string;
|
||||
description: string;
|
||||
}) {
|
||||
return prisma.contractDuration.create({
|
||||
data: {
|
||||
...data,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateContractDuration(
|
||||
id: number,
|
||||
data: {
|
||||
code?: string;
|
||||
description?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
) {
|
||||
return prisma.contractDuration.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteContractDuration(id: number) {
|
||||
// Check if contract duration is used by any contracts
|
||||
const count = await prisma.contract.count({
|
||||
where: { contractDurationId: id },
|
||||
});
|
||||
|
||||
if (count > 0) {
|
||||
throw new Error(
|
||||
`Laufzeit kann nicht gelöscht werden, da sie von ${count} Verträgen verwendet wird`
|
||||
);
|
||||
}
|
||||
|
||||
return prisma.contractDuration.delete({ where: { id } });
|
||||
}
|
||||
@@ -0,0 +1,780 @@
|
||||
import { PrismaClient, ContractType, ContractStatus } from '@prisma/client';
|
||||
import { generateContractNumber, paginate, buildPaginationResponse } from '../utils/helpers.js';
|
||||
import { encrypt, decrypt } from '../utils/encryption.js';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export interface ContractFilters {
|
||||
customerId?: number;
|
||||
customerIds?: number[]; // Für Kundenportal: eigene ID + vertretene Kunden
|
||||
type?: ContractType;
|
||||
status?: ContractStatus;
|
||||
search?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export async function getAllContracts(filters: ContractFilters) {
|
||||
const { customerId, customerIds, type, status, search, page = 1, limit = 20 } = filters;
|
||||
const { skip, take } = paginate(page, limit);
|
||||
|
||||
const where: Record<string, unknown> = {};
|
||||
|
||||
// Entweder einzelne customerId ODER Liste von customerIds (für Kundenportal)
|
||||
if (customerIds && customerIds.length > 0) {
|
||||
where.customerId = { in: customerIds };
|
||||
} else if (customerId) {
|
||||
where.customerId = customerId;
|
||||
}
|
||||
if (type) where.type = type;
|
||||
|
||||
// Status-Filter: Deaktivierte Verträge standardmäßig ausblenden
|
||||
if (status) {
|
||||
where.status = status;
|
||||
} else {
|
||||
// Wenn kein Status-Filter gesetzt, alle außer DEACTIVATED anzeigen
|
||||
where.status = { not: ContractStatus.DEACTIVATED };
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
// Basis-Vertragsfelder
|
||||
{ contractNumber: { contains: search } },
|
||||
{ providerName: { contains: search } },
|
||||
{ tariffName: { contains: search } },
|
||||
{ customerNumberAtProvider: { contains: search } },
|
||||
{ provider: { name: { contains: search } } },
|
||||
{ tariff: { name: { contains: search } } },
|
||||
// Kundenname
|
||||
{ customer: { firstName: { contains: search } } },
|
||||
{ customer: { lastName: { contains: search } } },
|
||||
{ customer: { companyName: { contains: search } } },
|
||||
{ customer: { customerNumber: { contains: search } } },
|
||||
// Internet-Vertragsdetails
|
||||
{ internetDetails: { routerSerialNumber: { contains: search } } },
|
||||
{ internetDetails: { homeId: { contains: search } } },
|
||||
{ internetDetails: { activationCode: { contains: search } } },
|
||||
{ internetDetails: { phoneNumbers: { some: { phoneNumber: { contains: search } } } } },
|
||||
// Mobilfunk-Vertragsdetails
|
||||
{ mobileDetails: { phoneNumber: { contains: search } } },
|
||||
{ mobileDetails: { simCardNumber: { contains: search } } },
|
||||
{ mobileDetails: { deviceImei: { contains: search } } },
|
||||
{ mobileDetails: { simCards: { some: { phoneNumber: { contains: search } } } } },
|
||||
{ mobileDetails: { simCards: { some: { simCardNumber: { contains: search } } } } },
|
||||
// Energie-Vertragsdetails (Zählernummer)
|
||||
{ energyDetails: { meter: { meterNumber: { contains: search } } } },
|
||||
// TV-Vertragsdetails
|
||||
{ tvDetails: { smartcardNumber: { contains: search } } },
|
||||
// KFZ-Versicherung
|
||||
{ carInsuranceDetails: { licensePlate: { contains: search } } },
|
||||
{ carInsuranceDetails: { vin: { contains: search } } },
|
||||
{ carInsuranceDetails: { policyNumber: { contains: search } } },
|
||||
];
|
||||
}
|
||||
|
||||
const [contracts, total] = await Promise.all([
|
||||
prisma.contract.findMany({
|
||||
where,
|
||||
skip,
|
||||
take,
|
||||
orderBy: [{ startDate: 'desc' }, { createdAt: 'desc' }],
|
||||
include: {
|
||||
customer: {
|
||||
select: {
|
||||
id: true,
|
||||
customerNumber: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
companyName: true,
|
||||
},
|
||||
},
|
||||
address: true,
|
||||
salesPlatform: true,
|
||||
cancellationPeriod: true,
|
||||
contractDuration: true,
|
||||
provider: true,
|
||||
tariff: true,
|
||||
contractCategory: true,
|
||||
},
|
||||
}),
|
||||
prisma.contract.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
contracts,
|
||||
pagination: buildPaginationResponse(page, limit, total),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getContractById(id: number, decryptPassword = false) {
|
||||
const contract = await prisma.contract.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
customer: true,
|
||||
address: true,
|
||||
bankCard: true,
|
||||
identityDocument: true,
|
||||
salesPlatform: true,
|
||||
cancellationPeriod: true,
|
||||
contractDuration: true,
|
||||
provider: true,
|
||||
tariff: true,
|
||||
contractCategory: true,
|
||||
previousContract: {
|
||||
include: {
|
||||
energyDetails: { include: { meter: { include: { readings: true } } } },
|
||||
internetDetails: { include: { phoneNumbers: true } },
|
||||
mobileDetails: { include: { simCards: true } },
|
||||
tvDetails: true,
|
||||
carInsuranceDetails: true,
|
||||
},
|
||||
},
|
||||
energyDetails: { include: { meter: { include: { readings: true } } } },
|
||||
internetDetails: { include: { phoneNumbers: true } },
|
||||
mobileDetails: { include: { simCards: true } },
|
||||
tvDetails: true,
|
||||
carInsuranceDetails: true,
|
||||
stressfreiEmail: true,
|
||||
followUpContract: {
|
||||
select: { id: true, contractNumber: true, status: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!contract) return null;
|
||||
|
||||
// Decrypt password if requested and exists
|
||||
if (decryptPassword && contract.portalPasswordEncrypted) {
|
||||
try {
|
||||
(contract as Record<string, unknown>).portalPasswordDecrypted = decrypt(
|
||||
contract.portalPasswordEncrypted
|
||||
);
|
||||
} catch {
|
||||
// Password decryption failed, leave as is
|
||||
}
|
||||
}
|
||||
|
||||
return contract;
|
||||
}
|
||||
|
||||
interface ContractCreateData {
|
||||
customerId: number;
|
||||
type: ContractType;
|
||||
contractCategoryId?: number;
|
||||
status?: ContractStatus;
|
||||
addressId?: number;
|
||||
bankCardId?: number;
|
||||
identityDocumentId?: number;
|
||||
salesPlatformId?: number;
|
||||
previousContractId?: number;
|
||||
providerId?: number;
|
||||
tariffId?: number;
|
||||
providerName?: string;
|
||||
tariffName?: string;
|
||||
customerNumberAtProvider?: string;
|
||||
priceFirst12Months?: string;
|
||||
priceFrom13Months?: string;
|
||||
priceAfter24Months?: string;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
cancellationPeriodId?: number;
|
||||
contractDurationId?: number;
|
||||
commission?: number;
|
||||
portalUsername?: string;
|
||||
portalPassword?: string;
|
||||
stressfreiEmailId?: number;
|
||||
notes?: string;
|
||||
// Kündigungsdaten
|
||||
cancellationConfirmationDate?: Date;
|
||||
cancellationConfirmationOptionsDate?: Date;
|
||||
wasSpecialCancellation?: boolean;
|
||||
// Type-specific details
|
||||
energyDetails?: {
|
||||
meterId?: number;
|
||||
annualConsumption?: number;
|
||||
basePrice?: number;
|
||||
unitPrice?: number;
|
||||
bonus?: number;
|
||||
previousProviderName?: string;
|
||||
previousCustomerNumber?: string;
|
||||
};
|
||||
internetDetails?: {
|
||||
downloadSpeed?: number;
|
||||
uploadSpeed?: number;
|
||||
routerModel?: string;
|
||||
routerSerialNumber?: string;
|
||||
installationDate?: Date;
|
||||
// Internet-Zugangsdaten
|
||||
internetUsername?: string;
|
||||
internetPassword?: string;
|
||||
// Glasfaser-spezifisch
|
||||
homeId?: string;
|
||||
// Vodafone DSL/Kabel spezifisch
|
||||
activationCode?: string;
|
||||
phoneNumbers?: {
|
||||
id?: number;
|
||||
phoneNumber: string;
|
||||
isMain?: boolean;
|
||||
sipUsername?: string;
|
||||
sipPassword?: string;
|
||||
sipServer?: string;
|
||||
}[];
|
||||
};
|
||||
mobileDetails?: {
|
||||
requiresMultisim?: boolean;
|
||||
dataVolume?: number;
|
||||
includedMinutes?: number;
|
||||
includedSMS?: number;
|
||||
deviceModel?: string;
|
||||
deviceImei?: string;
|
||||
// Legacy-Felder
|
||||
phoneNumber?: string;
|
||||
simCardNumber?: string;
|
||||
// SIM-Karten Liste
|
||||
simCards?: {
|
||||
id?: number;
|
||||
phoneNumber?: string;
|
||||
simCardNumber?: string;
|
||||
pin?: string;
|
||||
puk?: string;
|
||||
isMultisim?: boolean;
|
||||
isMain?: boolean;
|
||||
}[];
|
||||
};
|
||||
tvDetails?: {
|
||||
receiverModel?: string;
|
||||
smartcardNumber?: string;
|
||||
package?: string;
|
||||
};
|
||||
carInsuranceDetails?: {
|
||||
licensePlate?: string;
|
||||
hsn?: string;
|
||||
tsn?: string;
|
||||
vin?: string;
|
||||
vehicleType?: string;
|
||||
firstRegistration?: Date;
|
||||
noClaimsClass?: string;
|
||||
insuranceType?: 'LIABILITY' | 'PARTIAL' | 'FULL';
|
||||
deductiblePartial?: number;
|
||||
deductibleFull?: number;
|
||||
policyNumber?: string;
|
||||
previousInsurer?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function createContract(data: ContractCreateData) {
|
||||
const {
|
||||
energyDetails,
|
||||
internetDetails,
|
||||
mobileDetails,
|
||||
tvDetails,
|
||||
carInsuranceDetails,
|
||||
portalPassword,
|
||||
...contractData
|
||||
} = data;
|
||||
|
||||
// Encrypt password if provided
|
||||
const portalPasswordEncrypted = portalPassword
|
||||
? encrypt(portalPassword)
|
||||
: undefined;
|
||||
|
||||
const contract = await prisma.contract.create({
|
||||
data: {
|
||||
...contractData,
|
||||
contractNumber: generateContractNumber(data.type),
|
||||
portalPasswordEncrypted,
|
||||
...(energyDetails && ['ELECTRICITY', 'GAS'].includes(data.type)
|
||||
? { energyDetails: { create: energyDetails } }
|
||||
: {}),
|
||||
...(internetDetails && ['DSL', 'CABLE', 'FIBER'].includes(data.type)
|
||||
? {
|
||||
internetDetails: {
|
||||
create: {
|
||||
downloadSpeed: internetDetails.downloadSpeed,
|
||||
uploadSpeed: internetDetails.uploadSpeed,
|
||||
routerModel: internetDetails.routerModel,
|
||||
routerSerialNumber: internetDetails.routerSerialNumber,
|
||||
installationDate: internetDetails.installationDate,
|
||||
internetUsername: internetDetails.internetUsername,
|
||||
internetPasswordEncrypted: internetDetails.internetPassword
|
||||
? encrypt(internetDetails.internetPassword)
|
||||
: undefined,
|
||||
homeId: internetDetails.homeId,
|
||||
activationCode: internetDetails.activationCode,
|
||||
phoneNumbers: internetDetails.phoneNumbers && internetDetails.phoneNumbers.length > 0
|
||||
? {
|
||||
create: internetDetails.phoneNumbers.map((pn) => ({
|
||||
phoneNumber: pn.phoneNumber,
|
||||
isMain: pn.isMain ?? false,
|
||||
sipUsername: pn.sipUsername,
|
||||
sipPasswordEncrypted: pn.sipPassword
|
||||
? encrypt(pn.sipPassword)
|
||||
: undefined,
|
||||
sipServer: pn.sipServer,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(mobileDetails && data.type === 'MOBILE'
|
||||
? {
|
||||
mobileDetails: {
|
||||
create: {
|
||||
requiresMultisim: mobileDetails.requiresMultisim,
|
||||
dataVolume: mobileDetails.dataVolume,
|
||||
includedMinutes: mobileDetails.includedMinutes,
|
||||
includedSMS: mobileDetails.includedSMS,
|
||||
deviceModel: mobileDetails.deviceModel,
|
||||
deviceImei: mobileDetails.deviceImei,
|
||||
phoneNumber: mobileDetails.phoneNumber,
|
||||
simCardNumber: mobileDetails.simCardNumber,
|
||||
simCards: mobileDetails.simCards
|
||||
? {
|
||||
create: mobileDetails.simCards.map((sc) => ({
|
||||
phoneNumber: sc.phoneNumber,
|
||||
simCardNumber: sc.simCardNumber,
|
||||
pin: sc.pin ? encrypt(sc.pin) : undefined,
|
||||
puk: sc.puk ? encrypt(sc.puk) : undefined,
|
||||
isMultisim: sc.isMultisim ?? false,
|
||||
isMain: sc.isMain ?? false,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(tvDetails && data.type === 'TV'
|
||||
? { tvDetails: { create: tvDetails } }
|
||||
: {}),
|
||||
...(carInsuranceDetails && data.type === 'CAR_INSURANCE'
|
||||
? { carInsuranceDetails: { create: carInsuranceDetails } }
|
||||
: {}),
|
||||
},
|
||||
include: {
|
||||
customer: true,
|
||||
address: true,
|
||||
salesPlatform: true,
|
||||
energyDetails: true,
|
||||
internetDetails: { include: { phoneNumbers: true } },
|
||||
mobileDetails: { include: { simCards: true } },
|
||||
tvDetails: true,
|
||||
carInsuranceDetails: true,
|
||||
},
|
||||
});
|
||||
|
||||
return contract;
|
||||
}
|
||||
|
||||
export async function updateContract(
|
||||
id: number,
|
||||
data: Partial<ContractCreateData>
|
||||
) {
|
||||
const {
|
||||
energyDetails,
|
||||
internetDetails,
|
||||
mobileDetails,
|
||||
tvDetails,
|
||||
carInsuranceDetails,
|
||||
portalPassword,
|
||||
...contractData
|
||||
} = data;
|
||||
|
||||
// Encrypt password if provided
|
||||
const portalPasswordEncrypted = portalPassword
|
||||
? encrypt(portalPassword)
|
||||
: undefined;
|
||||
|
||||
// Update main contract
|
||||
await prisma.contract.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...contractData,
|
||||
...(portalPasswordEncrypted ? { portalPasswordEncrypted } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
// Update type-specific details
|
||||
if (energyDetails) {
|
||||
await prisma.energyContractDetails.upsert({
|
||||
where: { contractId: id },
|
||||
update: energyDetails,
|
||||
create: { contractId: id, ...energyDetails },
|
||||
});
|
||||
}
|
||||
|
||||
if (internetDetails) {
|
||||
const { phoneNumbers, internetPassword, ...internetData } = internetDetails;
|
||||
const existing = await prisma.internetContractDetails.findUnique({
|
||||
where: { contractId: id },
|
||||
include: { phoneNumbers: true },
|
||||
});
|
||||
|
||||
// Prepare internet data with encryption
|
||||
const preparedInternetData = {
|
||||
downloadSpeed: internetData.downloadSpeed,
|
||||
uploadSpeed: internetData.uploadSpeed,
|
||||
routerModel: internetData.routerModel,
|
||||
routerSerialNumber: internetData.routerSerialNumber,
|
||||
installationDate: internetData.installationDate,
|
||||
internetUsername: internetData.internetUsername,
|
||||
// Only update password if new value provided, otherwise keep existing
|
||||
...(internetPassword
|
||||
? { internetPasswordEncrypted: encrypt(internetPassword) }
|
||||
: {}),
|
||||
homeId: internetData.homeId,
|
||||
activationCode: internetData.activationCode,
|
||||
};
|
||||
|
||||
if (existing) {
|
||||
await prisma.internetContractDetails.update({
|
||||
where: { contractId: id },
|
||||
data: preparedInternetData,
|
||||
});
|
||||
|
||||
if (phoneNumbers) {
|
||||
// Get existing phone numbers for preserving encrypted passwords
|
||||
const existingPhoneNumbers = existing.phoneNumbers || [];
|
||||
|
||||
// Delete all existing phone numbers
|
||||
await prisma.phoneNumber.deleteMany({
|
||||
where: { internetContractDetailsId: existing.id },
|
||||
});
|
||||
|
||||
// Create new phone numbers with encryption
|
||||
await prisma.phoneNumber.createMany({
|
||||
data: phoneNumbers.map((pn) => {
|
||||
// Find existing entry to preserve sipPassword if not changed
|
||||
const existingPn = pn.id
|
||||
? existingPhoneNumbers.find((e) => e.id === pn.id)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
internetContractDetailsId: existing.id,
|
||||
phoneNumber: pn.phoneNumber,
|
||||
isMain: pn.isMain ?? false,
|
||||
sipUsername: pn.sipUsername,
|
||||
// Preserve existing sipPassword if no new value provided
|
||||
sipPasswordEncrypted: pn.sipPassword
|
||||
? encrypt(pn.sipPassword)
|
||||
: existingPn?.sipPasswordEncrypted ?? undefined,
|
||||
sipServer: pn.sipServer,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await prisma.internetContractDetails.create({
|
||||
data: {
|
||||
contractId: id,
|
||||
...preparedInternetData,
|
||||
...(internetPassword
|
||||
? { internetPasswordEncrypted: encrypt(internetPassword) }
|
||||
: {}),
|
||||
phoneNumbers: phoneNumbers
|
||||
? {
|
||||
create: phoneNumbers.map((pn) => ({
|
||||
phoneNumber: pn.phoneNumber,
|
||||
isMain: pn.isMain ?? false,
|
||||
sipUsername: pn.sipUsername,
|
||||
sipPasswordEncrypted: pn.sipPassword
|
||||
? encrypt(pn.sipPassword)
|
||||
: undefined,
|
||||
sipServer: pn.sipServer,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (mobileDetails) {
|
||||
const { simCards, ...mobileData } = mobileDetails;
|
||||
const existing = await prisma.mobileContractDetails.findUnique({
|
||||
where: { contractId: id },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
await prisma.mobileContractDetails.update({
|
||||
where: { contractId: id },
|
||||
data: mobileData,
|
||||
});
|
||||
|
||||
if (simCards) {
|
||||
// Get existing sim cards to preserve PIN/PUK if not provided
|
||||
const existingSimCards = await prisma.simCard.findMany({
|
||||
where: { mobileDetailsId: existing.id },
|
||||
});
|
||||
const existingSimCardMap = new Map(existingSimCards.map(sc => [sc.id, sc]));
|
||||
|
||||
// Delete existing sim cards
|
||||
await prisma.simCard.deleteMany({
|
||||
where: { mobileDetailsId: existing.id },
|
||||
});
|
||||
|
||||
// Create new sim cards, preserving PIN/PUK if not provided
|
||||
await prisma.simCard.createMany({
|
||||
data: simCards.map((sc) => {
|
||||
const existingSc = sc.id ? existingSimCardMap.get(sc.id) : undefined;
|
||||
return {
|
||||
mobileDetailsId: existing.id,
|
||||
phoneNumber: sc.phoneNumber,
|
||||
simCardNumber: sc.simCardNumber,
|
||||
// Preserve existing PIN/PUK if no new value provided
|
||||
pin: sc.pin ? encrypt(sc.pin) : (existingSc?.pin ?? undefined),
|
||||
puk: sc.puk ? encrypt(sc.puk) : (existingSc?.puk ?? undefined),
|
||||
isMultisim: sc.isMultisim ?? false,
|
||||
isMain: sc.isMain ?? false,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await prisma.mobileContractDetails.create({
|
||||
data: {
|
||||
contractId: id,
|
||||
...mobileData,
|
||||
simCards: simCards
|
||||
? {
|
||||
create: simCards.map((sc) => ({
|
||||
phoneNumber: sc.phoneNumber,
|
||||
simCardNumber: sc.simCardNumber,
|
||||
pin: sc.pin ? encrypt(sc.pin) : undefined,
|
||||
puk: sc.puk ? encrypt(sc.puk) : undefined,
|
||||
isMultisim: sc.isMultisim ?? false,
|
||||
isMain: sc.isMain ?? false,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tvDetails) {
|
||||
await prisma.tvContractDetails.upsert({
|
||||
where: { contractId: id },
|
||||
update: tvDetails,
|
||||
create: { contractId: id, ...tvDetails },
|
||||
});
|
||||
}
|
||||
|
||||
if (carInsuranceDetails) {
|
||||
await prisma.carInsuranceDetails.upsert({
|
||||
where: { contractId: id },
|
||||
update: carInsuranceDetails,
|
||||
create: { contractId: id, ...carInsuranceDetails },
|
||||
});
|
||||
}
|
||||
|
||||
return getContractById(id);
|
||||
}
|
||||
|
||||
export async function deleteContract(id: number) {
|
||||
// Vertragskette erhalten beim Löschen:
|
||||
// Wenn A → B → C und B gelöscht wird, soll C direkt auf A zeigen (A → C)
|
||||
|
||||
// 1. Zu löschenden Vertrag holen um dessen Vorgänger zu kennen
|
||||
const contractToDelete = await prisma.contract.findUnique({
|
||||
where: { id },
|
||||
select: { previousContractId: true },
|
||||
});
|
||||
|
||||
// 2. Folgevertrag(e) mit dem Vorgänger des gelöschten Vertrags verbinden
|
||||
// So bleibt die Kette erhalten: A → B → C wird zu A → C
|
||||
await prisma.contract.updateMany({
|
||||
where: { previousContractId: id },
|
||||
data: { previousContractId: contractToDelete?.previousContractId ?? null },
|
||||
});
|
||||
|
||||
return prisma.contract.delete({ where: { id } });
|
||||
}
|
||||
|
||||
export async function createFollowUpContract(previousContractId: number) {
|
||||
const previousContract = await getContractById(previousContractId);
|
||||
if (!previousContract) {
|
||||
throw new Error('Vorgängervertrag nicht gefunden');
|
||||
}
|
||||
|
||||
// Prüfen ob bereits ein Folgevertrag existiert
|
||||
const existingFollowUp = await prisma.contract.findFirst({
|
||||
where: { previousContractId },
|
||||
select: { id: true, contractNumber: true },
|
||||
});
|
||||
if (existingFollowUp) {
|
||||
throw new Error(`Es existiert bereits ein Folgevertrag: ${existingFollowUp.contractNumber}`);
|
||||
}
|
||||
|
||||
// Copy data but exclude provider credentials and some fields
|
||||
const newContractData: ContractCreateData = {
|
||||
customerId: previousContract.customerId,
|
||||
type: previousContract.type,
|
||||
status: 'DRAFT',
|
||||
addressId: previousContract.addressId ?? undefined,
|
||||
bankCardId: previousContract.bankCardId ?? undefined,
|
||||
identityDocumentId: previousContract.identityDocumentId ?? undefined,
|
||||
salesPlatformId: previousContract.salesPlatformId ?? undefined,
|
||||
previousContractId: previousContract.id,
|
||||
// Explicitly NOT copying: providerName, tariffName, portalUsername, portalPassword, price fields
|
||||
cancellationPeriodId: previousContract.cancellationPeriodId ?? undefined,
|
||||
contractDurationId: previousContract.contractDurationId ?? undefined,
|
||||
notes: `Folgevertrag zu ${previousContract.contractNumber}`,
|
||||
};
|
||||
|
||||
// Copy type-specific details (without credentials)
|
||||
if (previousContract.energyDetails) {
|
||||
newContractData.energyDetails = {
|
||||
meterId: previousContract.energyDetails.meterId ?? undefined,
|
||||
annualConsumption:
|
||||
previousContract.energyDetails.annualConsumption ?? undefined,
|
||||
basePrice: previousContract.energyDetails.basePrice ?? undefined,
|
||||
unitPrice: previousContract.energyDetails.unitPrice ?? undefined,
|
||||
bonus: previousContract.energyDetails.bonus ?? undefined,
|
||||
previousProviderName: previousContract.providerName ?? undefined,
|
||||
previousCustomerNumber:
|
||||
previousContract.customerNumberAtProvider ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (previousContract.internetDetails) {
|
||||
newContractData.internetDetails = {
|
||||
downloadSpeed:
|
||||
previousContract.internetDetails.downloadSpeed ?? undefined,
|
||||
uploadSpeed: previousContract.internetDetails.uploadSpeed ?? undefined,
|
||||
routerModel: previousContract.internetDetails.routerModel ?? undefined,
|
||||
routerSerialNumber:
|
||||
previousContract.internetDetails.routerSerialNumber ?? undefined,
|
||||
phoneNumbers: previousContract.internetDetails.phoneNumbers.map((pn) => ({
|
||||
phoneNumber: pn.phoneNumber,
|
||||
isMain: pn.isMain,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
if (previousContract.mobileDetails) {
|
||||
newContractData.mobileDetails = {
|
||||
requiresMultisim: previousContract.mobileDetails.requiresMultisim ?? undefined,
|
||||
dataVolume: previousContract.mobileDetails.dataVolume ?? undefined,
|
||||
includedMinutes:
|
||||
previousContract.mobileDetails.includedMinutes ?? undefined,
|
||||
includedSMS: previousContract.mobileDetails.includedSMS ?? undefined,
|
||||
deviceModel: previousContract.mobileDetails.deviceModel ?? undefined,
|
||||
deviceImei: previousContract.mobileDetails.deviceImei ?? undefined,
|
||||
phoneNumber: previousContract.mobileDetails.phoneNumber ?? undefined,
|
||||
simCardNumber: previousContract.mobileDetails.simCardNumber ?? undefined,
|
||||
// Copy simCards without PIN/PUK (security)
|
||||
simCards: previousContract.mobileDetails.simCards?.map((sc) => ({
|
||||
phoneNumber: sc.phoneNumber ?? undefined,
|
||||
simCardNumber: sc.simCardNumber ?? undefined,
|
||||
isMultisim: sc.isMultisim,
|
||||
isMain: sc.isMain,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
if (previousContract.tvDetails) {
|
||||
newContractData.tvDetails = {
|
||||
receiverModel: previousContract.tvDetails.receiverModel ?? undefined,
|
||||
smartcardNumber:
|
||||
previousContract.tvDetails.smartcardNumber ?? undefined,
|
||||
package: previousContract.tvDetails.package ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (previousContract.carInsuranceDetails) {
|
||||
newContractData.carInsuranceDetails = {
|
||||
licensePlate:
|
||||
previousContract.carInsuranceDetails.licensePlate ?? undefined,
|
||||
hsn: previousContract.carInsuranceDetails.hsn ?? undefined,
|
||||
tsn: previousContract.carInsuranceDetails.tsn ?? undefined,
|
||||
vin: previousContract.carInsuranceDetails.vin ?? undefined,
|
||||
vehicleType: previousContract.carInsuranceDetails.vehicleType ?? undefined,
|
||||
firstRegistration:
|
||||
previousContract.carInsuranceDetails.firstRegistration ?? undefined,
|
||||
noClaimsClass:
|
||||
previousContract.carInsuranceDetails.noClaimsClass ?? undefined,
|
||||
insuranceType: previousContract.carInsuranceDetails.insuranceType,
|
||||
deductiblePartial:
|
||||
previousContract.carInsuranceDetails.deductiblePartial ?? undefined,
|
||||
deductibleFull:
|
||||
previousContract.carInsuranceDetails.deductibleFull ?? undefined,
|
||||
previousInsurer: previousContract.providerName ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return createContract(newContractData);
|
||||
}
|
||||
|
||||
// Decrypt password for viewing
|
||||
export async function getContractPassword(id: number): Promise<string | null> {
|
||||
const contract = await prisma.contract.findUnique({
|
||||
where: { id },
|
||||
select: { portalPasswordEncrypted: true },
|
||||
});
|
||||
|
||||
if (!contract?.portalPasswordEncrypted) return null;
|
||||
|
||||
try {
|
||||
return decrypt(contract.portalPasswordEncrypted);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Decrypt SimCard PIN/PUK
|
||||
export async function getSimCardCredentials(simCardId: number): Promise<{ pin: string | null; puk: string | null }> {
|
||||
const simCard = await prisma.simCard.findUnique({
|
||||
where: { id: simCardId },
|
||||
select: { pin: true, puk: true },
|
||||
});
|
||||
|
||||
if (!simCard) return { pin: null, puk: null };
|
||||
|
||||
try {
|
||||
return {
|
||||
pin: simCard.pin ? decrypt(simCard.pin) : null,
|
||||
puk: simCard.puk ? decrypt(simCard.puk) : null,
|
||||
};
|
||||
} catch {
|
||||
return { pin: null, puk: null };
|
||||
}
|
||||
}
|
||||
|
||||
// Decrypt Internet password
|
||||
export async function getInternetCredentials(contractId: number): Promise<{ password: string | null }> {
|
||||
const internetDetails = await prisma.internetContractDetails.findUnique({
|
||||
where: { contractId },
|
||||
select: { internetPasswordEncrypted: true },
|
||||
});
|
||||
|
||||
if (!internetDetails?.internetPasswordEncrypted) return { password: null };
|
||||
|
||||
try {
|
||||
return {
|
||||
password: decrypt(internetDetails.internetPasswordEncrypted),
|
||||
};
|
||||
} catch {
|
||||
return { password: null };
|
||||
}
|
||||
}
|
||||
|
||||
// Decrypt SIP password for a phone number
|
||||
export async function getSipCredentials(phoneNumberId: number): Promise<{ password: string | null }> {
|
||||
const phoneNumber = await prisma.phoneNumber.findUnique({
|
||||
where: { id: phoneNumberId },
|
||||
select: { sipPasswordEncrypted: true },
|
||||
});
|
||||
|
||||
if (!phoneNumber?.sipPasswordEncrypted) return { password: null };
|
||||
|
||||
try {
|
||||
return {
|
||||
password: decrypt(phoneNumber.sipPasswordEncrypted),
|
||||
};
|
||||
} catch {
|
||||
return { password: null };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function getAllContractCategories(includeInactive = false) {
|
||||
return prisma.contractCategory.findMany({
|
||||
where: includeInactive ? {} : { isActive: true },
|
||||
orderBy: [{ sortOrder: 'asc' }, { name: 'asc' }],
|
||||
include: {
|
||||
_count: {
|
||||
select: { contracts: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getContractCategoryById(id: number) {
|
||||
return prisma.contractCategory.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
_count: {
|
||||
select: { contracts: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getContractCategoryByCode(code: string) {
|
||||
return prisma.contractCategory.findUnique({
|
||||
where: { code },
|
||||
});
|
||||
}
|
||||
|
||||
interface ContractCategoryCreateData {
|
||||
code: string;
|
||||
name: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
sortOrder?: number;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export async function createContractCategory(data: ContractCategoryCreateData) {
|
||||
return prisma.contractCategory.create({
|
||||
data,
|
||||
include: {
|
||||
_count: {
|
||||
select: { contracts: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateContractCategory(id: number, data: Partial<ContractCategoryCreateData>) {
|
||||
return prisma.contractCategory.update({
|
||||
where: { id },
|
||||
data,
|
||||
include: {
|
||||
_count: {
|
||||
select: { contracts: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteContractCategory(id: number) {
|
||||
// Check if category has contracts
|
||||
const category = await prisma.contractCategory.findUnique({
|
||||
where: { id },
|
||||
include: { _count: { select: { contracts: true } } },
|
||||
});
|
||||
|
||||
if (category && category._count.contracts > 0) {
|
||||
throw new Error(`Kategorie kann nicht gelöscht werden, da ${category._count.contracts} Verträge zugeordnet sind.`);
|
||||
}
|
||||
|
||||
return prisma.contractCategory.delete({ where: { id } });
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
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<CockpitResult> {
|
||||
// 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<UrgencyLevel, number> = {
|
||||
critical: 0,
|
||||
warning: 1,
|
||||
ok: 2,
|
||||
none: 3,
|
||||
};
|
||||
return urgencyOrder[a.highestUrgency] - urgencyOrder[b.highestUrgency];
|
||||
});
|
||||
|
||||
return {
|
||||
contracts: cockpitContracts,
|
||||
summary,
|
||||
thresholds: {
|
||||
criticalDays,
|
||||
warningDays,
|
||||
okDays,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
import { PrismaClient, ContractTaskStatus } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export interface ContractTaskFilters {
|
||||
contractId: number;
|
||||
status?: ContractTaskStatus;
|
||||
visibleInPortal?: boolean;
|
||||
// Für Kundenportal: Zeige Tasks die entweder sichtbar sind ODER vom Kunden erstellt wurden
|
||||
customerPortalEmails?: string[];
|
||||
}
|
||||
|
||||
export async function getTasksByContract(filters: ContractTaskFilters) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const where: any = {
|
||||
contractId: filters.contractId,
|
||||
};
|
||||
|
||||
if (filters.status) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
|
||||
// Spezielle Logik für Kundenportal
|
||||
if (filters.customerPortalEmails && filters.customerPortalEmails.length > 0) {
|
||||
// Zeige Tasks die:
|
||||
// 1. visibleInPortal = true ODER
|
||||
// 2. vom Kunden selbst erstellt wurden (createdBy in customerPortalEmails)
|
||||
where.OR = [
|
||||
{ visibleInPortal: true },
|
||||
{ createdBy: { in: filters.customerPortalEmails } },
|
||||
];
|
||||
} else if (filters.visibleInPortal !== undefined) {
|
||||
where.visibleInPortal = filters.visibleInPortal;
|
||||
}
|
||||
|
||||
return prisma.contractTask.findMany({
|
||||
where,
|
||||
include: {
|
||||
subtasks: {
|
||||
orderBy: [
|
||||
{ status: 'asc' },
|
||||
{ createdAt: 'asc' },
|
||||
],
|
||||
},
|
||||
},
|
||||
orderBy: [
|
||||
{ status: 'asc' }, // OPEN first, then COMPLETED
|
||||
{ createdAt: 'desc' },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export async function getTaskById(id: number) {
|
||||
return prisma.contractTask.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
export async function createTask(data: {
|
||||
contractId: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
visibleInPortal?: boolean;
|
||||
createdBy?: string;
|
||||
}) {
|
||||
return prisma.contractTask.create({
|
||||
data: {
|
||||
contractId: data.contractId,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
visibleInPortal: data.visibleInPortal ?? false,
|
||||
createdBy: data.createdBy,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateTask(
|
||||
id: number,
|
||||
data: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
visibleInPortal?: boolean;
|
||||
}
|
||||
) {
|
||||
return prisma.contractTask.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function completeTask(id: number) {
|
||||
return prisma.contractTask.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'COMPLETED',
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function reopenTask(id: number) {
|
||||
return prisma.contractTask.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'OPEN',
|
||||
completedAt: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteTask(id: number) {
|
||||
return prisma.contractTask.delete({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== SUBTASKS ====================
|
||||
|
||||
export async function createSubtask(data: { taskId: number; title: string; createdBy?: string }) {
|
||||
return prisma.contractTaskSubtask.create({
|
||||
data: {
|
||||
taskId: data.taskId,
|
||||
title: data.title,
|
||||
createdBy: data.createdBy,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateSubtask(id: number, data: { title?: string }) {
|
||||
return prisma.contractTaskSubtask.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function completeSubtask(id: number) {
|
||||
// Complete the subtask
|
||||
const subtask = await prisma.contractTaskSubtask.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'COMPLETED',
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// Check if all subtasks of the parent task are now completed
|
||||
const remainingOpenSubtasks = await prisma.contractTaskSubtask.count({
|
||||
where: {
|
||||
taskId: subtask.taskId,
|
||||
status: 'OPEN',
|
||||
},
|
||||
});
|
||||
|
||||
// If no open subtasks remain, automatically complete the parent task
|
||||
if (remainingOpenSubtasks === 0) {
|
||||
await prisma.contractTask.update({
|
||||
where: { id: subtask.taskId },
|
||||
data: {
|
||||
status: 'COMPLETED',
|
||||
completedAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return subtask;
|
||||
}
|
||||
|
||||
export async function reopenSubtask(id: number) {
|
||||
// Reopen the subtask
|
||||
const subtask = await prisma.contractTaskSubtask.update({
|
||||
where: { id },
|
||||
data: {
|
||||
status: 'OPEN',
|
||||
completedAt: null,
|
||||
},
|
||||
});
|
||||
|
||||
// If the parent task was completed, reopen it as well
|
||||
const parentTask = await prisma.contractTask.findUnique({
|
||||
where: { id: subtask.taskId },
|
||||
});
|
||||
|
||||
if (parentTask?.status === 'COMPLETED') {
|
||||
await prisma.contractTask.update({
|
||||
where: { id: subtask.taskId },
|
||||
data: {
|
||||
status: 'OPEN',
|
||||
completedAt: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return subtask;
|
||||
}
|
||||
|
||||
export async function deleteSubtask(id: number) {
|
||||
return prisma.contractTaskSubtask.delete({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSubtaskById(id: number) {
|
||||
return prisma.contractTaskSubtask.findUnique({
|
||||
where: { id },
|
||||
include: { task: true },
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== ALL TASKS ====================
|
||||
|
||||
export interface AllTasksFilters {
|
||||
status?: ContractTaskStatus;
|
||||
customerId?: number;
|
||||
// Für Kundenportal: Nur Tasks für erlaubte Verträge und sichtbare/eigene Tasks
|
||||
customerPortalCustomerIds?: number[];
|
||||
customerPortalEmails?: string[];
|
||||
}
|
||||
|
||||
export async function getAllTasks(filters: AllTasksFilters) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const where: any = {};
|
||||
|
||||
if (filters.status) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
|
||||
// Für Kundenportal: Filter auf erlaubte Verträge + Sichtbarkeit
|
||||
if (filters.customerPortalCustomerIds && filters.customerPortalCustomerIds.length > 0) {
|
||||
where.contract = {
|
||||
customerId: { in: filters.customerPortalCustomerIds },
|
||||
};
|
||||
// Zeige nur sichtbare Tasks ODER vom Kunden erstellte
|
||||
if (filters.customerPortalEmails && filters.customerPortalEmails.length > 0) {
|
||||
where.OR = [
|
||||
{ visibleInPortal: true },
|
||||
{ createdBy: { in: filters.customerPortalEmails } },
|
||||
];
|
||||
} else {
|
||||
where.visibleInPortal = true;
|
||||
}
|
||||
} else if (filters.customerId) {
|
||||
// Für Mitarbeiter: Optional nach Kunde filtern
|
||||
where.contract = {
|
||||
customerId: filters.customerId,
|
||||
};
|
||||
}
|
||||
|
||||
return prisma.contractTask.findMany({
|
||||
where,
|
||||
include: {
|
||||
subtasks: {
|
||||
orderBy: [
|
||||
{ status: 'asc' },
|
||||
{ createdAt: 'asc' },
|
||||
],
|
||||
},
|
||||
contract: {
|
||||
select: {
|
||||
id: true,
|
||||
contractNumber: true,
|
||||
customerId: true,
|
||||
customer: {
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
companyName: true,
|
||||
customerNumber: true,
|
||||
},
|
||||
},
|
||||
provider: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
tariff: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
providerName: true,
|
||||
tariffName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [
|
||||
{ status: 'asc' }, // OPEN first, then COMPLETED
|
||||
{ createdAt: 'desc' },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export async function getTaskStats(filters: AllTasksFilters) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const where: any = {
|
||||
status: 'OPEN',
|
||||
};
|
||||
|
||||
// Für Kundenportal: Filter auf erlaubte Verträge + Sichtbarkeit
|
||||
if (filters.customerPortalCustomerIds && filters.customerPortalCustomerIds.length > 0) {
|
||||
where.contract = {
|
||||
customerId: { in: filters.customerPortalCustomerIds },
|
||||
};
|
||||
// Zeige nur sichtbare Tasks ODER vom Kunden erstellte
|
||||
if (filters.customerPortalEmails && filters.customerPortalEmails.length > 0) {
|
||||
where.OR = [
|
||||
{ visibleInPortal: true },
|
||||
{ createdBy: { in: filters.customerPortalEmails } },
|
||||
];
|
||||
} else {
|
||||
where.visibleInPortal = true;
|
||||
}
|
||||
} else if (filters.customerId) {
|
||||
where.contract = {
|
||||
customerId: filters.customerId,
|
||||
};
|
||||
}
|
||||
|
||||
const openCount = await prisma.contractTask.count({ where });
|
||||
|
||||
return { openCount };
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
// ==================== EMAIL PROVIDER SERVICE ====================
|
||||
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { decrypt } from '../../utils/encryption.js';
|
||||
import {
|
||||
IEmailProvider,
|
||||
EmailProviderConfig,
|
||||
EmailExistsResult,
|
||||
EmailOperationResult,
|
||||
CreateEmailParams,
|
||||
} from './types.js';
|
||||
import { PleskEmailProvider } from './pleskProvider.js';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
// Factory-Funktion um den richtigen Provider zu erstellen
|
||||
function createProvider(config: EmailProviderConfig): IEmailProvider {
|
||||
switch (config.type) {
|
||||
case 'PLESK':
|
||||
return new PleskEmailProvider(config);
|
||||
case 'CPANEL':
|
||||
// TODO: cPanel Provider implementieren
|
||||
throw new Error('cPanel Provider noch nicht implementiert');
|
||||
case 'DIRECTADMIN':
|
||||
// TODO: DirectAdmin Provider implementieren
|
||||
throw new Error('DirectAdmin Provider noch nicht implementiert');
|
||||
default:
|
||||
throw new Error(`Unbekannter Provider-Typ: ${config.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== CONFIG CRUD ====================
|
||||
|
||||
export async function getAllProviderConfigs() {
|
||||
return prisma.emailProviderConfig.findMany({
|
||||
orderBy: [{ isDefault: 'desc' }, { name: 'asc' }],
|
||||
});
|
||||
}
|
||||
|
||||
export async function getProviderConfigById(id: number) {
|
||||
return prisma.emailProviderConfig.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getDefaultProviderConfig() {
|
||||
return prisma.emailProviderConfig.findFirst({
|
||||
where: { isActive: true, isDefault: true },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getActiveProviderConfig() {
|
||||
// Erst Default-Provider versuchen, dann irgendeinen aktiven
|
||||
const defaultProvider = await getDefaultProviderConfig();
|
||||
if (defaultProvider) return defaultProvider;
|
||||
|
||||
return prisma.emailProviderConfig.findFirst({
|
||||
where: { isActive: true },
|
||||
});
|
||||
}
|
||||
|
||||
export interface CreateProviderConfigData {
|
||||
name: string;
|
||||
type: 'PLESK' | 'CPANEL' | 'DIRECTADMIN';
|
||||
apiUrl: string;
|
||||
apiKey?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
domain: string;
|
||||
defaultForwardEmail?: string;
|
||||
isActive?: boolean;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
export async function createProviderConfig(data: CreateProviderConfigData) {
|
||||
// Falls isDefault=true, alle anderen auf false setzen
|
||||
if (data.isDefault) {
|
||||
await prisma.emailProviderConfig.updateMany({
|
||||
where: { isDefault: true },
|
||||
data: { isDefault: false },
|
||||
});
|
||||
}
|
||||
|
||||
// Passwort verschlüsseln falls vorhanden
|
||||
const { encrypt } = await import('../../utils/encryption.js');
|
||||
const passwordEncrypted = data.password ? encrypt(data.password) : null;
|
||||
|
||||
return prisma.emailProviderConfig.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
type: data.type,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey || null,
|
||||
username: data.username || null,
|
||||
passwordEncrypted,
|
||||
domain: data.domain,
|
||||
defaultForwardEmail: data.defaultForwardEmail || null,
|
||||
isActive: data.isActive ?? true,
|
||||
isDefault: data.isDefault ?? false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateProviderConfig(
|
||||
id: number,
|
||||
data: Partial<CreateProviderConfigData>
|
||||
) {
|
||||
// Falls isDefault=true, alle anderen auf false setzen
|
||||
if (data.isDefault) {
|
||||
await prisma.emailProviderConfig.updateMany({
|
||||
where: { isDefault: true, id: { not: id } },
|
||||
data: { isDefault: false },
|
||||
});
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
|
||||
if (data.name !== undefined) updateData.name = data.name;
|
||||
if (data.type !== undefined) updateData.type = data.type;
|
||||
if (data.apiUrl !== undefined) updateData.apiUrl = data.apiUrl;
|
||||
if (data.apiKey !== undefined) updateData.apiKey = data.apiKey || null;
|
||||
if (data.username !== undefined) updateData.username = data.username || null;
|
||||
if (data.domain !== undefined) updateData.domain = data.domain;
|
||||
if (data.defaultForwardEmail !== undefined)
|
||||
updateData.defaultForwardEmail = data.defaultForwardEmail || null;
|
||||
if (data.isActive !== undefined) updateData.isActive = data.isActive;
|
||||
if (data.isDefault !== undefined) updateData.isDefault = data.isDefault;
|
||||
|
||||
// Passwort-Logik:
|
||||
// - Wenn neues Passwort übergeben → verschlüsseln und speichern
|
||||
// - Wenn Benutzername gelöscht wird → Passwort auch löschen (gehören zusammen)
|
||||
if (data.password) {
|
||||
const { encrypt } = await import('../../utils/encryption.js');
|
||||
updateData.passwordEncrypted = encrypt(data.password);
|
||||
} else if (data.username !== undefined && !data.username) {
|
||||
// Benutzername wird gelöscht → Passwort auch löschen
|
||||
updateData.passwordEncrypted = null;
|
||||
}
|
||||
|
||||
return prisma.emailProviderConfig.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteProviderConfig(id: number) {
|
||||
return prisma.emailProviderConfig.delete({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== EMAIL OPERATIONS ====================
|
||||
|
||||
// Provider-Instanz aus DB-Config erstellen
|
||||
async function getProviderInstance(): Promise<IEmailProvider> {
|
||||
const dbConfig = await getActiveProviderConfig();
|
||||
|
||||
if (!dbConfig) {
|
||||
throw new Error('Kein aktiver Email-Provider konfiguriert');
|
||||
}
|
||||
|
||||
// Passwort entschlüsseln
|
||||
let password: string | undefined;
|
||||
if (dbConfig.passwordEncrypted) {
|
||||
try {
|
||||
password = decrypt(dbConfig.passwordEncrypted);
|
||||
} catch {
|
||||
console.error('Konnte Passwort nicht entschlüsseln');
|
||||
}
|
||||
}
|
||||
|
||||
const config: EmailProviderConfig = {
|
||||
id: dbConfig.id,
|
||||
name: dbConfig.name,
|
||||
type: dbConfig.type as 'PLESK' | 'CPANEL' | 'DIRECTADMIN',
|
||||
apiUrl: dbConfig.apiUrl,
|
||||
apiKey: dbConfig.apiKey || undefined,
|
||||
username: dbConfig.username || undefined,
|
||||
password,
|
||||
domain: dbConfig.domain,
|
||||
defaultForwardEmail: dbConfig.defaultForwardEmail || undefined,
|
||||
isActive: dbConfig.isActive,
|
||||
isDefault: dbConfig.isDefault,
|
||||
};
|
||||
|
||||
return createProvider(config);
|
||||
}
|
||||
|
||||
// Prüfen ob eine E-Mail existiert
|
||||
export async function checkEmailExists(localPart: string): Promise<EmailExistsResult> {
|
||||
try {
|
||||
const provider = await getProviderInstance();
|
||||
return provider.emailExists(localPart);
|
||||
} catch (error) {
|
||||
console.error('checkEmailExists error:', error);
|
||||
return { exists: false };
|
||||
}
|
||||
}
|
||||
|
||||
// E-Mail erstellen mit Weiterleitungen
|
||||
export async function provisionEmail(
|
||||
localPart: string,
|
||||
customerEmail: string
|
||||
): Promise<EmailOperationResult> {
|
||||
try {
|
||||
const provider = await getProviderInstance();
|
||||
const config = await getActiveProviderConfig();
|
||||
|
||||
// Weiterleitungsziele zusammenstellen
|
||||
const forwardTargets: string[] = [customerEmail];
|
||||
|
||||
// Unsere eigene Weiterleitungsadresse hinzufügen falls konfiguriert
|
||||
if (config?.defaultForwardEmail) {
|
||||
forwardTargets.push(config.defaultForwardEmail);
|
||||
}
|
||||
|
||||
// Prüfen ob existiert
|
||||
const exists = await provider.emailExists(localPart);
|
||||
if (exists.exists) {
|
||||
return {
|
||||
success: true,
|
||||
message: `E-Mail ${exists.email} existiert bereits`,
|
||||
};
|
||||
}
|
||||
|
||||
// Erstellen
|
||||
const result = await provider.createEmail({
|
||||
localPart,
|
||||
forwardTargets,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// E-Mail löschen
|
||||
export async function deprovisionEmail(localPart: string): Promise<EmailOperationResult> {
|
||||
try {
|
||||
const provider = await getProviderInstance();
|
||||
return provider.deleteEmail(localPart);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// E-Mail umbenennen
|
||||
export async function renameProvisionedEmail(
|
||||
oldLocalPart: string,
|
||||
newLocalPart: string
|
||||
): Promise<EmailOperationResult> {
|
||||
try {
|
||||
const provider = await getProviderInstance();
|
||||
return provider.renameEmail({ oldLocalPart, newLocalPart });
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Domain aus aktivem Provider holen
|
||||
export async function getProviderDomain(): Promise<string | null> {
|
||||
const config = await getActiveProviderConfig();
|
||||
return config?.domain || null;
|
||||
}
|
||||
|
||||
// Provider-Instanz aus übergebener Config erstellen (für Tests mit ungespeicherten Daten)
|
||||
function createProviderFromFormData(data: {
|
||||
type: 'PLESK' | 'CPANEL' | 'DIRECTADMIN';
|
||||
apiUrl: string;
|
||||
apiKey?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
domain: string;
|
||||
}): IEmailProvider {
|
||||
const config: EmailProviderConfig = {
|
||||
id: 0,
|
||||
name: 'Test',
|
||||
type: data.type,
|
||||
apiUrl: data.apiUrl,
|
||||
apiKey: data.apiKey,
|
||||
username: data.username,
|
||||
password: data.password,
|
||||
domain: data.domain,
|
||||
isActive: true,
|
||||
isDefault: false,
|
||||
};
|
||||
return createProvider(config);
|
||||
}
|
||||
|
||||
// Provider-Instanz aus DB-Config per ID erstellen
|
||||
async function getProviderInstanceById(id: number): Promise<IEmailProvider> {
|
||||
const dbConfig = await getProviderConfigById(id);
|
||||
|
||||
if (!dbConfig) {
|
||||
throw new Error('Email-Provider nicht gefunden');
|
||||
}
|
||||
|
||||
// Passwort entschlüsseln
|
||||
let password: string | undefined;
|
||||
if (dbConfig.passwordEncrypted) {
|
||||
try {
|
||||
password = decrypt(dbConfig.passwordEncrypted);
|
||||
} catch {
|
||||
console.error('Konnte Passwort nicht entschlüsseln');
|
||||
}
|
||||
}
|
||||
|
||||
const config: EmailProviderConfig = {
|
||||
id: dbConfig.id,
|
||||
name: dbConfig.name,
|
||||
type: dbConfig.type as 'PLESK' | 'CPANEL' | 'DIRECTADMIN',
|
||||
apiUrl: dbConfig.apiUrl,
|
||||
apiKey: dbConfig.apiKey || undefined,
|
||||
username: dbConfig.username || undefined,
|
||||
password,
|
||||
domain: dbConfig.domain,
|
||||
defaultForwardEmail: dbConfig.defaultForwardEmail || undefined,
|
||||
isActive: dbConfig.isActive,
|
||||
isDefault: dbConfig.isDefault,
|
||||
};
|
||||
|
||||
return createProvider(config);
|
||||
}
|
||||
|
||||
// Provider-Verbindung testen (mit ID, Formulardaten oder Default-Provider)
|
||||
export async function testProviderConnection(options?: {
|
||||
id?: number;
|
||||
testData?: {
|
||||
type: 'PLESK' | 'CPANEL' | 'DIRECTADMIN';
|
||||
apiUrl: string;
|
||||
apiKey?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
domain: string;
|
||||
};
|
||||
}): Promise<EmailOperationResult> {
|
||||
try {
|
||||
let provider: IEmailProvider;
|
||||
|
||||
if (options?.testData) {
|
||||
// Mit übergebenen Daten testen (z.B. aus Modal beim Neuanlegen)
|
||||
provider = createProviderFromFormData(options.testData);
|
||||
} else if (options?.id) {
|
||||
// Gespeicherten Provider per ID testen
|
||||
provider = await getProviderInstanceById(options.id);
|
||||
} else {
|
||||
// Default-Provider testen
|
||||
provider = await getProviderInstance();
|
||||
}
|
||||
|
||||
// Expliziter Verbindungstest (wirft Fehler bei Auth-Problemen)
|
||||
await provider.testConnection();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Verbindung zum Email-Provider erfolgreich',
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// ==================== EMAIL PROVIDER EXPORTS ====================
|
||||
|
||||
export * from './types.js';
|
||||
export * from './emailProviderService.js';
|
||||
export { PleskEmailProvider } from './pleskProvider.js';
|
||||
@@ -0,0 +1,348 @@
|
||||
// ==================== PLESK EMAIL PROVIDER ====================
|
||||
|
||||
import { Agent, fetch as undiciFetch } from 'undici';
|
||||
import {
|
||||
IEmailProvider,
|
||||
EmailProviderConfig,
|
||||
EmailExistsResult,
|
||||
EmailOperationResult,
|
||||
CreateEmailParams,
|
||||
RenameEmailParams,
|
||||
} from './types.js';
|
||||
|
||||
// Undici-Agent der selbstsignierte Zertifikate akzeptiert
|
||||
// Mit Timeouts und Connection-Limits um Probleme zu vermeiden
|
||||
const httpsAgent = new Agent({
|
||||
connect: {
|
||||
rejectUnauthorized: false,
|
||||
timeout: 10000, // 10 Sekunden Connect-Timeout
|
||||
},
|
||||
bodyTimeout: 30000, // 30 Sekunden für Response-Body
|
||||
headersTimeout: 30000, // 30 Sekunden für Headers
|
||||
keepAliveTimeout: 1000, // Connections nach 1 Sekunde schließen
|
||||
keepAliveMaxTimeout: 5000, // Maximal 5 Sekunden Keep-Alive
|
||||
connections: 1, // Nur eine Connection gleichzeitig pro Host
|
||||
pipelining: 1, // Kein Pipelining
|
||||
});
|
||||
|
||||
export class PleskEmailProvider implements IEmailProvider {
|
||||
readonly type = 'PLESK';
|
||||
private config: EmailProviderConfig;
|
||||
|
||||
constructor(config: EmailProviderConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
// Basis-URL für API-Requests
|
||||
private get baseUrl(): string {
|
||||
// Entferne trailing slash falls vorhanden
|
||||
return this.config.apiUrl.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
// HTTP-Request an Plesk API senden
|
||||
private async request<T>(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
||||
endpoint: string,
|
||||
data?: Record<string, unknown>
|
||||
): Promise<T> {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
};
|
||||
|
||||
// Authentifizierung: API-Key hat Priorität, sonst Basic Auth
|
||||
if (this.config.apiKey) {
|
||||
// Nur API-Key verwenden (ohne Basic Auth)
|
||||
headers['X-API-Key'] = this.config.apiKey;
|
||||
} else if (this.config.username && this.config.password) {
|
||||
// Basic Auth nur wenn kein API-Key
|
||||
const authHeader = Buffer.from(
|
||||
`${this.config.username}:${this.config.password}`
|
||||
).toString('base64');
|
||||
headers['Authorization'] = `Basic ${authHeader}`;
|
||||
} else {
|
||||
// Keine Authentifizierung vorhanden
|
||||
throw new Error('Keine Zugangsdaten angegeben - bitte API-Key oder Benutzername/Passwort eingeben');
|
||||
}
|
||||
|
||||
const options: Parameters<typeof undiciFetch>[1] = {
|
||||
method,
|
||||
headers,
|
||||
dispatcher: httpsAgent,
|
||||
};
|
||||
|
||||
if (data && (method === 'POST' || method === 'PUT')) {
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await undiciFetch(url, options);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Plesk API Fehler: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
// Leere Response bei DELETE
|
||||
if (response.status === 204) {
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
return await response.json() as T;
|
||||
} catch (error) {
|
||||
// Verbesserte Fehlermeldungen für häufige Probleme
|
||||
if (error instanceof Error) {
|
||||
const msg = error.message.toLowerCase();
|
||||
|
||||
// Netzwerkfehler
|
||||
if (msg.includes('econnrefused')) {
|
||||
throw new Error(`Server nicht erreichbar unter ${this.baseUrl} - Ist der Server gestartet?`);
|
||||
}
|
||||
if (msg.includes('enotfound') || msg.includes('getaddrinfo')) {
|
||||
throw new Error(`Server-Adresse nicht gefunden: ${this.baseUrl} - Bitte URL prüfen`);
|
||||
}
|
||||
if (msg.includes('etimedout') || msg.includes('timeout')) {
|
||||
throw new Error(`Zeitüberschreitung bei Verbindung zu ${this.baseUrl}`);
|
||||
}
|
||||
if (msg.includes('econnreset')) {
|
||||
throw new Error(`Verbindung wurde vom Server abgebrochen`);
|
||||
}
|
||||
|
||||
// SSL/TLS Fehler
|
||||
if (msg.includes('cert') || msg.includes('ssl') || msg.includes('tls') || msg.includes('unable_to_verify')) {
|
||||
throw new Error(`SSL-Zertifikatsfehler - Selbstsigniertes Zertifikat wird nicht akzeptiert`);
|
||||
}
|
||||
|
||||
// fetch failed ist meist ein Netzwerk/SSL Problem
|
||||
if (msg.includes('fetch failed')) {
|
||||
throw new Error(`Verbindung fehlgeschlagen zu ${this.baseUrl} - Bitte prüfen: Server erreichbar? HTTPS-Port korrekt?`);
|
||||
}
|
||||
}
|
||||
|
||||
console.error('Plesk API Request failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async testConnection(): Promise<void> {
|
||||
// Versuche Server-Info abzurufen - wirft Fehler bei Auth-Problemen
|
||||
try {
|
||||
await this.request('GET', '/api/v2/server');
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
// Verbesserte Fehlermeldung
|
||||
if (error.message.includes('401')) {
|
||||
throw new Error('Authentifizierung fehlgeschlagen - Benutzername/Passwort oder API-Key prüfen');
|
||||
}
|
||||
if (error.message.includes('403')) {
|
||||
throw new Error('Zugriff verweigert - Berechtigungen prüfen');
|
||||
}
|
||||
// Andere Fehler wurden schon in request() übersetzt
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async emailExists(localPart: string): Promise<EmailExistsResult> {
|
||||
const email = `${localPart}@${this.config.domain}`;
|
||||
|
||||
try {
|
||||
// Plesk CLI API: Mail-Info abfragen
|
||||
const result = await this.request<{ code: number; stdout: string; stderr: string }>(
|
||||
'POST',
|
||||
'/api/v2/cli/mail/call',
|
||||
{ params: ['--info', email] }
|
||||
);
|
||||
|
||||
// Debug: Response-Struktur loggen
|
||||
console.log('Plesk emailExists response:', JSON.stringify(result, null, 2));
|
||||
|
||||
// Plesk gibt code=0 bei Erfolg, code!=0 bei Fehler
|
||||
// stderr enthält Fehlermeldung wenn Mail nicht existiert
|
||||
const hasError = result.code !== 0 ||
|
||||
result.stderr?.toLowerCase().includes('not found') ||
|
||||
result.stderr?.toLowerCase().includes('does not exist') ||
|
||||
result.stderr?.toLowerCase().includes('unable to find') ||
|
||||
result.stderr?.toLowerCase().includes('no such');
|
||||
|
||||
if (hasError) {
|
||||
return { exists: false };
|
||||
}
|
||||
|
||||
// stdout sollte die Mail-Infos enthalten
|
||||
const exists = result.stdout?.toLowerCase().includes(localPart.toLowerCase());
|
||||
return {
|
||||
exists,
|
||||
email: exists ? email : undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
// HTTP-Fehler oder Netzwerkfehler
|
||||
if (error instanceof Error) {
|
||||
const msg = error.message.toLowerCase();
|
||||
// "not found" = Mail gibt es nicht
|
||||
if (msg.includes('not found') || msg.includes('does not exist') || msg.includes('unable to find')) {
|
||||
return { exists: false };
|
||||
}
|
||||
}
|
||||
console.error('Plesk emailExists error:', error);
|
||||
return { exists: false };
|
||||
}
|
||||
}
|
||||
|
||||
async createEmail(params: CreateEmailParams): Promise<EmailOperationResult> {
|
||||
const { localPart, forwardTargets } = params;
|
||||
const email = `${localPart}@${this.config.domain}`;
|
||||
|
||||
try {
|
||||
// Prüfen ob schon existiert
|
||||
const exists = await this.emailExists(localPart);
|
||||
if (exists.exists) {
|
||||
return {
|
||||
success: false,
|
||||
error: `E-Mail ${email} existiert bereits`,
|
||||
};
|
||||
}
|
||||
|
||||
// Plesk CLI API: Mail-Account mit Weiterleitung erstellen
|
||||
// Verwendet den CLI-Wrapper unter /api/v2/cli/mail/call
|
||||
// Format für -forwarding-addresses: "add:email1,email2" oder "set:email1,email2"
|
||||
await this.request('POST', '/api/v2/cli/mail/call', {
|
||||
params: [
|
||||
'--create', email,
|
||||
'-forwarding', 'true',
|
||||
'-forwarding-addresses', `add:${forwardTargets.join(',')}`,
|
||||
'-mailbox', 'false',
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `E-Mail ${email} erfolgreich erstellt mit Weiterleitung an: ${forwardTargets.join(', ')}`,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
console.error('Plesk createEmail error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: `Fehler beim Erstellen der E-Mail: ${errorMessage}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async deleteEmail(localPart: string): Promise<EmailOperationResult> {
|
||||
const email = `${localPart}@${this.config.domain}`;
|
||||
|
||||
try {
|
||||
// Prüfen ob Mail existiert
|
||||
const exists = await this.emailExists(localPart);
|
||||
if (!exists.exists) {
|
||||
return {
|
||||
success: false,
|
||||
error: `E-Mail ${email} nicht gefunden`,
|
||||
};
|
||||
}
|
||||
|
||||
// Plesk CLI API: Mail-Account löschen
|
||||
await this.request('POST', '/api/v2/cli/mail/call', {
|
||||
params: ['--remove', email],
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `E-Mail ${email} erfolgreich gelöscht`,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
console.error('Plesk deleteEmail error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: `Fehler beim Löschen der E-Mail: ${errorMessage}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async renameEmail(params: RenameEmailParams): Promise<EmailOperationResult> {
|
||||
const { oldLocalPart, newLocalPart } = params;
|
||||
const oldEmail = `${oldLocalPart}@${this.config.domain}`;
|
||||
const newEmail = `${newLocalPart}@${this.config.domain}`;
|
||||
|
||||
try {
|
||||
// Prüfen ob alte Mail existiert
|
||||
const oldExists = await this.emailExists(oldLocalPart);
|
||||
if (!oldExists.exists) {
|
||||
return {
|
||||
success: false,
|
||||
error: `E-Mail ${oldEmail} nicht gefunden`,
|
||||
};
|
||||
}
|
||||
|
||||
// Prüfen ob neue Adresse schon existiert
|
||||
const newExists = await this.emailExists(newLocalPart);
|
||||
if (newExists.exists) {
|
||||
return {
|
||||
success: false,
|
||||
error: `E-Mail ${newEmail} existiert bereits`,
|
||||
};
|
||||
}
|
||||
|
||||
// Plesk CLI API: Mail-Account umbenennen
|
||||
await this.request('POST', '/api/v2/cli/mail/call', {
|
||||
params: ['--rename', oldEmail, '-new-name', newLocalPart],
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `E-Mail erfolgreich umbenannt von ${oldEmail} zu ${newEmail}`,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
console.error('Plesk renameEmail error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: `Fehler beim Umbenennen der E-Mail: ${errorMessage}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async updateForwardTargets(
|
||||
localPart: string,
|
||||
targets: string[]
|
||||
): Promise<EmailOperationResult> {
|
||||
const email = `${localPart}@${this.config.domain}`;
|
||||
|
||||
try {
|
||||
// Prüfen ob Mail existiert
|
||||
const exists = await this.emailExists(localPart);
|
||||
if (!exists.exists) {
|
||||
return {
|
||||
success: false,
|
||||
error: `E-Mail ${email} nicht gefunden`,
|
||||
};
|
||||
}
|
||||
|
||||
// Plesk CLI API: Weiterleitungsziele aktualisieren
|
||||
// Format für -forwarding-addresses: "set:email1,email2" ersetzt alle Adressen
|
||||
await this.request('POST', '/api/v2/cli/mail/call', {
|
||||
params: [
|
||||
'--update', email,
|
||||
'-forwarding', 'true',
|
||||
'-forwarding-addresses', `set:${targets.join(',')}`,
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Weiterleitungen für ${email} aktualisiert: ${targets.join(', ')}`,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
console.error('Plesk updateForwardTargets error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: `Fehler beim Aktualisieren der Weiterleitungen: ${errorMessage}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// ==================== EMAIL PROVIDER TYPES ====================
|
||||
|
||||
export interface EmailForwardTarget {
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface CreateEmailParams {
|
||||
localPart: string; // z.B. "max.mustermann"
|
||||
forwardTargets: string[]; // Weiterleitungsziele
|
||||
}
|
||||
|
||||
export interface RenameEmailParams {
|
||||
oldLocalPart: string;
|
||||
newLocalPart: string;
|
||||
}
|
||||
|
||||
export interface EmailExistsResult {
|
||||
exists: boolean;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export interface EmailOperationResult {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Interface das alle Email-Provider implementieren müssen
|
||||
export interface IEmailProvider {
|
||||
// Provider-Typ (z.B. 'PLESK', 'CPANEL')
|
||||
readonly type: string;
|
||||
|
||||
// Testet die Verbindung zum Provider (wirft Fehler bei Fehlschlag)
|
||||
testConnection(): Promise<void>;
|
||||
|
||||
// Prüft ob eine E-Mail-Adresse existiert
|
||||
emailExists(localPart: string): Promise<EmailExistsResult>;
|
||||
|
||||
// Erstellt eine neue E-Mail-Weiterleitung
|
||||
createEmail(params: CreateEmailParams): Promise<EmailOperationResult>;
|
||||
|
||||
// Löscht eine E-Mail-Adresse
|
||||
deleteEmail(localPart: string): Promise<EmailOperationResult>;
|
||||
|
||||
// Benennt eine E-Mail-Adresse um
|
||||
renameEmail(params: RenameEmailParams): Promise<EmailOperationResult>;
|
||||
|
||||
// Aktualisiert die Weiterleitungsziele
|
||||
updateForwardTargets(localPart: string, targets: string[]): Promise<EmailOperationResult>;
|
||||
}
|
||||
|
||||
// Konfiguration für Provider (aus DB)
|
||||
export interface EmailProviderConfig {
|
||||
id: number;
|
||||
name: string;
|
||||
type: 'PLESK' | 'CPANEL' | 'DIRECTADMIN';
|
||||
apiUrl: string;
|
||||
apiKey?: string;
|
||||
username?: string;
|
||||
password?: string; // Entschlüsselt
|
||||
domain: string;
|
||||
defaultForwardEmail?: string;
|
||||
isActive: boolean;
|
||||
isDefault: boolean;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function getAllPlatforms(includeInactive = false) {
|
||||
const where = includeInactive ? {} : { isActive: true };
|
||||
return prisma.salesPlatform.findMany({
|
||||
where,
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPlatformById(id: number) {
|
||||
return prisma.salesPlatform.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
_count: {
|
||||
select: { contracts: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function createPlatform(data: {
|
||||
name: string;
|
||||
contactInfo?: string;
|
||||
}) {
|
||||
return prisma.salesPlatform.create({
|
||||
data: {
|
||||
...data,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updatePlatform(
|
||||
id: number,
|
||||
data: {
|
||||
name?: string;
|
||||
contactInfo?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
) {
|
||||
return prisma.salesPlatform.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deletePlatform(id: number) {
|
||||
// Check if platform is used by any contracts
|
||||
const count = await prisma.contract.count({
|
||||
where: { salesPlatformId: id },
|
||||
});
|
||||
|
||||
if (count > 0) {
|
||||
throw new Error(
|
||||
`Plattform kann nicht gelöscht werden, da sie von ${count} Verträgen verwendet wird`
|
||||
);
|
||||
}
|
||||
|
||||
return prisma.salesPlatform.delete({ where: { id } });
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function getAllProviders(includeInactive = false) {
|
||||
const where = includeInactive ? {} : { isActive: true };
|
||||
return prisma.provider.findMany({
|
||||
where,
|
||||
orderBy: { name: 'asc' },
|
||||
include: {
|
||||
tariffs: {
|
||||
where: includeInactive ? {} : { isActive: true },
|
||||
orderBy: { name: 'asc' },
|
||||
},
|
||||
_count: {
|
||||
select: { contracts: true, tariffs: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getProviderById(id: number) {
|
||||
return prisma.provider.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
tariffs: {
|
||||
orderBy: { name: 'asc' },
|
||||
},
|
||||
_count: {
|
||||
select: { contracts: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function createProvider(data: {
|
||||
name: string;
|
||||
portalUrl?: string;
|
||||
usernameFieldName?: string;
|
||||
passwordFieldName?: string;
|
||||
}) {
|
||||
return prisma.provider.create({
|
||||
data: {
|
||||
...data,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateProvider(
|
||||
id: number,
|
||||
data: {
|
||||
name?: string;
|
||||
portalUrl?: string;
|
||||
usernameFieldName?: string;
|
||||
passwordFieldName?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
) {
|
||||
return prisma.provider.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteProvider(id: number) {
|
||||
// Check if provider is used by any contracts
|
||||
const count = await prisma.contract.count({
|
||||
where: { providerId: id },
|
||||
});
|
||||
|
||||
if (count > 0) {
|
||||
throw new Error(
|
||||
`Anbieter kann nicht gelöscht werden, da er von ${count} Verträgen verwendet wird`
|
||||
);
|
||||
}
|
||||
|
||||
return prisma.provider.delete({ where: { id } });
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function getEmailsByCustomerId(customerId: number, includeInactive = false) {
|
||||
const where: Record<string, unknown> = { customerId };
|
||||
if (!includeInactive) {
|
||||
where.isActive = true;
|
||||
}
|
||||
return prisma.stressfreiEmail.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getEmailById(id: number) {
|
||||
return prisma.stressfreiEmail.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
export async function createEmail(data: {
|
||||
customerId: number;
|
||||
email: string;
|
||||
platform?: string;
|
||||
notes?: string;
|
||||
}) {
|
||||
return prisma.stressfreiEmail.create({
|
||||
data: {
|
||||
...data,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateEmail(
|
||||
id: number,
|
||||
data: {
|
||||
email?: string;
|
||||
platform?: string;
|
||||
notes?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
) {
|
||||
return prisma.stressfreiEmail.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteEmail(id: number) {
|
||||
return prisma.stressfreiEmail.delete({ where: { id } });
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function getTariffsByProvider(providerId: number, includeInactive = false) {
|
||||
const where: { providerId: number; isActive?: boolean } = { providerId };
|
||||
if (!includeInactive) {
|
||||
where.isActive = true;
|
||||
}
|
||||
return prisma.tariff.findMany({
|
||||
where,
|
||||
orderBy: { name: 'asc' },
|
||||
include: {
|
||||
_count: {
|
||||
select: { contracts: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getTariffById(id: number) {
|
||||
return prisma.tariff.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
provider: true,
|
||||
_count: {
|
||||
select: { contracts: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function createTariff(data: {
|
||||
providerId: number;
|
||||
name: string;
|
||||
}) {
|
||||
return prisma.tariff.create({
|
||||
data: {
|
||||
...data,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateTariff(
|
||||
id: number,
|
||||
data: {
|
||||
name?: string;
|
||||
isActive?: boolean;
|
||||
}
|
||||
) {
|
||||
return prisma.tariff.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteTariff(id: number) {
|
||||
// Check if tariff is used by any contracts
|
||||
const count = await prisma.contract.count({
|
||||
where: { tariffId: id },
|
||||
});
|
||||
|
||||
if (count > 0) {
|
||||
throw new Error(
|
||||
`Tarif kann nicht gelöscht werden, da er von ${count} Verträgen verwendet wird`
|
||||
);
|
||||
}
|
||||
|
||||
return prisma.tariff.delete({ where: { id } });
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { paginate, buildPaginationResponse } from '../utils/helpers.js';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export interface UserFilters {
|
||||
search?: string;
|
||||
isActive?: boolean;
|
||||
roleId?: number;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export async function getAllUsers(filters: UserFilters) {
|
||||
const { search, isActive, roleId, page = 1, limit = 20 } = filters;
|
||||
const { skip, take } = paginate(page, limit);
|
||||
|
||||
const where: Record<string, unknown> = {};
|
||||
|
||||
if (isActive !== undefined) {
|
||||
where.isActive = isActive;
|
||||
}
|
||||
|
||||
if (roleId) {
|
||||
where.roles = { some: { roleId } };
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ email: { contains: search } },
|
||||
{ firstName: { contains: search } },
|
||||
{ lastName: { contains: search } },
|
||||
];
|
||||
}
|
||||
|
||||
const [users, total] = await Promise.all([
|
||||
prisma.user.findMany({
|
||||
where,
|
||||
skip,
|
||||
take,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
isActive: true,
|
||||
customerId: true,
|
||||
createdAt: true,
|
||||
roles: {
|
||||
include: {
|
||||
role: {
|
||||
include: {
|
||||
permissions: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.user.count({ where }),
|
||||
]);
|
||||
|
||||
// Get Developer role ID
|
||||
const developerRole = await prisma.role.findFirst({
|
||||
where: { name: 'Developer' },
|
||||
});
|
||||
|
||||
return {
|
||||
users: users.map((u) => {
|
||||
// Check if user has developer role assigned
|
||||
const hasDeveloperAccess = developerRole
|
||||
? u.roles.some((ur) => ur.roleId === developerRole.id)
|
||||
: false;
|
||||
return {
|
||||
...u,
|
||||
roles: u.roles.map((r) => r.role),
|
||||
hasDeveloperAccess,
|
||||
};
|
||||
}),
|
||||
pagination: buildPaginationResponse(page, limit, total),
|
||||
};
|
||||
}
|
||||
|
||||
export async function getUserById(id: number) {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
isActive: true,
|
||||
customerId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
roles: {
|
||||
include: {
|
||||
role: {
|
||||
include: {
|
||||
permissions: {
|
||||
include: { permission: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
const permissions = new Set<string>();
|
||||
for (const userRole of user.roles) {
|
||||
for (const rolePerm of userRole.role.permissions) {
|
||||
permissions.add(
|
||||
`${rolePerm.permission.resource}:${rolePerm.permission.action}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...user,
|
||||
roles: user.roles.map((r) => r.role),
|
||||
permissions: Array.from(permissions),
|
||||
};
|
||||
}
|
||||
|
||||
export async function createUser(data: {
|
||||
email: string;
|
||||
password: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
roleIds: number[];
|
||||
customerId?: number;
|
||||
}) {
|
||||
const hashedPassword = await bcrypt.hash(data.password, 10);
|
||||
|
||||
return prisma.user.create({
|
||||
data: {
|
||||
email: data.email,
|
||||
password: hashedPassword,
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
customerId: data.customerId,
|
||||
roles: {
|
||||
create: data.roleIds.map((roleId) => ({ roleId })),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
isActive: true,
|
||||
customerId: true,
|
||||
roles: {
|
||||
include: { role: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateUser(
|
||||
id: number,
|
||||
data: {
|
||||
email?: string;
|
||||
password?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
isActive?: boolean;
|
||||
roleIds?: number[];
|
||||
customerId?: number;
|
||||
hasDeveloperAccess?: boolean;
|
||||
}
|
||||
) {
|
||||
const { roleIds, password, hasDeveloperAccess, ...userData } = data;
|
||||
|
||||
// Check if this would remove the last admin
|
||||
const isBeingDeactivated = userData.isActive === false;
|
||||
const rolesAreBeingChanged = roleIds !== undefined;
|
||||
|
||||
if (isBeingDeactivated || rolesAreBeingChanged) {
|
||||
// Check if user currently has admin permissions
|
||||
const currentUser = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
roles: {
|
||||
include: {
|
||||
role: {
|
||||
include: {
|
||||
permissions: {
|
||||
include: { permission: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const isCurrentlyAdmin = currentUser?.roles.some((ur) =>
|
||||
ur.role.permissions.some(
|
||||
(rp) => rp.permission.resource === 'users' && rp.permission.action === 'delete'
|
||||
)
|
||||
);
|
||||
|
||||
if (isCurrentlyAdmin) {
|
||||
// Check if user will still be admin after role change
|
||||
let willStillBeAdmin = false;
|
||||
if (rolesAreBeingChanged) {
|
||||
const newRoles = await prisma.role.findMany({
|
||||
where: { id: { in: roleIds } },
|
||||
include: {
|
||||
permissions: {
|
||||
include: { permission: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
willStillBeAdmin = newRoles.some((role) =>
|
||||
role.permissions.some(
|
||||
(rp) => rp.permission.resource === 'users' && rp.permission.action === 'delete'
|
||||
)
|
||||
);
|
||||
} else {
|
||||
willStillBeAdmin = true; // Roles not being changed
|
||||
}
|
||||
|
||||
// If user is losing admin status or being deactivated, check for other admins
|
||||
if (!willStillBeAdmin || isBeingDeactivated) {
|
||||
const otherAdminCount = await prisma.user.count({
|
||||
where: {
|
||||
id: { not: id },
|
||||
isActive: true,
|
||||
roles: {
|
||||
some: {
|
||||
role: {
|
||||
permissions: {
|
||||
some: {
|
||||
permission: {
|
||||
resource: 'users',
|
||||
action: 'delete',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (otherAdminCount === 0) {
|
||||
if (isBeingDeactivated) {
|
||||
throw new Error(
|
||||
'Dieser Benutzer ist der letzte Administrator und kann nicht deaktiviert werden'
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
'Die Admin-Rolle kann nicht entfernt werden, da dies der letzte Administrator ist'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hash password if provided
|
||||
if (password) {
|
||||
(userData as Record<string, unknown>).password = await bcrypt.hash(password, 10);
|
||||
}
|
||||
|
||||
// Update user
|
||||
await prisma.user.update({
|
||||
where: { id },
|
||||
data: userData,
|
||||
});
|
||||
|
||||
// Update roles if provided
|
||||
if (roleIds) {
|
||||
await prisma.userRole.deleteMany({ where: { userId: id } });
|
||||
await prisma.userRole.createMany({
|
||||
data: roleIds.map((roleId) => ({ userId: id, roleId })),
|
||||
});
|
||||
}
|
||||
|
||||
// Handle developer access
|
||||
console.log('updateUser - hasDeveloperAccess:', hasDeveloperAccess);
|
||||
if (hasDeveloperAccess !== undefined) {
|
||||
await setUserDeveloperAccess(id, hasDeveloperAccess);
|
||||
}
|
||||
|
||||
return getUserById(id);
|
||||
}
|
||||
|
||||
// Helper to set developer access for a user
|
||||
async function setUserDeveloperAccess(userId: number, enabled: boolean) {
|
||||
console.log('setUserDeveloperAccess called - userId:', userId, 'enabled:', enabled);
|
||||
|
||||
// Get or create developer:access permission
|
||||
let developerPerm = await prisma.permission.findFirst({
|
||||
where: { resource: 'developer', action: 'access' },
|
||||
});
|
||||
|
||||
if (!developerPerm) {
|
||||
developerPerm = await prisma.permission.create({
|
||||
data: { resource: 'developer', action: 'access' },
|
||||
});
|
||||
}
|
||||
|
||||
// Get or create Developer role
|
||||
let developerRole = await prisma.role.findFirst({
|
||||
where: { name: 'Developer' },
|
||||
});
|
||||
|
||||
if (!developerRole) {
|
||||
developerRole = await prisma.role.create({
|
||||
data: {
|
||||
name: 'Developer',
|
||||
description: 'Entwicklerzugriff auf Datenbanktools',
|
||||
permissions: {
|
||||
create: [{ permissionId: developerPerm.id }],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user already has Developer role
|
||||
const hasRole = await prisma.userRole.findFirst({
|
||||
where: { userId, roleId: developerRole.id },
|
||||
});
|
||||
|
||||
console.log('setUserDeveloperAccess - developerRole.id:', developerRole.id, 'hasRole:', hasRole);
|
||||
|
||||
if (enabled && !hasRole) {
|
||||
// Add Developer role
|
||||
console.log('Adding Developer role');
|
||||
await prisma.userRole.create({
|
||||
data: { userId, roleId: developerRole.id },
|
||||
});
|
||||
} else if (!enabled && hasRole) {
|
||||
// Remove Developer role
|
||||
console.log('Removing Developer role');
|
||||
await prisma.userRole.delete({
|
||||
where: { userId_roleId: { userId, roleId: developerRole.id } },
|
||||
});
|
||||
} else {
|
||||
console.log('No action needed - enabled:', enabled, 'hasRole:', !!hasRole);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteUser(id: number) {
|
||||
// Check if user is an admin
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
roles: {
|
||||
include: {
|
||||
role: {
|
||||
include: {
|
||||
permissions: {
|
||||
include: { permission: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error('Benutzer nicht gefunden');
|
||||
}
|
||||
|
||||
// Check if user has admin permissions (users:delete means admin)
|
||||
const isAdmin = user.roles.some((ur) =>
|
||||
ur.role.permissions.some(
|
||||
(rp) => rp.permission.resource === 'users' && rp.permission.action === 'delete'
|
||||
)
|
||||
);
|
||||
|
||||
if (isAdmin) {
|
||||
// Count other admins (users with users:delete permission)
|
||||
const adminCount = await prisma.user.count({
|
||||
where: {
|
||||
id: { not: id },
|
||||
isActive: true,
|
||||
roles: {
|
||||
some: {
|
||||
role: {
|
||||
permissions: {
|
||||
some: {
|
||||
permission: {
|
||||
resource: 'users',
|
||||
action: 'delete',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (adminCount === 0) {
|
||||
throw new Error(
|
||||
'Dieser Benutzer ist der letzte Administrator und kann nicht gelöscht werden'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return prisma.user.delete({ where: { id } });
|
||||
}
|
||||
|
||||
// Role operations
|
||||
export async function getAllRoles() {
|
||||
return prisma.role.findMany({
|
||||
include: {
|
||||
permissions: {
|
||||
include: { permission: true },
|
||||
},
|
||||
_count: {
|
||||
select: { users: true },
|
||||
},
|
||||
},
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getRoleById(id: number) {
|
||||
return prisma.role.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
permissions: {
|
||||
include: { permission: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function createRole(data: {
|
||||
name: string;
|
||||
description?: string;
|
||||
permissionIds: number[];
|
||||
}) {
|
||||
return prisma.role.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
permissions: {
|
||||
create: data.permissionIds.map((permissionId) => ({ permissionId })),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
permissions: {
|
||||
include: { permission: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateRole(
|
||||
id: number,
|
||||
data: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
permissionIds?: number[];
|
||||
}
|
||||
) {
|
||||
const { permissionIds, ...roleData } = data;
|
||||
|
||||
await prisma.role.update({
|
||||
where: { id },
|
||||
data: roleData,
|
||||
});
|
||||
|
||||
if (permissionIds) {
|
||||
await prisma.rolePermission.deleteMany({ where: { roleId: id } });
|
||||
await prisma.rolePermission.createMany({
|
||||
data: permissionIds.map((permissionId) => ({ roleId: id, permissionId })),
|
||||
});
|
||||
}
|
||||
|
||||
return getRoleById(id);
|
||||
}
|
||||
|
||||
export async function deleteRole(id: number) {
|
||||
// Check if role is assigned to any users
|
||||
const count = await prisma.userRole.count({ where: { roleId: id } });
|
||||
if (count > 0) {
|
||||
throw new Error(
|
||||
`Rolle kann nicht gelöscht werden, da sie ${count} Benutzern zugewiesen ist`
|
||||
);
|
||||
}
|
||||
|
||||
return prisma.role.delete({ where: { id } });
|
||||
}
|
||||
|
||||
// Permission operations
|
||||
export async function getAllPermissions() {
|
||||
return prisma.permission.findMany({
|
||||
orderBy: [{ resource: 'asc' }, { action: 'asc' }],
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Request } from 'express';
|
||||
|
||||
export interface JwtPayload {
|
||||
userId?: number; // User-ID (nur bei Mitarbeiter-Login)
|
||||
email: string;
|
||||
permissions: string[];
|
||||
customerId?: number; // Eigene Kunden-ID (bei Kundenportal-Login)
|
||||
isCustomerPortal?: boolean; // Ist dies ein Kundenportal-Login?
|
||||
representedCustomerIds?: number[]; // IDs der Kunden, die dieser Kunde vertreten kann
|
||||
}
|
||||
|
||||
export interface AuthRequest extends Request {
|
||||
user?: JwtPayload;
|
||||
}
|
||||
|
||||
export interface PaginationParams {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
sortBy?: string;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface ApiResponse<T = unknown> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: string;
|
||||
pagination?: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
const IV_LENGTH = 16;
|
||||
const TAG_LENGTH = 16;
|
||||
|
||||
function getEncryptionKey(): Buffer {
|
||||
const key = process.env.ENCRYPTION_KEY;
|
||||
if (!key) {
|
||||
throw new Error('ENCRYPTION_KEY environment variable is not set');
|
||||
}
|
||||
// Convert hex string to buffer or hash if not correct length
|
||||
if (key.length === 64) {
|
||||
return Buffer.from(key, 'hex');
|
||||
}
|
||||
// Hash the key to get 32 bytes
|
||||
return crypto.createHash('sha256').update(key).digest();
|
||||
}
|
||||
|
||||
export function encrypt(text: string): string {
|
||||
const key = getEncryptionKey();
|
||||
const iv = crypto.randomBytes(IV_LENGTH);
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
||||
|
||||
let encrypted = cipher.update(text, 'utf8', 'hex');
|
||||
encrypted += cipher.final('hex');
|
||||
|
||||
const authTag = cipher.getAuthTag();
|
||||
|
||||
// Format: iv:authTag:encryptedData (all in hex)
|
||||
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
|
||||
}
|
||||
|
||||
export function decrypt(encryptedText: string): string {
|
||||
const key = getEncryptionKey();
|
||||
const parts = encryptedText.split(':');
|
||||
|
||||
if (parts.length !== 3) {
|
||||
throw new Error('Invalid encrypted text format');
|
||||
}
|
||||
|
||||
const iv = Buffer.from(parts[0], 'hex');
|
||||
const authTag = Buffer.from(parts[1], 'hex');
|
||||
const encrypted = parts[2];
|
||||
|
||||
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
||||
decipher.setAuthTag(authTag);
|
||||
|
||||
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
export function generateCustomerNumber(): string {
|
||||
const timestamp = Date.now().toString(36).toUpperCase();
|
||||
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
|
||||
return `K${timestamp}${random}`;
|
||||
}
|
||||
|
||||
export function generateContractNumber(type: string): string {
|
||||
const prefix = type.substring(0, 3).toUpperCase();
|
||||
const timestamp = Date.now().toString(36).toUpperCase();
|
||||
const random = Math.random().toString(36).substring(2, 5).toUpperCase();
|
||||
return `${prefix}-${timestamp}${random}`;
|
||||
}
|
||||
|
||||
export function paginate(page: number = 1, limit: number = 20) {
|
||||
const skip = (page - 1) * limit;
|
||||
return { skip, take: limit };
|
||||
}
|
||||
|
||||
export function buildPaginationResponse(
|
||||
page: number,
|
||||
limit: number,
|
||||
total: number
|
||||
) {
|
||||
return {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user