PDF-Auftragsvorlagen-System, Objekttyp/Lage-Felder, Eigentümer-Fallback bei Bankverbindung
- PDF-Template-Editor in Einstellungen: Vorlagen hochladen, Formularfelder automatisch auslesen, CRM-Felder zuordnen - PDF-Vorschau mit annotierten Feldnamen, seitenweise Sortierung der Felder - Auftrag generieren aus Vertragsdaten (Button im Vertrags-Detail) - Dynamische Rufnummern-Felder mit Vorwahl-Extraktion und konfigurierbarer Maximalanzahl - Nicht zugeordnete Felder bleiben editierbar im generierten PDF - Eigentümer-Felder mit Namens-Kombinationen (Firma+Name etc.) und Fallback auf Kundendaten - Stressfrei-E-Mail als Feld-Option im Template-Editor - Objekttyp, Lage und Lage des Anschlusses als neue Felder bei Festnetz-Verträgen (DSL, Glasfaser, Kabel) - Bankverbindung-Fallback: wenn keine am Vertrag verknüpft, wird automatisch die neueste aktive des Kunden genommen Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -202,6 +202,11 @@ export async function updateAddress(req: Request, res: Response): Promise<void>
|
||||
const fieldLabels: Record<string, string> = {
|
||||
street: 'Straße', houseNumber: 'Hausnummer', postalCode: 'PLZ',
|
||||
city: 'Stadt', country: 'Land', type: 'Typ', isDefault: 'Standard',
|
||||
ownerCompany: 'Eigentümer Firma', ownerFirstName: 'Eigentümer Vorname',
|
||||
ownerLastName: 'Eigentümer Nachname', ownerStreet: 'Eigentümer Straße',
|
||||
ownerHouseNumber: 'Eigentümer Hausnr.', ownerPostalCode: 'Eigentümer PLZ',
|
||||
ownerCity: 'Eigentümer Ort', ownerPhone: 'Eigentümer Telefon',
|
||||
ownerMobile: 'Eigentümer Mobil', ownerEmail: 'Eigentümer E-Mail',
|
||||
};
|
||||
for (const [key, newVal] of Object.entries(data)) {
|
||||
if (['id', 'createdAt', 'updatedAt'].includes(key)) continue;
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import { Response } from 'express';
|
||||
import { AuthRequest } from '../types/index.js';
|
||||
import * as pdfTemplateService from '../services/pdfTemplate.service.js';
|
||||
import { logChange } from '../services/audit.service.js';
|
||||
|
||||
export async function getTemplates(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const templates = await pdfTemplateService.getAllTemplates();
|
||||
res.json({ success: true, data: templates });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden' });
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTemplate(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const template = await pdfTemplateService.getTemplateById(parseInt(req.params.id));
|
||||
if (!template) return res.status(404).json({ success: false, error: 'Vorlage nicht gefunden' });
|
||||
res.json({ success: true, data: template });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: 'Fehler beim Laden' });
|
||||
}
|
||||
}
|
||||
|
||||
export async function createTemplate(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
if (!req.file) return res.status(400).json({ success: false, error: 'PDF-Datei erforderlich' });
|
||||
|
||||
const { name, description, providerName, phoneFieldPrefix, maxPhoneFields } = req.body;
|
||||
const templatePath = `/uploads/pdf-templates/${req.file.filename}`;
|
||||
|
||||
// PDF-Felder auslesen
|
||||
let pdfFields: { name: string; type: string; page: number; y: number }[] = [];
|
||||
try {
|
||||
const extracted = await pdfTemplateService.extractPdfFields(templatePath);
|
||||
pdfFields = extracted.fields;
|
||||
} catch {
|
||||
// PDF hat keine Formularfelder - OK, kann trotzdem gespeichert werden
|
||||
}
|
||||
|
||||
const template = await pdfTemplateService.createTemplate({
|
||||
name,
|
||||
description,
|
||||
providerName,
|
||||
templatePath,
|
||||
originalName: req.file.originalname,
|
||||
phoneFieldPrefix,
|
||||
maxPhoneFields: maxPhoneFields ? parseInt(maxPhoneFields) : undefined,
|
||||
});
|
||||
|
||||
await logChange({
|
||||
req, action: 'CREATE', resourceType: 'PdfTemplate',
|
||||
resourceId: template.id.toString(),
|
||||
label: `Auftragsvorlage "${name}" angelegt`,
|
||||
});
|
||||
|
||||
res.status(201).json({ success: true, data: { ...template, pdfFields } });
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Erstellen',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateTemplate(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const { name, description, providerName, fieldMapping, phoneFieldPrefix, maxPhoneFields, isActive } = req.body;
|
||||
|
||||
const template = await pdfTemplateService.updateTemplate(id, {
|
||||
name,
|
||||
description,
|
||||
providerName,
|
||||
fieldMapping: fieldMapping ? JSON.stringify(fieldMapping) : undefined,
|
||||
phoneFieldPrefix,
|
||||
maxPhoneFields: maxPhoneFields !== undefined ? parseInt(maxPhoneFields) : undefined,
|
||||
isActive,
|
||||
});
|
||||
|
||||
await logChange({
|
||||
req, action: 'UPDATE', resourceType: 'PdfTemplate',
|
||||
resourceId: id.toString(),
|
||||
label: `Auftragsvorlage "${template.name}" aktualisiert`,
|
||||
});
|
||||
|
||||
res.json({ success: true, data: template });
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Aktualisieren',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteTemplate(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const template = await pdfTemplateService.getTemplateById(id);
|
||||
await pdfTemplateService.deleteTemplate(id);
|
||||
await logChange({
|
||||
req, action: 'DELETE', resourceType: 'PdfTemplate',
|
||||
resourceId: id.toString(),
|
||||
label: `Auftragsvorlage "${template?.name}" gelöscht`,
|
||||
});
|
||||
res.json({ success: true, message: 'Vorlage gelöscht' });
|
||||
} catch (error) {
|
||||
res.status(400).json({ success: false, error: 'Fehler beim Löschen' });
|
||||
}
|
||||
}
|
||||
|
||||
export async function extractFields(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const template = await pdfTemplateService.getTemplateById(id);
|
||||
if (!template) return res.status(404).json({ success: false, error: 'Vorlage nicht gefunden' });
|
||||
|
||||
const result = await pdfTemplateService.extractPdfFields(template.templatePath);
|
||||
res.json({ success: true, data: result.fields, totalPages: result.totalPages });
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Auslesen der PDF-Felder',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAnnotatedPreview(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const template = await pdfTemplateService.getTemplateById(id);
|
||||
if (!template) return res.status(404).json({ success: false, error: 'Vorlage nicht gefunden' });
|
||||
|
||||
const pdfBuffer = await pdfTemplateService.generateAnnotatedPreview(template.templatePath);
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', 'inline; filename="preview.pdf"');
|
||||
res.send(pdfBuffer);
|
||||
} catch (error) {
|
||||
res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Fehler' });
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCrmFields(req: AuthRequest, res: Response) {
|
||||
const maxPhoneFields = req.query.maxPhoneFields ? parseInt(req.query.maxPhoneFields as string) : 8;
|
||||
res.json({ success: true, data: pdfTemplateService.getCrmFieldsForTemplate(maxPhoneFields) });
|
||||
}
|
||||
|
||||
export async function getRequiredInputs(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const templateId = parseInt(req.params.id);
|
||||
const contractId = parseInt(req.params.contractId);
|
||||
const inputs = await pdfTemplateService.getRequiredInputs(templateId, contractId);
|
||||
res.json({ success: true, data: inputs });
|
||||
} catch (error) {
|
||||
res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Fehler' });
|
||||
}
|
||||
}
|
||||
|
||||
export async function generatePdf(req: AuthRequest, res: Response) {
|
||||
try {
|
||||
const templateId = parseInt(req.params.id);
|
||||
const contractId = parseInt(req.params.contractId);
|
||||
|
||||
// Extras aus Body (POST) oder Query-Parametern (GET)
|
||||
const stressfreiEmailId = req.body?.stressfreiEmailId || req.query.stressfreiEmailId;
|
||||
const manualValues: Record<string, string> = req.body?.manualValues || {};
|
||||
|
||||
// Manual-Werte aus Query-Parametern extrahieren (manual_manual:1=Wert)
|
||||
if (req.query) {
|
||||
for (const [key, value] of Object.entries(req.query)) {
|
||||
if (key.startsWith('manual_') && typeof value === 'string') {
|
||||
manualValues[key.replace('manual_', '')] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pdfBuffer = await pdfTemplateService.generateFilledPdf(templateId, contractId, {
|
||||
stressfreiEmailId: stressfreiEmailId ? parseInt(stressfreiEmailId as string) : undefined,
|
||||
manualValues: Object.keys(manualValues).length > 0 ? manualValues : undefined,
|
||||
});
|
||||
|
||||
const template = await pdfTemplateService.getTemplateById(templateId);
|
||||
const filename = `${template?.name || 'Auftrag'}_${new Date().toISOString().split('T')[0]}.pdf`;
|
||||
|
||||
await logChange({
|
||||
req, action: 'CREATE', resourceType: 'GeneratedPdf',
|
||||
label: `PDF "${template?.name}" generiert für Vertrag #${contractId}`,
|
||||
});
|
||||
|
||||
res.setHeader('Content-Type', 'application/pdf');
|
||||
res.setHeader('Content-Disposition', `inline; filename="${encodeURIComponent(filename)}"`);
|
||||
res.send(pdfBuffer);
|
||||
} catch (error) {
|
||||
console.error('PDF generate error:', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Generieren',
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user