feature/excel #1
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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