195 lines
6.2 KiB
TypeScript
195 lines
6.2 KiB
TypeScript
/**
|
||
* Factory-Defaults: Export + Import von Stammdaten-Katalogen.
|
||
* Enthält KEINE Kundendaten, Verträge, Dokumente oder Einstellungen –
|
||
* nur reine Kataloge: Anbieter, Tarife, Kündigungsfristen, Laufzeiten,
|
||
* Vertragskategorien und PDF-Auftragsvorlagen.
|
||
*/
|
||
|
||
import fs from 'fs';
|
||
import path from 'path';
|
||
import archiver from 'archiver';
|
||
import prisma from '../lib/prisma.js';
|
||
|
||
export interface FactoryDefaultsManifest {
|
||
version: 1;
|
||
exportedAt: string;
|
||
counts: {
|
||
providers: number;
|
||
tariffs: number;
|
||
cancellationPeriods: number;
|
||
contractDurations: number;
|
||
contractCategories: number;
|
||
pdfTemplates: number;
|
||
};
|
||
}
|
||
|
||
export interface ProviderExport {
|
||
name: string;
|
||
portalUrl: string | null;
|
||
usernameFieldName: string | null;
|
||
passwordFieldName: string | null;
|
||
isActive: boolean;
|
||
tariffs: { name: string; isActive: boolean }[];
|
||
}
|
||
|
||
export interface PdfTemplateExport {
|
||
name: string;
|
||
description: string | null;
|
||
providerName: string | null;
|
||
originalName: string;
|
||
fieldMapping: any;
|
||
phoneFieldPrefix: string | null;
|
||
maxPhoneFields: number | null;
|
||
isActive: boolean;
|
||
// Datei wird separat im ZIP abgelegt unter pdf-templates/<name>.pdf
|
||
pdfFilename: string;
|
||
}
|
||
|
||
/**
|
||
* Sammelt alle Katalog-Daten aus der DB.
|
||
*/
|
||
export async function collectFactoryDefaults() {
|
||
const [providers, cancellationPeriods, contractDurations, contractCategories, pdfTemplates] =
|
||
await Promise.all([
|
||
prisma.provider.findMany({
|
||
include: { tariffs: { select: { name: true, isActive: true } } },
|
||
orderBy: { name: 'asc' },
|
||
}),
|
||
prisma.cancellationPeriod.findMany({ orderBy: { code: 'asc' } }),
|
||
prisma.contractDuration.findMany({ orderBy: { code: 'asc' } }),
|
||
prisma.contractCategory.findMany({ orderBy: { sortOrder: 'asc' } }),
|
||
prisma.pdfTemplate.findMany({ orderBy: { name: 'asc' } }),
|
||
]);
|
||
|
||
return {
|
||
providers: providers.map<ProviderExport>((p) => ({
|
||
name: p.name,
|
||
portalUrl: p.portalUrl,
|
||
usernameFieldName: p.usernameFieldName,
|
||
passwordFieldName: p.passwordFieldName,
|
||
isActive: p.isActive,
|
||
tariffs: p.tariffs.map((t) => ({ name: t.name, isActive: t.isActive })),
|
||
})),
|
||
cancellationPeriods: cancellationPeriods.map((c) => ({
|
||
code: c.code,
|
||
description: c.description,
|
||
isActive: c.isActive,
|
||
})),
|
||
contractDurations: contractDurations.map((d) => ({
|
||
code: d.code,
|
||
description: d.description,
|
||
isActive: d.isActive,
|
||
})),
|
||
contractCategories: contractCategories.map((c) => ({
|
||
code: c.code,
|
||
name: c.name,
|
||
icon: c.icon,
|
||
color: c.color,
|
||
sortOrder: c.sortOrder,
|
||
isActive: c.isActive,
|
||
})),
|
||
pdfTemplates: pdfTemplates.map<PdfTemplateExport>((t) => {
|
||
// fieldMapping ist im DB als String abgelegt – zu Objekt parsen
|
||
let fieldMapping: any = {};
|
||
try {
|
||
fieldMapping = JSON.parse(t.fieldMapping);
|
||
} catch {
|
||
fieldMapping = {};
|
||
}
|
||
return {
|
||
name: t.name,
|
||
description: t.description,
|
||
providerName: t.providerName,
|
||
originalName: t.originalName,
|
||
fieldMapping,
|
||
phoneFieldPrefix: t.phoneFieldPrefix,
|
||
maxPhoneFields: t.maxPhoneFields,
|
||
isActive: t.isActive,
|
||
pdfFilename: sanitizeFilename(t.name) + path.extname(t.originalName || '.pdf'),
|
||
};
|
||
}),
|
||
};
|
||
}
|
||
|
||
function sanitizeFilename(name: string): string {
|
||
return name.replace(/[^a-zA-Z0-9äöüÄÖÜß_-]/g, '_').replace(/_+/g, '_');
|
||
}
|
||
|
||
/**
|
||
* Erstellt ein ZIP-Archiv mit allen Factory-Defaults.
|
||
* Die PDF-Dateien werden aus dem uploads-Verzeichnis gelesen.
|
||
*/
|
||
export async function exportFactoryDefaults(): Promise<Buffer> {
|
||
const data = await collectFactoryDefaults();
|
||
|
||
const manifest: FactoryDefaultsManifest = {
|
||
version: 1,
|
||
exportedAt: new Date().toISOString(),
|
||
counts: {
|
||
providers: data.providers.length,
|
||
tariffs: data.providers.reduce((sum, p) => sum + p.tariffs.length, 0),
|
||
cancellationPeriods: data.cancellationPeriods.length,
|
||
contractDurations: data.contractDurations.length,
|
||
contractCategories: data.contractCategories.length,
|
||
pdfTemplates: data.pdfTemplates.length,
|
||
},
|
||
};
|
||
|
||
return new Promise<Buffer>((resolve, reject) => {
|
||
const archive = archiver('zip', { zlib: { level: 9 } });
|
||
const chunks: Buffer[] = [];
|
||
|
||
archive.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||
archive.on('end', () => resolve(Buffer.concat(chunks)));
|
||
archive.on('error', reject);
|
||
|
||
// JSON-Dateien vorbereiten
|
||
archive.append(JSON.stringify(manifest, null, 2), { name: 'manifest.json' });
|
||
archive.append(JSON.stringify(data.providers, null, 2), {
|
||
name: 'providers/providers.json',
|
||
});
|
||
archive.append(JSON.stringify(data.cancellationPeriods, null, 2), {
|
||
name: 'contract-meta/cancellation-periods.json',
|
||
});
|
||
archive.append(JSON.stringify(data.contractDurations, null, 2), {
|
||
name: 'contract-meta/contract-durations.json',
|
||
});
|
||
archive.append(JSON.stringify(data.contractCategories, null, 2), {
|
||
name: 'contract-meta/contract-categories.json',
|
||
});
|
||
archive.append(JSON.stringify(data.pdfTemplates, null, 2), {
|
||
name: 'pdf-templates/pdf-templates.json',
|
||
});
|
||
|
||
// PDF-Dateien physisch hinzufügen (Pfade aus DB laden)
|
||
const uploadsRoot = path.join(process.cwd(), 'uploads');
|
||
(async () => {
|
||
try {
|
||
const templateRows = await prisma.pdfTemplate.findMany({
|
||
select: { name: true, templatePath: true },
|
||
});
|
||
const pathByName = new Map(templateRows.map((r) => [r.name, r.templatePath]));
|
||
|
||
for (const t of data.pdfTemplates) {
|
||
const storedPath = pathByName.get(t.name);
|
||
if (!storedPath) continue;
|
||
|
||
const relPath = storedPath.startsWith('/uploads/')
|
||
? storedPath.substring('/uploads/'.length)
|
||
: storedPath;
|
||
const absPath = path.join(uploadsRoot, relPath);
|
||
|
||
if (fs.existsSync(absPath)) {
|
||
archive.file(absPath, { name: `pdf-templates/${t.pdfFilename}` });
|
||
} else {
|
||
console.warn(`[factoryDefaults] PDF-Datei nicht gefunden: ${absPath}`);
|
||
}
|
||
}
|
||
archive.finalize();
|
||
} catch (err) {
|
||
reject(err);
|
||
}
|
||
})();
|
||
});
|
||
}
|