E-Mail als PDF speichern: Tab "Vertragsdokument" ergänzt
Bisher hatte das "E-Mail als PDF speichern"-Modal nur die Tabs
"Als Dokument" + "Als Rechnung" (nur Energieverträge). Wenn die
E-Mail einem Vertrag zugeordnet ist, fehlte die Möglichkeit, sie
direkt als Vertragsdokument (Auftragsformular, Lieferbestätigung
etc.) zu hinterlegen – analog zum Anhang-Modal.
Backend: neuer Endpoint POST /api/emails/:id/save-as-contract-document
{ documentType, notes?, deliveryDate? } – generiert das Mail-PDF,
speichert es unter /uploads/contract-documents und legt einen
ContractDocument-Eintrag an. Bei documentType "Lieferbestätigung"
wird der bestehende maybeActivateOnDeliveryConfirmation-Workflow
getriggert (DRAFT → ACTIVE, startDate-Übernahme).
Frontend: SaveEmailAsPdfModal bekommt den dritten Tab parallel zu
SaveAttachmentModal. Tab erscheint, sobald die E-Mail einem Vertrag
zugeordnet ist (auch bei Nicht-Energieverträgen); Tab "Als Rechnung"
bleibt auf Energieverträge beschränkt. Dokumenttyp-Dropdown und
Notizen-Feld werden aus dem Anhang-Modal übernommen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1814,6 +1814,93 @@ export async function saveEmailAsInvoice(req: AuthRequest, res: Response): Promi
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== SAVE EMAIL AS CONTRACT DOCUMENT ====================
|
||||
|
||||
// E-Mail als PDF exportieren und als Vertragsdokument hinterlegen.
|
||||
// Parallel zu saveAttachmentAsContractDocument (Anhang-Variante) – damit
|
||||
// auch reine Mail-Bestätigungen ohne Anhang als Auftragsformular/
|
||||
// Lieferbestätigung etc. an einem Vertrag landen können.
|
||||
export async function saveEmailAsContractDocument(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const emailId = parseInt(req.params.id);
|
||||
if (!(await canAccessCachedEmail(req, res, emailId))) return;
|
||||
const { documentType, notes } = req.body;
|
||||
|
||||
if (!documentType || typeof documentType !== 'string') {
|
||||
res.status(400).json({ success: false, error: 'documentType ist erforderlich' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const email = await cachedEmailService.getCachedEmailById(emailId);
|
||||
if (!email) {
|
||||
res.status(404).json({ success: false, error: 'E-Mail nicht gefunden' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
if (!email.contractId) {
|
||||
res.status(400).json({ success: false, error: 'E-Mail ist keinem Vertrag zugeordnet' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const contract = await prisma.contract.findUnique({
|
||||
where: { id: email.contractId },
|
||||
select: { id: true, contractNumber: true, customerId: true },
|
||||
});
|
||||
if (!contract) {
|
||||
res.status(404).json({ success: false, error: 'Vertrag nicht gefunden' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
if (!(await canAccessContract(req as AuthRequest, res, contract.id))) return;
|
||||
|
||||
// Empfänger-Adressen parsen
|
||||
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 */ }
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
const uploadsDir = path.join(process.cwd(), 'uploads', 'contract-documents');
|
||||
if (!fs.existsSync(uploadsDir)) fs.mkdirSync(uploadsDir, { recursive: true });
|
||||
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||
const safeType = documentType.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
||||
const newFilename = `${safeType}-email-${uniqueSuffix}.pdf`;
|
||||
const filePath = path.join(uploadsDir, newFilename);
|
||||
const relativePath = `/uploads/contract-documents/${newFilename}`;
|
||||
|
||||
fs.writeFileSync(filePath, pdfBuffer);
|
||||
|
||||
const doc = await prisma.contractDocument.create({
|
||||
data: {
|
||||
contractId: contract.id,
|
||||
documentType,
|
||||
documentPath: relativePath,
|
||||
originalName: `${email.subject || 'email'}.pdf`,
|
||||
notes: notes || null,
|
||||
uploadedBy: (req as any).user?.email || 'email-import',
|
||||
},
|
||||
});
|
||||
|
||||
// Falls Lieferbestätigung: DRAFT → ACTIVE + startDate setzen falls leer
|
||||
const deliveryDate = typeof req.body?.deliveryDate === 'string' ? req.body.deliveryDate : null;
|
||||
await maybeActivateOnDeliveryConfirmation(contract.id, documentType, req, deliveryDate);
|
||||
|
||||
res.json({ success: true, data: doc } as ApiResponse);
|
||||
} catch (error) {
|
||||
console.error('saveEmailAsContractDocument error:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
res.status(500).json({ success: false, error: `Fehler beim Speichern: ${errorMessage}` } as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== SAVE ATTACHMENT AS INVOICE ====================
|
||||
|
||||
// E-Mail-Anhang als Rechnung speichern
|
||||
|
||||
@@ -194,6 +194,15 @@ router.post(
|
||||
cachedEmailController.saveEmailAsInvoice
|
||||
);
|
||||
|
||||
// E-Mail als PDF exportieren und als Vertragsdokument hinterlegen
|
||||
// POST /api/emails/:id/save-as-contract-document { documentType, notes?, deliveryDate? }
|
||||
router.post(
|
||||
'/emails/:id/save-as-contract-document',
|
||||
authenticate,
|
||||
requirePermission('contracts:update'),
|
||||
cachedEmailController.saveEmailAsContractDocument
|
||||
);
|
||||
|
||||
// Anhang als Rechnung speichern
|
||||
// POST /api/emails/:id/attachments/:filename/save-as-invoice { invoiceDate, invoiceType, notes? }
|
||||
router.post(
|
||||
|
||||
Reference in New Issue
Block a user