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
223 lines
7.4 KiB
TypeScript
223 lines
7.4 KiB
TypeScript
#!/usr/bin/env ts-node
|
|
/**
|
|
* Excel Datasheet Generator
|
|
*
|
|
* Generates per-product .xlsx datasheets using ONLY data from Payload CMS.
|
|
* No external Excel files required.
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
import * as XLSX from 'xlsx';
|
|
import { getPayload } from 'payload';
|
|
import configPromise from '@payload-config';
|
|
import { buildExcelModel, ProductData as ExcelProductData } from './lib/excel-data-parser';
|
|
|
|
const CONFIG = {
|
|
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[];
|
|
}>;
|
|
}
|
|
|
|
// ─── Helpers ────────────────────────────────────────────────────────────────────
|
|
|
|
function stripHtml(html: string): string {
|
|
if (!html) return '';
|
|
return html.replace(/<[^>]*>/g, '').trim();
|
|
}
|
|
|
|
function ensureOutputDir(): void {
|
|
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 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;
|
|
|
|
const excelProductData: ExcelProductData = {
|
|
name: String(doc.title),
|
|
slug: String(doc.slug),
|
|
sku: String(doc.sku || ''),
|
|
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);
|
|
}
|
|
|
|
return products;
|
|
}
|
|
|
|
// ─── Excel Generation ───────────────────────────────────────────────────────────
|
|
|
|
function generateExcelForProduct(product: ProductData): Buffer {
|
|
const workbook = XLSX.utils.book_new();
|
|
const l = product.locale === 'de';
|
|
|
|
const allRows: any[][] = [];
|
|
|
|
// --- 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),
|
|
];
|
|
allRows.push(headers);
|
|
|
|
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);
|
|
}
|
|
}
|
|
} else {
|
|
allRows.push([l ? 'Keine Querschnittsdaten verfügbar' : 'No cross-section data available']);
|
|
}
|
|
|
|
const ws = XLSX.utils.aoa_to_sheet(allRows);
|
|
|
|
// 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 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 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 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 });
|
|
|
|
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`);
|
|
}
|
|
|
|
main().catch(console.error);
|