added invoices and status in cockpit, created info button for contract status types
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as cachedEmailService from '../services/cachedEmail.service.js';
|
||||
import * as stressfreiEmailService from '../services/stressfreiEmail.service.js';
|
||||
import * as invoiceService from '../services/invoice.service.js';
|
||||
import { sendEmail, SmtpCredentials, SendEmailParams, EmailAttachment } from '../services/smtpService.js';
|
||||
import { fetchAttachment, appendToSent, ImapCredentials } from '../services/imapService.js';
|
||||
import { getImapSmtpSettings } from '../services/emailProvider/emailProviderService.js';
|
||||
@@ -885,6 +886,8 @@ export async function getAttachmentTargets(req: Request, res: Response): Promise
|
||||
contract?: {
|
||||
id: number;
|
||||
contractNumber: string;
|
||||
type: string;
|
||||
energyDetailsId?: number;
|
||||
slots: TargetSlot[];
|
||||
};
|
||||
}
|
||||
@@ -954,10 +957,14 @@ export async function getAttachmentTargets(req: Request, res: Response): Promise
|
||||
select: {
|
||||
id: true,
|
||||
contractNumber: true,
|
||||
type: true,
|
||||
cancellationLetterPath: true,
|
||||
cancellationConfirmationPath: true,
|
||||
cancellationLetterOptionsPath: true,
|
||||
cancellationConfirmationOptionsPath: true,
|
||||
energyDetails: {
|
||||
select: { id: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -966,6 +973,8 @@ export async function getAttachmentTargets(req: Request, res: Response): Promise
|
||||
response.contract = {
|
||||
id: contract.id,
|
||||
contractNumber: contract.contractNumber,
|
||||
type: contract.type,
|
||||
energyDetailsId: contract.energyDetails?.id,
|
||||
slots: contractTargets.map(target => ({
|
||||
key: target.key,
|
||||
label: target.label,
|
||||
@@ -1518,3 +1527,309 @@ export async function saveEmailAsPdf(req: Request, res: Response): Promise<void>
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== SAVE EMAIL AS INVOICE ====================
|
||||
|
||||
// E-Mail als PDF exportieren und als Rechnung speichern
|
||||
export async function saveEmailAsInvoice(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const emailId = parseInt(req.params.id);
|
||||
const { invoiceDate, invoiceType, notes } = req.body;
|
||||
|
||||
console.log('[saveEmailAsInvoice] Request:', { emailId, invoiceDate, invoiceType, notes });
|
||||
|
||||
// Validierung
|
||||
if (!invoiceDate || !invoiceType) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'invoiceDate und invoiceType sind erforderlich',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validiere invoiceType
|
||||
if (!['INTERIM', 'FINAL', 'NOT_AVAILABLE'].includes(invoiceType)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Ungültiger Rechnungstyp',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// E-Mail aus Cache laden
|
||||
const email = await cachedEmailService.getCachedEmailById(emailId);
|
||||
if (!email) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'E-Mail nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfen ob E-Mail einem Vertrag zugeordnet ist
|
||||
if (!email.contractId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'E-Mail ist keinem Vertrag zugeordnet',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vertrag laden und prüfen ob es ein Energievertrag ist
|
||||
const contract = await prisma.contract.findUnique({
|
||||
where: { id: email.contractId },
|
||||
include: { energyDetails: true },
|
||||
});
|
||||
|
||||
if (!contract) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Vertrag nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!['ELECTRICITY', 'GAS'].includes(contract.type)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Nur für Strom- und Gas-Verträge verfügbar',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!contract.energyDetails) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Keine Energie-Details für diesen Vertrag vorhanden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Empfänger-Adressen parsen (JSON Array)
|
||||
let toAddresses: string[] = [];
|
||||
let ccAddresses: string[] = [];
|
||||
try {
|
||||
toAddresses = JSON.parse(email.toAddresses);
|
||||
} catch { toAddresses = [email.toAddresses]; }
|
||||
try {
|
||||
if (email.ccAddresses) ccAddresses = JSON.parse(email.ccAddresses);
|
||||
} catch { /* ignore */ }
|
||||
|
||||
// PDF generieren
|
||||
const pdfBuffer = await generateEmailPdf({
|
||||
from: email.fromAddress,
|
||||
to: toAddresses.join(', '),
|
||||
cc: ccAddresses.length > 0 ? ccAddresses.join(', ') : undefined,
|
||||
subject: email.subject || '(Kein Betreff)',
|
||||
date: email.receivedAt,
|
||||
bodyText: email.textBody || undefined,
|
||||
bodyHtml: email.htmlBody || undefined,
|
||||
});
|
||||
|
||||
// Uploads-Verzeichnis erstellen
|
||||
const uploadsDir = path.join(process.cwd(), 'uploads', 'invoices');
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Eindeutigen Dateinamen generieren
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||
const newFilename = `invoice-email-${uniqueSuffix}.pdf`;
|
||||
const filePath = path.join(uploadsDir, newFilename);
|
||||
const relativePath = `/uploads/invoices/${newFilename}`;
|
||||
|
||||
// PDF speichern
|
||||
fs.writeFileSync(filePath, pdfBuffer);
|
||||
|
||||
// Invoice in DB erstellen
|
||||
const invoice = await invoiceService.addInvoice(contract.energyDetails.id, {
|
||||
invoiceDate: new Date(invoiceDate),
|
||||
invoiceType,
|
||||
documentPath: relativePath,
|
||||
notes: notes || undefined,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: invoice,
|
||||
} as ApiResponse);
|
||||
} catch (error) {
|
||||
console.error('saveEmailAsInvoice error:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: `Fehler beim Erstellen der Rechnung: ${errorMessage}`,
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== SAVE ATTACHMENT AS INVOICE ====================
|
||||
|
||||
// E-Mail-Anhang als Rechnung speichern
|
||||
export async function saveAttachmentAsInvoice(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const emailId = parseInt(req.params.id);
|
||||
const filename = decodeURIComponent(req.params.filename);
|
||||
const { invoiceDate, invoiceType, notes } = req.body;
|
||||
|
||||
console.log('[saveAttachmentAsInvoice] Request:', { emailId, filename, invoiceDate, invoiceType, notes });
|
||||
|
||||
// Validierung
|
||||
if (!invoiceDate || !invoiceType) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'invoiceDate und invoiceType sind erforderlich',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validiere invoiceType
|
||||
if (!['INTERIM', 'FINAL', 'NOT_AVAILABLE'].includes(invoiceType)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Ungültiger Rechnungstyp',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// E-Mail aus Cache laden
|
||||
const email = await cachedEmailService.getCachedEmailById(emailId);
|
||||
if (!email) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'E-Mail nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prüfen ob E-Mail einem Vertrag zugeordnet ist
|
||||
if (!email.contractId) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'E-Mail ist keinem Vertrag zugeordnet',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vertrag laden und prüfen ob es ein Energievertrag ist
|
||||
const contract = await prisma.contract.findUnique({
|
||||
where: { id: email.contractId },
|
||||
include: { energyDetails: true },
|
||||
});
|
||||
|
||||
if (!contract) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Vertrag nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!['ELECTRICITY', 'GAS'].includes(contract.type)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Nur für Strom- und Gas-Verträge verfügbar',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!contract.energyDetails) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Keine Energie-Details für diesen Vertrag vorhanden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Für gesendete E-Mails: Prüfen ob UID vorhanden
|
||||
if (email.folder === 'SENT' && email.uid === 0) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Anhang nicht verfügbar - E-Mail wurde vor der IMAP-Speicherung gesendet',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// StressfreiEmail laden um Zugangsdaten zu bekommen
|
||||
const stressfreiEmail = await stressfreiEmailService.getEmailWithMailboxById(email.stressfreiEmailId);
|
||||
if (!stressfreiEmail || !stressfreiEmail.emailPasswordEncrypted) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Keine Mailbox-Zugangsdaten verfügbar',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// IMAP-Einstellungen laden
|
||||
const settings = await getImapSmtpSettings();
|
||||
if (!settings) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Keine E-Mail-Provider-Einstellungen gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Passwort entschlüsseln
|
||||
const password = decrypt(stressfreiEmail.emailPasswordEncrypted);
|
||||
|
||||
// IMAP-Credentials zusammenstellen
|
||||
const credentials: ImapCredentials = {
|
||||
host: settings.imapServer,
|
||||
port: settings.imapPort,
|
||||
user: stressfreiEmail.email,
|
||||
password,
|
||||
encryption: settings.imapEncryption,
|
||||
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
||||
};
|
||||
|
||||
// IMAP-Ordner bestimmen
|
||||
const imapFolder = email.folder === 'SENT' ? 'Sent' : 'INBOX';
|
||||
|
||||
// Anhang vom IMAP-Server laden
|
||||
const attachment = await fetchAttachment(credentials, email.uid, filename, imapFolder);
|
||||
if (!attachment) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Anhang nicht gefunden oder nicht mehr verfügbar',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Uploads-Verzeichnis erstellen
|
||||
const uploadsDir = path.join(process.cwd(), 'uploads', 'invoices');
|
||||
if (!fs.existsSync(uploadsDir)) {
|
||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Dateiendung extrahieren
|
||||
const ext = path.extname(filename) || '.pdf';
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||
const newFilename = `invoice-attachment-${uniqueSuffix}${ext}`;
|
||||
const filePath = path.join(uploadsDir, newFilename);
|
||||
const relativePath = `/uploads/invoices/${newFilename}`;
|
||||
|
||||
// Datei speichern
|
||||
fs.writeFileSync(filePath, attachment.content);
|
||||
|
||||
// Invoice in DB erstellen
|
||||
const invoice = await invoiceService.addInvoice(contract.energyDetails.id, {
|
||||
invoiceDate: new Date(invoiceDate),
|
||||
invoiceType,
|
||||
documentPath: relativePath,
|
||||
notes: notes || undefined,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: invoice,
|
||||
} as ApiResponse);
|
||||
} catch (error) {
|
||||
console.error('saveAttachmentAsInvoice error:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: `Fehler beim Erstellen der Rechnung: ${errorMessage}`,
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as invoiceService from '../services/invoice.service.js';
|
||||
import { ApiResponse } from '../types/index.js';
|
||||
|
||||
/**
|
||||
* Alle Rechnungen für ein EnergyContractDetails abrufen
|
||||
*/
|
||||
export async function getInvoices(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const ecdId = parseInt(req.params.ecdId);
|
||||
const invoices = await invoiceService.getInvoices(ecdId);
|
||||
res.json({ success: true, data: invoices } as ApiResponse);
|
||||
} catch (error) {
|
||||
console.error('getInvoices error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Rechnungen',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Einzelne Rechnung abrufen
|
||||
*/
|
||||
export async function getInvoice(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const ecdId = parseInt(req.params.ecdId);
|
||||
const invoiceId = parseInt(req.params.invoiceId);
|
||||
const invoice = await invoiceService.getInvoice(ecdId, invoiceId);
|
||||
|
||||
if (!invoice) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Rechnung nicht gefunden',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true, data: invoice } as ApiResponse);
|
||||
} catch (error) {
|
||||
console.error('getInvoice error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Rechnung',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Neue Rechnung hinzufügen
|
||||
*/
|
||||
export async function addInvoice(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const ecdId = parseInt(req.params.ecdId);
|
||||
const { invoiceDate, invoiceType, documentPath, notes } = req.body;
|
||||
|
||||
if (!invoiceDate || !invoiceType) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'invoiceDate und invoiceType sind erforderlich',
|
||||
} as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const invoice = await invoiceService.addInvoice(ecdId, {
|
||||
invoiceDate: new Date(invoiceDate),
|
||||
invoiceType,
|
||||
documentPath,
|
||||
notes,
|
||||
});
|
||||
|
||||
res.status(201).json({ success: true, data: invoice } as ApiResponse);
|
||||
} catch (error) {
|
||||
console.error('addInvoice error:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Hinzufügen der Rechnung',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rechnung aktualisieren
|
||||
*/
|
||||
export async function updateInvoice(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const ecdId = parseInt(req.params.ecdId);
|
||||
const invoiceId = parseInt(req.params.invoiceId);
|
||||
const { invoiceDate, invoiceType, documentPath, notes } = req.body;
|
||||
|
||||
const invoice = await invoiceService.updateInvoice(ecdId, invoiceId, {
|
||||
invoiceDate: invoiceDate ? new Date(invoiceDate) : undefined,
|
||||
invoiceType,
|
||||
documentPath,
|
||||
notes,
|
||||
});
|
||||
|
||||
res.json({ success: true, data: invoice } as ApiResponse);
|
||||
} catch (error) {
|
||||
console.error('updateInvoice error:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren der Rechnung',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rechnung löschen
|
||||
*/
|
||||
export async function deleteInvoice(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const ecdId = parseInt(req.params.ecdId);
|
||||
const invoiceId = parseInt(req.params.invoiceId);
|
||||
|
||||
await invoiceService.deleteInvoice(ecdId, invoiceId);
|
||||
|
||||
res.json({ success: true, data: null } as ApiResponse);
|
||||
} catch (error) {
|
||||
console.error('deleteInvoice error:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Löschen der Rechnung',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user