feat: include full product info and tech specs in excel datasheets
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Successful in 2m21s
Build & Deploy / 🏗️ Build (push) Successful in 3m43s
Build & Deploy / 🚀 Deploy (push) Successful in 32s
Build & Deploy / 🧪 Post-Deploy Verification (push) Failing after 5m19s
Build & Deploy / 🔔 Notify (push) Successful in 1s

This commit is contained in:
2026-03-10 11:36:59 +01:00
parent 53d1e62b42
commit dd27f77c71
43 changed files with 147 additions and 117 deletions

View File

@@ -15,178 +15,208 @@ import configPromise from '@payload-config';
import { buildExcelModel, ProductData as ExcelProductData } from './lib/excel-data-parser'; import { buildExcelModel, ProductData as ExcelProductData } from './lib/excel-data-parser';
const CONFIG = { const CONFIG = {
outputDir: path.join(process.cwd(), 'public/datasheets'), outputDir: path.join(process.cwd(), 'public/datasheets'),
} as const; } as const;
// ─── Types ────────────────────────────────────────────────────────────────────── // ─── Types ──────────────────────────────────────────────────────────────────────
interface ProductData { interface ProductData {
title: string; title: string;
slug: string; slug: string;
sku: string; sku: string;
locale: string; locale: string;
categories: string[]; categories: string[];
description: string; description: string;
technicalItems: Array<{ label: string; value: string; unit?: string }>; technicalItems: Array<{ label: string; value: string; unit?: string }>;
voltageTables: Array<{ voltageTables: Array<{
voltageLabel: string; voltageLabel: string;
metaItems: Array<{ label: string; value: string; unit?: string }>; metaItems: Array<{ label: string; value: string; unit?: string }>;
columns: Array<{ key: string; label: string; get: (rowIndex: number) => string }>; columns: Array<{ key: string; label: string; get: (rowIndex: number) => string }>;
crossSections: string[]; crossSections: string[];
}>; }>;
} }
// ─── Helpers ──────────────────────────────────────────────────────────────────── // ─── Helpers ────────────────────────────────────────────────────────────────────
function stripHtml(html: string): string { function stripHtml(html: string): string {
if (!html) return ''; if (!html) return '';
return html.replace(/<[^>]*>/g, '').trim(); return html.replace(/<[^>]*>/g, '').trim();
} }
function ensureOutputDir(): void { function ensureOutputDir(): void {
if (!fs.existsSync(CONFIG.outputDir)) { if (!fs.existsSync(CONFIG.outputDir)) {
fs.mkdirSync(CONFIG.outputDir, { recursive: true }); fs.mkdirSync(CONFIG.outputDir, { recursive: true });
} }
} }
// ─── CMS Product Loading ──────────────────────────────────────────────────────── // ─── CMS Product Loading ────────────────────────────────────────────────────────
async function fetchProductsFromCMS(locale: 'en' | 'de'): Promise<ProductData[]> { async function fetchProductsFromCMS(locale: 'en' | 'de'): Promise<ProductData[]> {
const products: ProductData[] = []; const products: ProductData[] = [];
try { try {
const payload = await getPayload({ config: configPromise }); const payload = await getPayload({ config: configPromise });
const isDev = process.env.NODE_ENV === 'development'; const isDev = process.env.NODE_ENV === 'development';
const result = await payload.find({ const result = await payload.find({
collection: 'products', collection: 'products',
where: { where: {
...(!isDev ? { _status: { equals: 'published' } } : {}), ...(!isDev ? { _status: { equals: 'published' } } : {}),
}, },
locale: locale as any, locale: locale as any,
pagination: false, pagination: false,
}); });
for (const doc of result.docs) { for (const doc of result.docs) {
if (!doc.title || !doc.slug) continue; if (!doc.title || !doc.slug) continue;
const excelProductData: ExcelProductData = { const excelProductData: ExcelProductData = {
name: String(doc.title), name: String(doc.title),
slug: String(doc.slug), slug: String(doc.slug),
sku: String(doc.sku || ''), sku: String(doc.sku || ''),
locale, locale,
}; };
const parsedModel = buildExcelModel({ product: excelProductData, locale }); const parsedModel = buildExcelModel({ product: excelProductData, locale });
products.push({ products.push({
title: String(doc.title), title: String(doc.title),
slug: String(doc.slug), slug: String(doc.slug),
sku: String(doc.sku || ''), sku: String(doc.sku || ''),
locale, locale,
categories: Array.isArray(doc.categories) categories: Array.isArray(doc.categories)
? doc.categories.map((c: any) => String(c.category || c)).filter(Boolean) ? doc.categories.map((c: any) => String(c.category || c)).filter(Boolean)
: [], : [],
description: stripHtml(String(doc.description || '')), description: stripHtml(String(doc.description || '')),
technicalItems: parsedModel.ok ? parsedModel.technicalItems : [], technicalItems: parsedModel.ok ? parsedModel.technicalItems : [],
voltageTables: parsedModel.ok ? parsedModel.voltageTables : [], voltageTables: parsedModel.ok ? parsedModel.voltageTables : [],
}); });
}
} catch (error) {
console.error(`[Payload] Failed to fetch products (${locale}):`, error);
} }
} catch (error) {
console.error(`[Payload] Failed to fetch products (${locale}):`, error);
}
return products; return products;
} }
// ─── Excel Generation ─────────────────────────────────────────────────────────── // ─── Excel Generation ───────────────────────────────────────────────────────────
function generateExcelForProduct(product: ProductData): Buffer { function generateExcelForProduct(product: ProductData): Buffer {
const workbook = XLSX.utils.book_new(); const workbook = XLSX.utils.book_new();
const l = product.locale === 'de'; const l = product.locale === 'de';
// Single sheet: all cross-sections from all voltage tables combined const allRows: any[][] = [];
const hasMultipleVoltages = product.voltageTables.length > 1;
const allRows: string[][] = [];
// Build unified header row // --- 1. Product Meta Data ---
// Use columns from the first voltage table (they're the same across tables) allRows.push([product.title]);
const refTable = product.voltageTables[0]; const categoriesLine = product.categories.join(' • ');
if (!refTable) { if (categoriesLine) {
// No voltage tables — create a minimal info sheet allRows.push([l ? 'Kategorien:' : 'Categories:', categoriesLine]);
const ws = XLSX.utils.aoa_to_sheet([ }
[product.title], if (product.sku) {
[l ? 'Keine Querschnittsdaten verfügbar' : 'No cross-section data available'], allRows.push([l ? 'Artikelnummer:' : 'SKU:', product.sku]);
]); }
ws['!cols'] = [{ wch: 40 }]; allRows.push([]); // blank row
XLSX.utils.book_append_sheet(workbook, ws, product.title.substring(0, 31));
return Buffer.from(XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' })); // --- 2. Application / Description ---
if (product.description) {
allRows.push([l ? 'ANWENDUNG' : 'APPLICATION']);
allRows.push([product.description]);
allRows.push([]); // blank row
}
// --- 3. Technical Specifications ---
if (product.technicalItems && product.technicalItems.length > 0) {
allRows.push([l ? 'TECHNISCHE DATEN' : 'TECHNICAL DATA']);
for (const item of product.technicalItems) {
const val = item.unit ? `${item.value} ${item.unit}` : item.value;
allRows.push([item.label, val]);
} }
allRows.push([]); // blank row
}
// --- 4. Cross-section Configurations ---
const hasMultipleVoltages = product.voltageTables.length > 1;
if (product.voltageTables.length > 0) {
allRows.push([l ? 'KONFIGURATIONEN' : 'CONFIGURATIONS']);
const refTable = product.voltageTables[0];
const headers: string[] = [ const headers: string[] = [
l ? 'Querschnitt' : 'Cross-section', l ? 'Querschnitt' : 'Cross-section',
...(hasMultipleVoltages ? [l ? 'Spannung' : 'Voltage'] : []), ...(hasMultipleVoltages ? [l ? 'Spannung' : 'Voltage'] : []),
...refTable.columns.map(c => c.label), ...refTable.columns.map((c) => c.label),
]; ];
allRows.push(headers); allRows.push(headers);
// Merge rows from all voltage tables
for (const table of product.voltageTables) { for (const table of product.voltageTables) {
for (let rowIdx = 0; rowIdx < table.crossSections.length; rowIdx++) { for (let rowIdx = 0; rowIdx < table.crossSections.length; rowIdx++) {
const row: string[] = [ const row: string[] = [
table.crossSections[rowIdx], table.crossSections[rowIdx],
...(hasMultipleVoltages ? [table.voltageLabel] : []), ...(hasMultipleVoltages ? [table.voltageLabel] : []),
...table.columns.map(c => c.get(rowIdx) || '-'), ...table.columns.map((c) => c.get(rowIdx) || '-'),
]; ];
allRows.push(row); allRows.push(row);
} }
} }
} else {
allRows.push([l ? 'Keine Querschnittsdaten verfügbar' : 'No cross-section data available']);
}
const ws = XLSX.utils.aoa_to_sheet(allRows); const ws = XLSX.utils.aoa_to_sheet(allRows);
// Auto-width: first col wider for cross-section labels
ws['!cols'] = headers.map((_, i) => ({ wch: i === 0 ? 30 : 18 }));
const sheetName = product.title.substring(0, 31); // Auto-width: Col 0 wide for description, headers.
XLSX.utils.book_append_sheet(workbook, ws, sheetName); ws['!cols'] = [
{ wch: 45 },
{ wch: 20 },
{ wch: 15 },
{ wch: 15 },
{ wch: 15 },
{ wch: 15 },
{ wch: 15 },
{ wch: 15 },
];
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }); const sheetName = product.title.substring(0, 31);
return Buffer.from(buffer); XLSX.utils.book_append_sheet(workbook, ws, sheetName);
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' });
return Buffer.from(buffer);
} }
// ─── Main ─────────────────────────────────────────────────────────────────────── // ─── Main ───────────────────────────────────────────────────────────────────────
async function main(): Promise<void> { async function main(): Promise<void> {
const start = Date.now(); const start = Date.now();
console.log('Starting Excel datasheet generation (Legacy Excel Source)'); console.log('Starting Excel datasheet generation (Legacy Excel Source)');
ensureOutputDir(); ensureOutputDir();
const locales: Array<'en' | 'de'> = ['en', 'de']; const locales: Array<'en' | 'de'> = ['en', 'de'];
let generated = 0; let generated = 0;
for (const locale of locales) { for (const locale of locales) {
console.log(`\n[${locale.toUpperCase()}] Fetching products...`); console.log(`\n[${locale.toUpperCase()}] Fetching products...`);
const products = await fetchProductsFromCMS(locale); const products = await fetchProductsFromCMS(locale);
console.log(`Found ${products.length} products.`); console.log(`Found ${products.length} products.`);
for (const product of products) { for (const product of products) {
try { try {
const buffer = generateExcelForProduct(product); const buffer = generateExcelForProduct(product);
const fileName = `${product.slug}-${locale}.xlsx`; const fileName = `${product.slug}-${locale}.xlsx`;
const subfolder = path.join(CONFIG.outputDir, 'products'); const subfolder = path.join(CONFIG.outputDir, 'products');
if (!fs.existsSync(subfolder)) fs.mkdirSync(subfolder, { recursive: true }); if (!fs.existsSync(subfolder)) fs.mkdirSync(subfolder, { recursive: true });
fs.writeFileSync(path.join(subfolder, fileName), buffer); fs.writeFileSync(path.join(subfolder, fileName), buffer);
console.log(`✓ Generated: ${fileName}`); console.log(`✓ Generated: ${fileName}`);
generated++; generated++;
} catch (error) { } catch (error) {
console.error(`✗ Failed for ${product.title}:`, error); console.error(`✗ Failed for ${product.title}:`, error);
} }
}
} }
}
console.log(`\n✅ Done! Generated ${generated} files.`); console.log(`\n✅ Done! Generated ${generated} files.`);
console.log(`Output: ${CONFIG.outputDir}`); console.log(`Output: ${CONFIG.outputDir}`);
console.log(`Time: ${((Date.now() - start) / 1000).toFixed(2)}s`); console.log(`Time: ${((Date.now() - start) / 1000).toFixed(2)}s`);
} }
main().catch(console.error); main().catch(console.error);