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