feat: product catalog
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Failing after 2m15s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Failing after 2m15s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 1s
This commit is contained in:
41
scripts/debug-cms.ts
Normal file
41
scripts/debug-cms.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { getPayload } from 'payload';
|
||||
import configPromise from '@payload-config';
|
||||
|
||||
async function main() {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const result = await payload.find({
|
||||
collection: 'products',
|
||||
where: { slug: { equals: 'n2xsy' } },
|
||||
locale: 'en' as any,
|
||||
});
|
||||
const doc = result.docs[0];
|
||||
if (!doc) { console.log('No doc found'); process.exit(0); }
|
||||
console.log('--- doc.title:', doc.title);
|
||||
|
||||
if (doc.content?.root?.children) {
|
||||
const children = doc.content.root.children as any[];
|
||||
console.log(`--- ${children.length} children found`);
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i];
|
||||
console.log(`\n[${i}] type=${child.type} blockType=${child.blockType}`);
|
||||
if (child.fields) {
|
||||
console.log(' fields keys:', Object.keys(child.fields));
|
||||
if (child.fields.items) console.log(' fields.items count:', child.fields.items.length);
|
||||
if (child.fields.technicalItems) console.log(' fields.technicalItems count:', child.fields.technicalItems.length);
|
||||
if (child.fields.voltageTables) console.log(' fields.voltageTables count:', child.fields.voltageTables.length);
|
||||
}
|
||||
// Also check top-level (in case fields are flat)
|
||||
const topKeys = Object.keys(child).filter(k => !['children', 'type', 'version', 'format', 'indent', 'direction', 'textFormat', 'textStyle', 'fields'].includes(k));
|
||||
if (topKeys.length > 0) console.log(' top-level keys:', topKeys);
|
||||
if (child.items) console.log(' items (top-level) count:', child.items.length);
|
||||
if (child.technicalItems) console.log(' technicalItems (top-level) count:', child.technicalItems.length);
|
||||
if (child.voltageTables) console.log(' voltageTables (top-level) count:', child.voltageTables.length);
|
||||
}
|
||||
} else {
|
||||
console.log('No content.root.children');
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((e) => { console.error(e); process.exit(1); });
|
||||
24
scripts/debug-product.ts
Normal file
24
scripts/debug-product.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { getPayload } from 'payload';
|
||||
import configPromise from '../payload.config';
|
||||
|
||||
async function debug() {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const result = await payload.find({
|
||||
collection: 'products',
|
||||
where: {
|
||||
slug: { equals: 'na2xsy' }
|
||||
},
|
||||
locale: 'de'
|
||||
});
|
||||
|
||||
if (result.docs.length > 0) {
|
||||
const doc = result.docs[0];
|
||||
console.log('Product:', doc.title);
|
||||
console.log('Content Blocks:', JSON.stringify(doc.content?.root?.children?.filter((n: any) => n.type === 'block'), null, 2));
|
||||
} else {
|
||||
console.log('Product not found');
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
debug();
|
||||
459
scripts/generate-brochure.ts
Normal file
459
scripts/generate-brochure.ts
Normal file
@@ -0,0 +1,459 @@
|
||||
#!/usr/bin/env ts-node
|
||||
/**
|
||||
* Brochure Generator
|
||||
*
|
||||
* Generates a complete product catalog PDF brochure combining all products
|
||||
* with company information, using ONLY data from Payload CMS.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as React from 'react';
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { getPayload } from 'payload';
|
||||
import configPromise from '@payload-config';
|
||||
import { renderToBuffer } from '@react-pdf/renderer';
|
||||
// pdf-lib removed: no longer bundling individual datasheets
|
||||
|
||||
import { PDFBrochure, type BrochureProduct, type BrochureProps } from '../lib/pdf-brochure';
|
||||
import { getDatasheetPath } from '../lib/datasheets';
|
||||
import { mapFileSlugToTranslated } from '../lib/slugs';
|
||||
|
||||
const CONFIG = {
|
||||
outputDir: path.join(process.cwd(), 'public/brochure'),
|
||||
host: process.env.NEXT_PUBLIC_SITE_URL || 'https://klz-cables.com',
|
||||
} as const;
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
async function resolveImage(url: string): Promise<string | Buffer> {
|
||||
if (!url) return '';
|
||||
let localPath = '';
|
||||
// If it's a Payload media URL like /api/media/file/filename.ext
|
||||
if (url.startsWith('/api/media/file/')) {
|
||||
const filename = url.replace('/api/media/file/', '');
|
||||
localPath = path.join(process.cwd(), 'public/media', filename);
|
||||
} else if (url.startsWith('/media/')) {
|
||||
localPath = path.join(process.cwd(), 'public', url);
|
||||
}
|
||||
|
||||
if (localPath && fs.existsSync(localPath)) {
|
||||
// If it's webp, convert to png buffer for react-pdf
|
||||
if (localPath.toLowerCase().endsWith('.webp')) {
|
||||
try {
|
||||
return await sharp(localPath).png().toBuffer();
|
||||
} catch (err) {
|
||||
return localPath;
|
||||
}
|
||||
}
|
||||
return localPath;
|
||||
}
|
||||
// Fallback to absolute URL if starting with /
|
||||
if (url.startsWith('/')) return `${CONFIG.host}${url}`;
|
||||
return url;
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchQrCodeBuffer(url: string): Promise<Buffer | undefined> {
|
||||
if (!url) return undefined;
|
||||
try {
|
||||
const qrApi = `https://api.qrserver.com/v1/create-qr-code/?size=80x80&data=${encodeURIComponent(url)}&margin=0`;
|
||||
const res = await fetch(qrApi);
|
||||
if (res.ok) {
|
||||
const arrayBuffer = await res.arrayBuffer();
|
||||
return Buffer.from(arrayBuffer);
|
||||
} else {
|
||||
console.error(` [QR] Failed (HTTP ${res.status}) for ${url}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(` [QR] Failed for ${url}:`, err);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function resolveLocalFile(relativePath: string): Promise<string | Buffer | undefined> {
|
||||
const abs = path.join(process.cwd(), 'public', relativePath);
|
||||
if (!fs.existsSync(abs)) return undefined;
|
||||
if (abs.endsWith('.svg')) {
|
||||
try {
|
||||
const svgBuf = fs.readFileSync(abs);
|
||||
return await sharp(svgBuf).resize(600).png().toBuffer();
|
||||
} catch { return abs; }
|
||||
}
|
||||
return abs;
|
||||
}
|
||||
|
||||
// ─── CMS Product Loading ────────────────────────────────────────────────────
|
||||
|
||||
async function loadProducts(locale: 'en' | 'de'): Promise<BrochureProduct[]> {
|
||||
const products: BrochureProduct[] = [];
|
||||
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 productsSlug = await mapFileSlugToTranslated('products', locale);
|
||||
|
||||
let id = 1;
|
||||
for (const doc of result.docs) {
|
||||
if (!doc.title || !doc.slug) continue;
|
||||
|
||||
const images: any[] = [];
|
||||
const rawImages: string[] = [];
|
||||
|
||||
if (doc.featuredImage) {
|
||||
const url = typeof doc.featuredImage === 'string' ? doc.featuredImage : (doc.featuredImage as any).url;
|
||||
if (url) rawImages.push(url);
|
||||
}
|
||||
if (Array.isArray(doc.images)) {
|
||||
for (const img of doc.images) {
|
||||
const url = typeof img === 'string' ? img : (img as any).url;
|
||||
if (url && !rawImages.includes(url)) rawImages.push(url);
|
||||
}
|
||||
}
|
||||
|
||||
for (const url of rawImages) {
|
||||
const resolved = await resolveImage(url);
|
||||
if (resolved) images.push(resolved);
|
||||
}
|
||||
|
||||
const attributes: any[] = [];
|
||||
// Extract basic technical attributes from Lexical AST
|
||||
if (Array.isArray(doc.content?.root?.children)) {
|
||||
const productTabsBlock = doc.content.root.children.find(
|
||||
(node: any) => node.type === 'block' && node.fields?.blockType === 'productTabs'
|
||||
);
|
||||
|
||||
if (productTabsBlock && productTabsBlock.fields) {
|
||||
if (Array.isArray(productTabsBlock.fields.technicalItems)) {
|
||||
for (const item of productTabsBlock.fields.technicalItems) {
|
||||
const label = item.unit ? `${item.label} [${item.unit}]` : item.label;
|
||||
if (label && item.value) {
|
||||
attributes.push({ name: label, options: [item.value] });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Array.isArray(productTabsBlock.fields.voltageTables)) {
|
||||
for (const vt of productTabsBlock.fields.voltageTables) {
|
||||
if (vt.voltageLabel) {
|
||||
attributes.push({ name: 'Voltage', options: [vt.voltageLabel] });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const categories = Array.isArray(doc.categories)
|
||||
? doc.categories.map((c: any) => ({ name: String(c.category || c), slug: String(c.slug || c) })).filter((c: any) => c.name)
|
||||
: [];
|
||||
|
||||
// Compute QR URLs
|
||||
let qrWebsiteUrl = '';
|
||||
if (categories.length > 0 && categories[0].slug) {
|
||||
const catTranslatedSlug = await mapFileSlugToTranslated(categories[0].slug, locale);
|
||||
qrWebsiteUrl = `${CONFIG.host}/${locale}/${productsSlug}/${catTranslatedSlug}/${doc.slug}`;
|
||||
}
|
||||
|
||||
let qrDatasheetUrl = '';
|
||||
const datasheetRelativePath = getDatasheetPath(String(doc.slug), locale);
|
||||
if (datasheetRelativePath) {
|
||||
qrDatasheetUrl = `${CONFIG.host}${datasheetRelativePath}`;
|
||||
}
|
||||
|
||||
const [qrWebsite, qrDatasheet] = await Promise.all([
|
||||
qrWebsiteUrl ? fetchQrCodeBuffer(qrWebsiteUrl) : Promise.resolve(undefined),
|
||||
qrDatasheetUrl ? fetchQrCodeBuffer(qrDatasheetUrl) : Promise.resolve(undefined),
|
||||
]);
|
||||
|
||||
products.push({
|
||||
id: id++,
|
||||
name: String(doc.title),
|
||||
slug: String(doc.slug),
|
||||
sku: String(doc.sku || ''),
|
||||
shortDescriptionHtml: '',
|
||||
descriptionHtml: stripHtml(String(doc.description || '')),
|
||||
images: images as any, // mix of paths and buffers
|
||||
featuredImage: images[0] || null,
|
||||
categories,
|
||||
attributes,
|
||||
qrWebsite,
|
||||
qrDatasheet,
|
||||
});
|
||||
console.log(` - ${doc.title} (QR: ${qrWebsite ? 'Web ' : ''}${qrDatasheet ? 'PDF' : ''})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Payload] Failed to fetch products (${locale}):`, error);
|
||||
}
|
||||
|
||||
products.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// FILTER: Only include products that have images for the high-fidelity brochure
|
||||
const filteredProducts = products.filter(p => p.images.length > 0 || p.featuredImage);
|
||||
console.log(` Filtered: ${filteredProducts.length} products with images (out of ${products.length})`);
|
||||
|
||||
return filteredProducts;
|
||||
}
|
||||
|
||||
// ─── CMS Start/Intro Page ───────────────────────────────────────────────────
|
||||
|
||||
async function loadIntroContent(locale: 'en' | 'de'): Promise<BrochureProps['introContent'] | undefined> {
|
||||
try {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const result = await payload.find({
|
||||
collection: 'pages',
|
||||
where: { slug: { equals: 'start' } },
|
||||
locale: locale as any,
|
||||
});
|
||||
|
||||
if (result.docs.length > 0) {
|
||||
const doc = result.docs[0];
|
||||
const heroUrl = typeof doc.featuredImage === 'string'
|
||||
? doc.featuredImage
|
||||
: (doc.featuredImage as any)?.url;
|
||||
|
||||
const heroImage = await resolveImage(heroUrl);
|
||||
|
||||
return {
|
||||
title: String(doc.title),
|
||||
excerpt: String(doc.excerpt || ''),
|
||||
heroImage: heroImage as any,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Payload] Failed to fetch intro content (${locale}):`, error);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ─── Marketing Sections ───────────────────────────────────────────────────
|
||||
|
||||
async function loadMarketingSections(locale: 'en' | 'de'): Promise<BrochureProps['marketingSections'] | undefined> {
|
||||
try {
|
||||
const messagesPath = path.join(process.cwd(), `messages/${locale}.json`);
|
||||
const messagesJson = fs.readFileSync(messagesPath, 'utf-8');
|
||||
const messages = JSON.parse(messagesJson);
|
||||
|
||||
const sections: NonNullable<BrochureProps['marketingSections']> = [];
|
||||
|
||||
// 1. What we do — short label, long subtitle becomes the description
|
||||
if (messages.Home?.whatWeDo) {
|
||||
const label = locale === 'de' ? 'Unser Angebot' : 'Our Services';
|
||||
sections.push({
|
||||
title: messages.Home.whatWeDo.title,
|
||||
subtitle: label,
|
||||
description: messages.Home.whatWeDo.subtitle,
|
||||
items: messages.Home.whatWeDo.items,
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Our Legacy — with stats highlight
|
||||
if (messages.Team?.legacy) {
|
||||
const label = locale === 'de' ? 'Unsere Geschichte' : 'Our Story';
|
||||
sections.push({
|
||||
title: messages.Team.legacy.title,
|
||||
subtitle: label,
|
||||
description: `${messages.Team.legacy.p1}\n\n${messages.Team.legacy.p2}`,
|
||||
highlights: [
|
||||
{ value: messages.Team.legacy.expertise || 'Expertise', label: messages.Team.legacy.expertiseDesc || '' },
|
||||
{ value: messages.Team.legacy.network || 'Netzwerk', label: messages.Team.legacy.networkDesc || '' },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Experience stats section
|
||||
if (messages.Home?.experience) {
|
||||
const label = locale === 'de' ? 'Erfahrung' : 'Experience';
|
||||
sections.push({
|
||||
title: messages.Home.experience.title,
|
||||
subtitle: label,
|
||||
description: `${messages.Home.experience.p1 || ''}\n\n${messages.Home.experience.p2 || ''}`.trim(),
|
||||
highlights: [
|
||||
{ value: messages.Home.experience.certifiedQuality || (locale === 'de' ? 'Zertifizierte Qualität' : 'Certified Quality'), label: messages.Home.experience.vdeApproved || '' },
|
||||
{ value: messages.Home.experience.fullSpectrum || (locale === 'de' ? 'Volles Spektrum' : 'Full Spectrum'), label: messages.Home.experience.solutionsRange || '' },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Why choose us
|
||||
if (messages.Home?.whyChooseUs) {
|
||||
const label = locale === 'de' ? 'Warum KLZ' : 'Why KLZ';
|
||||
sections.push({
|
||||
title: messages.Home.whyChooseUs.title,
|
||||
subtitle: label,
|
||||
description: messages.Home.whyChooseUs.subtitle || '',
|
||||
items: messages.Home.whyChooseUs.items,
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Team intro + quotes as pull quotes
|
||||
if (messages.Team?.klaus || messages.Team?.michael) {
|
||||
const label = locale === 'de' ? 'Die Geschäftsführer' : 'The Directors';
|
||||
const title = messages.Home?.meetTheTeam?.title || (locale === 'de' ? 'Das Team' : 'The Team');
|
||||
const teamItems: Array<{ title: string; description: string }> = [];
|
||||
if (messages.Team?.klaus) {
|
||||
teamItems.push({
|
||||
title: `${messages.Team.klaus.name} – ${messages.Team.klaus.role}`,
|
||||
description: messages.Team.klaus.description,
|
||||
});
|
||||
}
|
||||
if (messages.Team?.michael) {
|
||||
teamItems.push({
|
||||
title: `${messages.Team.michael.name} – ${messages.Team.michael.role}`,
|
||||
description: messages.Team.michael.description,
|
||||
});
|
||||
}
|
||||
const desc = messages.Home?.meetTheTeam?.description || '';
|
||||
sections.push({
|
||||
title,
|
||||
subtitle: label,
|
||||
description: desc,
|
||||
items: teamItems,
|
||||
pullQuote: messages.Team.klaus?.quote || messages.Team.michael?.quote || '',
|
||||
});
|
||||
}
|
||||
|
||||
// 6. Our Values (Manifesto)
|
||||
if (messages.Team?.manifesto) {
|
||||
const label = locale === 'de' ? 'Grundprinzipien' : 'Core Principles';
|
||||
sections.push({
|
||||
title: messages.Team.manifesto.title,
|
||||
subtitle: label,
|
||||
description: messages.Team.manifesto.tagline,
|
||||
items: messages.Team.manifesto.items,
|
||||
});
|
||||
}
|
||||
|
||||
return sections.length > 0 ? sections : undefined;
|
||||
} catch (error) {
|
||||
console.error(`[Messages] Failed to fetch marketing sections (${locale}):`, error);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ─── Company Info ───────────────────────────────────────────────────────────
|
||||
|
||||
function getCompanyInfo(locale: 'en' | 'de'): BrochureProps['companyInfo'] {
|
||||
const values = locale === 'de' ? [
|
||||
{ title: 'Kompetenz', description: 'Jahrzehntelange Erfahrung und europaweites Know-how.' },
|
||||
{ title: 'Verfügbarkeit', description: 'Immer für Sie da – schnelle Unterstützung.' },
|
||||
{ title: 'Lösungen', description: 'Wir finden die beste Kabellösung für Ihr Projekt.' },
|
||||
{ title: 'Zuverlässigkeit', description: 'Wir halten, was wir versprechen.' },
|
||||
] : [
|
||||
{ title: 'Competence', description: 'Decades of experience and Europe-wide know-how.' },
|
||||
{ title: 'Availability', description: 'Always there for you – fast support.' },
|
||||
{ title: 'Solutions', description: 'We find the best cable solution for your project.' },
|
||||
{ title: 'Reliability', description: 'We deliver what we promise.' },
|
||||
];
|
||||
|
||||
return {
|
||||
tagline: locale === 'de'
|
||||
? 'Wegweisend in der Kabelinfrastruktur.'
|
||||
: 'Leading the way in cable infrastructure.',
|
||||
values,
|
||||
address: 'Raiffeisenstraße 22, 73630 Remshalden, Germany',
|
||||
phone: '+49 (0) 7151 959 89-0',
|
||||
email: 'info@klz-cables.com',
|
||||
website: 'www.klz-cables.com',
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Main ───────────────────────────────────────────────────────────────────
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const start = Date.now();
|
||||
console.log('Starting brochure generation (Full Brochure with website content)');
|
||||
ensureOutputDir();
|
||||
|
||||
const locales: Array<'en' | 'de'> = ['en', 'de'];
|
||||
|
||||
// Load the REAL logos (not the favicon/icon!)
|
||||
const logoWhitePath = path.join(process.cwd(), 'public/logo-white.png');
|
||||
const logoBlackPath = path.join(process.cwd(), 'public/logo-black.png');
|
||||
const logoFallbackPath = path.join(process.cwd(), 'public/logo.png');
|
||||
|
||||
const logoWhite = fs.existsSync(logoWhitePath) ? logoWhitePath : undefined;
|
||||
const logoBlack = fs.existsSync(logoBlackPath) ? logoBlackPath : (fs.existsSync(logoFallbackPath) ? logoFallbackPath : undefined);
|
||||
|
||||
console.log(`Logos: white=${!!logoWhite} black=${!!logoBlack}`);
|
||||
|
||||
// Load gallery images — 7 diverse images for different sections
|
||||
const galleryPaths = [
|
||||
'uploads/2024/12/DSC07433-Large-600x400.webp', // 0: Cover
|
||||
'uploads/2024/12/DSC07460-Large-600x400.webp', // 1: About section
|
||||
'uploads/2025/01/technicians-inspecting-wind-turbines-in-a-green-en-2024-12-09-01-25-20-utc-scaled.webp', // 2: After "Was wir tun"
|
||||
'uploads/2025/01/power-grid-station-electrical-distribution-statio-2023-11-27-05-25-36-utc-scaled.webp', // 3: After Legacy
|
||||
'uploads/2025/01/transportation-and-logistics-trucking-2023-11-27-04-54-40-utc-scaled.webp', // 4: After Experience
|
||||
'uploads/2024/12/DSC07539-Large-600x400.webp', // 5: TOC page
|
||||
'uploads/2025/01/business-planning-hand-using-laptop-for-working-te-2024-11-01-21-25-44-utc-scaled.webp', // 6: Back cover
|
||||
];
|
||||
const galleryImages: (string | Buffer)[] = [];
|
||||
for (const gp of galleryPaths) {
|
||||
const fullPath = path.join(process.cwd(), 'public', gp);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
try {
|
||||
const buf = await sharp(fullPath).png({ quality: 80 }).resize(600).toBuffer();
|
||||
galleryImages.push(buf);
|
||||
} catch { /* skip */ }
|
||||
} else {
|
||||
galleryImages.push(Buffer.alloc(0)); // placeholder to maintain index mapping
|
||||
}
|
||||
}
|
||||
console.log(`Gallery images loaded: ${galleryImages.filter(b => (b as Buffer).length > 0).length}`);
|
||||
|
||||
for (const locale of locales) {
|
||||
console.log(`\nGenerating ${locale.toUpperCase()} brochure...`);
|
||||
const [products, introContent, marketingSections] = await Promise.all([
|
||||
loadProducts(locale),
|
||||
loadIntroContent(locale),
|
||||
loadMarketingSections(locale)
|
||||
]);
|
||||
|
||||
if (products.length === 0) continue;
|
||||
const companyInfo = getCompanyInfo(locale);
|
||||
|
||||
try {
|
||||
// Render the React-PDF brochure
|
||||
const buffer = await renderToBuffer(
|
||||
React.createElement(PDFBrochure, {
|
||||
products, locale, companyInfo, introContent,
|
||||
marketingSections, logoBlack, logoWhite, galleryImages,
|
||||
} as any) as any
|
||||
);
|
||||
|
||||
// Write final PDF
|
||||
const outPath = path.join(process.cwd(), `public/brochure/klz-product-catalog-${locale}.pdf`);
|
||||
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
||||
fs.writeFileSync(outPath, buffer);
|
||||
|
||||
const sizeKB = Math.round(buffer.length / 1024);
|
||||
console.log(` ✓ Generated: klz-product-catalog-${locale}.pdf (${sizeKB} KB)`);
|
||||
} catch (error) {
|
||||
console.error(` ✗ Failed to generate ${locale} brochure:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✅ Done!`);
|
||||
console.log(`Output: ${CONFIG.outputDir}`);
|
||||
console.log(`Time: ${((Date.now() - start) / 1000).toFixed(2)}s`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
190
scripts/generate-excel-datasheets.ts
Normal file
190
scripts/generate-excel-datasheets.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
#!/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';
|
||||
|
||||
// ── Sheet 1: Product Info ──
|
||||
const infoData: Array<[string, string]> = [
|
||||
[l ? 'Produktname' : 'Product Name', product.title],
|
||||
[l ? 'Artikelnummer' : 'SKU', product.sku],
|
||||
[l ? 'Kategorie' : 'Category', product.categories.join(', ') || '-'],
|
||||
[l ? 'Beschreibung' : 'Description', product.description || '-'],
|
||||
];
|
||||
|
||||
const infoSheet = XLSX.utils.aoa_to_sheet(infoData);
|
||||
infoSheet['!cols'] = [{ wch: 25 }, { wch: 65 }];
|
||||
XLSX.utils.book_append_sheet(workbook, infoSheet, l ? 'Produktinfo' : 'Product Info');
|
||||
|
||||
// ── Sheet 2: Technical Data ──
|
||||
if (product.technicalItems.length > 0) {
|
||||
const techData: Array<[string, string]> = product.technicalItems.map(item => {
|
||||
const label = item.unit ? `${item.label} [${item.unit}]` : item.label;
|
||||
return [label, item.value];
|
||||
});
|
||||
|
||||
const techSheet = XLSX.utils.aoa_to_sheet([
|
||||
[l ? 'Eigenschaft' : 'Property', l ? 'Wert' : 'Value'],
|
||||
...techData
|
||||
]);
|
||||
techSheet['!cols'] = [{ wch: 40 }, { wch: 60 }];
|
||||
XLSX.utils.book_append_sheet(workbook, techSheet, l ? 'Technische Daten' : 'Technical Data');
|
||||
}
|
||||
|
||||
// ── Sheet 3+: Voltage Tables ──
|
||||
for (const table of product.voltageTables) {
|
||||
const headers = ['Configuration/Cross-section', ...table.columns.map(c => c.label)];
|
||||
const dataRows = table.crossSections.map((cs, rowIndex) => {
|
||||
return [cs, ...table.columns.map(c => c.get(rowIndex) || '-')];
|
||||
});
|
||||
|
||||
const ws = XLSX.utils.aoa_to_sheet([headers, ...dataRows]);
|
||||
ws['!cols'] = headers.map(() => ({ wch: 22 }));
|
||||
|
||||
const safeName = table.voltageLabel.replace(/[:\\/?*[\]]/g, '-').trim();
|
||||
const sheetName = safeName.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);
|
||||
17
scripts/inspect-pages.ts
Normal file
17
scripts/inspect-pages.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { getPayload } from "payload";
|
||||
import configPromise from "@payload-config";
|
||||
|
||||
async function run() {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const resEn = await payload.find({ collection: "pages", locale: "en" as any });
|
||||
const resDe = await payload.find({ collection: "pages", locale: "de" as any });
|
||||
|
||||
console.log("EN Pages:");
|
||||
resEn.docs.forEach(d => console.log(`- ${d.slug}: ${d.title}`));
|
||||
console.log("DE Pages:");
|
||||
resDe.docs.forEach(d => console.log(`- ${d.slug}: ${d.title}`));
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
run();
|
||||
27
scripts/inspect-start.ts
Normal file
27
scripts/inspect-start.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { getPayload } from 'payload';
|
||||
import configPromise from '../payload.config';
|
||||
|
||||
async function inspectStart() {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const result = await payload.find({
|
||||
collection: 'pages',
|
||||
where: { slug: { equals: 'start' } },
|
||||
locale: 'de'
|
||||
});
|
||||
|
||||
if (result.docs.length > 0) {
|
||||
const doc = result.docs[0];
|
||||
console.log('Start Page:', doc.title);
|
||||
console.log('Excerpt:', doc.excerpt);
|
||||
// Print block types in content
|
||||
if (doc.content?.root?.children) {
|
||||
const blocks = doc.content.root.children.filter((n: any) => n.type === 'block');
|
||||
console.log('Blocks found:', blocks.map((b: any) => b.blockType || b.type));
|
||||
}
|
||||
} else {
|
||||
console.log('Start page not found');
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
inspectStart();
|
||||
843
scripts/lib/excel-data-parser.ts
Normal file
843
scripts/lib/excel-data-parser.ts
Normal file
@@ -0,0 +1,843 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const EXCEL_SOURCE_FILES = [
|
||||
path.join(process.cwd(), 'data/source/high-voltage.xlsx'),
|
||||
path.join(process.cwd(), 'data/source/medium-voltage-KM.xlsx'),
|
||||
path.join(process.cwd(), 'data/source/low-voltage-KM.xlsx'),
|
||||
path.join(process.cwd(), 'data/source/solar-cables.xlsx'),
|
||||
];
|
||||
|
||||
export interface ProductData {
|
||||
id?: number;
|
||||
name: string;
|
||||
slug?: string;
|
||||
sku: string;
|
||||
translationKey?: string;
|
||||
locale?: 'en' | 'de';
|
||||
}
|
||||
|
||||
export type ExcelRow = Record<string, any>;
|
||||
export type ExcelMatch = { rows: ExcelRow[]; units: Record<string, string> };
|
||||
let EXCEL_INDEX: Map<string, ExcelMatch> | null = null;
|
||||
|
||||
export type KeyValueItem = { label: string; value: string; unit?: string };
|
||||
export type VoltageTableModel = {
|
||||
voltageLabel: string;
|
||||
metaItems: KeyValueItem[];
|
||||
crossSections: string[];
|
||||
columns: Array<{ key: string; label: string; get: (rowIndex: number) => string }>;
|
||||
};
|
||||
|
||||
function stripHtml(html: string): string {
|
||||
if (!html) return '';
|
||||
return html.replace(/<[^>]*>/g, '').trim();
|
||||
}
|
||||
|
||||
function normalizeValue(value: string): string {
|
||||
return stripHtml(value).replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function formatNumber(n: number): string {
|
||||
const s = Number.isInteger(n) ? String(n) : String(n);
|
||||
return s.replace(/\.0+$/, '');
|
||||
}
|
||||
|
||||
function parseNumericOption(value: string): number | null {
|
||||
const v = normalizeValue(value).replace(/,/g, '.');
|
||||
const m = v.match(/-?\d+(?:\.\d+)?/);
|
||||
if (!m) return null;
|
||||
const n = Number(m[0]);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
|
||||
function summarizeNumericRange(options: string[] | undefined): { ok: boolean; text: string } {
|
||||
const vals = (options || []).map(parseNumericOption).filter((n): n is number => n !== null);
|
||||
if (vals.length < 3) return { ok: false, text: '' };
|
||||
const uniq = Array.from(new Set(vals));
|
||||
if (uniq.length < 4) return { ok: false, text: '' };
|
||||
uniq.sort((a, b) => a - b);
|
||||
const min = uniq[0];
|
||||
const max = uniq[uniq.length - 1];
|
||||
return { ok: true, text: `${formatNumber(min)}–${formatNumber(max)}` };
|
||||
}
|
||||
|
||||
function summarizeOptions(options: string[] | undefined, maxItems: number = 3): string {
|
||||
const vals = (options || []).map(normalizeValue).filter(Boolean);
|
||||
if (vals.length === 0) return '';
|
||||
const uniq = Array.from(new Set(vals));
|
||||
if (uniq.length === 1) return uniq[0];
|
||||
if (uniq.length <= maxItems) return uniq.join(' / ');
|
||||
return `${uniq.slice(0, maxItems).join(' / ')} / ...`;
|
||||
}
|
||||
|
||||
function summarizeSmartOptions(label: string, options: string[] | undefined): string {
|
||||
const range = summarizeNumericRange(options);
|
||||
if (range.ok) return range.text;
|
||||
return summarizeOptions(options, 3);
|
||||
}
|
||||
|
||||
function looksNumeric(value: string): boolean {
|
||||
const v = normalizeValue(value).replace(/,/g, '.');
|
||||
return /^-?\d+(?:\.\d+)?$/.test(v);
|
||||
}
|
||||
|
||||
function normalizeUnit(unitRaw: string): string {
|
||||
const u = normalizeValue(unitRaw);
|
||||
if (!u) return '';
|
||||
if (/^c$/i.test(u) || /^°c$/i.test(u)) return '°C';
|
||||
return u
|
||||
.replace(/Ω/gi, 'Ohm')
|
||||
.replace(/[\u00B5\u03BC]/g, 'u');
|
||||
}
|
||||
|
||||
function denseAbbrevLabel(args: {
|
||||
key: string;
|
||||
locale: 'en' | 'de';
|
||||
unit?: string;
|
||||
withUnit?: boolean;
|
||||
}): string {
|
||||
const u = normalizeUnit(args.unit || '');
|
||||
const withUnit = args.withUnit ?? true;
|
||||
const unitSafe = u
|
||||
.replace(/Ω/gi, 'Ohm')
|
||||
.replace(/[\u00B5\u03BC]/g, 'u');
|
||||
const suffix = withUnit && unitSafe ? ` [${unitSafe}]` : '';
|
||||
|
||||
switch (args.key) {
|
||||
case 'DI':
|
||||
case 'RI':
|
||||
case 'Wi':
|
||||
case 'Ibl':
|
||||
case 'Ibe':
|
||||
case 'Wm':
|
||||
case 'Rbv':
|
||||
case 'Fzv':
|
||||
case 'G':
|
||||
return `${args.key}${suffix}`;
|
||||
case 'Ik_cond':
|
||||
return `Ik${suffix}`;
|
||||
case 'Ik_screen':
|
||||
return `Ik_s${suffix}`;
|
||||
case 'Ø':
|
||||
return `Ø${suffix}`;
|
||||
case 'Cond':
|
||||
return args.locale === 'de' ? 'Leiter' : 'Cond.';
|
||||
case 'shape':
|
||||
return args.locale === 'de' ? 'Form' : 'Shape';
|
||||
case 'cap':
|
||||
return `C${suffix}`;
|
||||
case 'X':
|
||||
return `X${suffix}`;
|
||||
case 'temp_range':
|
||||
return `T${suffix}`;
|
||||
case 'max_op_temp':
|
||||
return `T_op${suffix}`;
|
||||
case 'max_sc_temp':
|
||||
return `T_sc${suffix}`;
|
||||
case 'min_store_temp':
|
||||
return `T_st${suffix}`;
|
||||
case 'min_lay_temp':
|
||||
return `T_lay${suffix}`;
|
||||
case 'cpr':
|
||||
return `CPR${suffix}`;
|
||||
case 'flame':
|
||||
return `FR${suffix}`;
|
||||
case 'test_volt':
|
||||
return `U_test${suffix}`;
|
||||
case 'rated_volt':
|
||||
return `U0/U${suffix}`;
|
||||
default:
|
||||
return args.key || '';
|
||||
}
|
||||
}
|
||||
|
||||
function formatExcelHeaderLabel(key: string, unit?: string): string {
|
||||
const k = normalizeValue(key);
|
||||
if (!k) return '';
|
||||
const u = normalizeValue(unit || '');
|
||||
|
||||
const compact = k
|
||||
.replace(/\s*\(approx\.?\)\s*/gi, ' (approx.) ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
if (!u) return compact;
|
||||
if (new RegExp(`\\(${u.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&')}\\)`, 'i').test(compact)) return compact;
|
||||
return `${compact} (${u})`;
|
||||
}
|
||||
|
||||
function metaFullLabel(args: { key: string; excelKey: string; locale: 'en' | 'de' }): string {
|
||||
const key = normalizeValue(args.key);
|
||||
if (args.locale === 'de') {
|
||||
switch (key) {
|
||||
case 'test_volt':
|
||||
return 'Prüfspannung';
|
||||
case 'temp_range':
|
||||
return 'Temperaturbereich';
|
||||
case 'max_op_temp':
|
||||
return 'Leitertemperatur (max.)';
|
||||
case 'max_sc_temp':
|
||||
return 'Kurzschlusstemperatur (max.)';
|
||||
case 'min_lay_temp':
|
||||
return 'Minimale Verlegetemperatur';
|
||||
case 'min_store_temp':
|
||||
return 'Minimale Lagertemperatur';
|
||||
case 'cpr':
|
||||
return 'CPR-Klasse';
|
||||
case 'flame':
|
||||
return 'Flammhemmend';
|
||||
default:
|
||||
return formatExcelHeaderLabel(args.excelKey);
|
||||
}
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case 'test_volt':
|
||||
return 'Test voltage';
|
||||
case 'temp_range':
|
||||
return 'Operating temperature range';
|
||||
case 'max_op_temp':
|
||||
return 'Conductor temperature (max.)';
|
||||
case 'max_sc_temp':
|
||||
return 'Short-circuit temperature (max.)';
|
||||
case 'min_lay_temp':
|
||||
return 'Minimum laying temperature';
|
||||
case 'min_store_temp':
|
||||
return 'Minimum storage temperature';
|
||||
case 'cpr':
|
||||
return 'CPR class';
|
||||
case 'flame':
|
||||
return 'Flame retardant';
|
||||
default:
|
||||
return formatExcelHeaderLabel(args.excelKey);
|
||||
}
|
||||
}
|
||||
|
||||
function technicalFullLabel(args: { key: string; excelKey: string; locale: 'en' | 'de' }): string {
|
||||
const k = normalizeValue(args.key);
|
||||
|
||||
if (args.locale === 'de') {
|
||||
switch (k) {
|
||||
case 'DI': return 'Durchmesser über Isolierung';
|
||||
case 'RI': return 'DC-Leiterwiderstand (20 °C)';
|
||||
case 'Wi': return 'Isolationsdicke';
|
||||
case 'Ibl': return 'Strombelastbarkeit in Luft (trefoil)';
|
||||
case 'Ibe': return 'Strombelastbarkeit im Erdreich (trefoil)';
|
||||
case 'Ik_cond': return 'Kurzschlussstrom Leiter';
|
||||
case 'Ik_screen': return 'Kurzschlussstrom Schirm';
|
||||
case 'Wm': return 'Manteldicke';
|
||||
case 'Rbv': return 'Biegeradius (min.)';
|
||||
case 'Ø': return 'Außen-Ø';
|
||||
case 'Fzv': return 'Zugkraft (max.)';
|
||||
case 'G': return 'Gewicht';
|
||||
case 'Cond':
|
||||
case 'conductor': return 'Leiter';
|
||||
case 'shape': return 'Leiterform';
|
||||
case 'insulation': return 'Isolierung';
|
||||
case 'sheath': return 'Mantel';
|
||||
case 'cap': return 'Kapazität';
|
||||
case 'ind_trefoil': return 'Induktivität (trefoil)';
|
||||
case 'ind_air_flat': return 'Induktivität (Luft, flach)';
|
||||
case 'ind_ground_flat': return 'Induktivität (Erdreich, flach)';
|
||||
case 'X': return 'Reaktanz';
|
||||
case 'test_volt': return 'Prüfspannung';
|
||||
case 'rated_volt': return 'Nennspannung';
|
||||
case 'temp_range': return 'Temperaturbereich';
|
||||
case 'max_op_temp': return 'Leitertemperatur (max.)';
|
||||
case 'max_sc_temp': return 'Kurzschlusstemperatur (max.)';
|
||||
case 'min_store_temp': return 'Minimale Lagertemperatur';
|
||||
case 'min_lay_temp': return 'Minimale Verlegetemperatur';
|
||||
case 'cpr': return 'CPR-Klasse';
|
||||
case 'flame': return 'Flammhemmend';
|
||||
case 'packaging': return 'Verpackung';
|
||||
case 'ce': return 'CE-Konformität';
|
||||
case 'norm': return 'Norm';
|
||||
case 'standard': return 'Standard';
|
||||
case 'D_screen': return 'Durchmesser über Schirm';
|
||||
case 'S_screen': return 'Metallischer Schirm';
|
||||
default: break;
|
||||
}
|
||||
|
||||
const raw = normalizeValue(args.excelKey);
|
||||
if (!raw) return '';
|
||||
return raw
|
||||
.replace(/\(approx\.?\)/gi, '(ca.)')
|
||||
.replace(/\bcapacitance\b/gi, 'Kapazität')
|
||||
.replace(/\binductance\b/gi, 'Induktivität')
|
||||
.replace(/\breactance\b/gi, 'Reaktanz')
|
||||
.replace(/\btest voltage\b/gi, 'Prüfspannung')
|
||||
.replace(/\brated voltage\b/gi, 'Nennspannung')
|
||||
.replace(/\boperating temperature range\b/gi, 'Temperaturbereich')
|
||||
.replace(/\bminimum sheath thickness\b/gi, 'Manteldicke (min.)')
|
||||
.replace(/\bsheath thickness\b/gi, 'Manteldicke')
|
||||
.replace(/\bnominal insulation thickness\b/gi, 'Isolationsdicke (nom.)')
|
||||
.replace(/\binsulation thickness\b/gi, 'Isolationsdicke')
|
||||
.replace(/\bdc resistance at 20\s*°?c\b/gi, 'DC-Leiterwiderstand (20 °C)')
|
||||
.replace(/\bouter diameter(?: of cable)?\b/gi, 'Außen-Ø')
|
||||
.replace(/\bbending radius\b/gi, 'Biegeradius')
|
||||
.replace(/\bpackaging\b/gi, 'Verpackung')
|
||||
.replace(/\bce\s*-?conformity\b/gi, 'CE-Konformität');
|
||||
}
|
||||
|
||||
return normalizeValue(args.excelKey);
|
||||
}
|
||||
|
||||
function compactNumericForLocale(value: string, locale: 'en' | 'de'): string {
|
||||
const v = normalizeValue(value);
|
||||
if (!v) return '';
|
||||
|
||||
if (/\d+xD/.test(v)) {
|
||||
const numbers = [];
|
||||
const matches = Array.from(v.matchAll(/(\d+)xD/g));
|
||||
for (let i = 0; i < matches.length; i++) numbers.push(matches[i][1]);
|
||||
if (numbers.length > 0) {
|
||||
const unique: string[] = [];
|
||||
for (const num of numbers) {
|
||||
if (!unique.includes(num)) {
|
||||
unique.push(num);
|
||||
}
|
||||
}
|
||||
return unique.join('/') + 'xD';
|
||||
}
|
||||
}
|
||||
|
||||
const hasDigit = /\d/.test(v);
|
||||
if (!hasDigit) return v;
|
||||
const trimmed = v.replace(/\s+/g, ' ').trim();
|
||||
const parts = trimmed.split(/(–|-)/);
|
||||
const out = parts.map(p => {
|
||||
if (p === '–' || p === '-') return p;
|
||||
const s = p.trim();
|
||||
if (!/^-?\d+(?:[\.,]\d+)?$/.test(s)) return p;
|
||||
const n = s.replace(/,/g, '.');
|
||||
|
||||
const compact = n
|
||||
.replace(/\.0+$/, '')
|
||||
.replace(/(\.\d*?)0+$/, '$1')
|
||||
.replace(/\.$/, '');
|
||||
|
||||
const hadPlus = /^\+/.test(s);
|
||||
const withPlus = hadPlus && !/^\+/.test(compact) ? `+${compact}` : compact;
|
||||
return locale === 'de' ? withPlus.replace(/\./g, ',') : withPlus;
|
||||
});
|
||||
return out.join('');
|
||||
}
|
||||
|
||||
function compactCellForDenseTable(value: string, unit: string | undefined, locale: 'en' | 'de'): string {
|
||||
let v = normalizeValue(value);
|
||||
if (!v) return '';
|
||||
const u = normalizeValue(unit || '');
|
||||
if (u) {
|
||||
const esc = u.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
v = v.replace(new RegExp(`\\s*${esc}\\b`, 'ig'), '').trim();
|
||||
v = v
|
||||
.replace(/\bkg\s*\/\s*km\b/gi, '')
|
||||
.replace(/\bohm\s*\/\s*km\b/gi, '')
|
||||
.replace(/\bΩ\s*\/\s*km\b/gi, '')
|
||||
.replace(/\bu\s*f\s*\/\s*km\b/gi, '')
|
||||
.replace(/\bmh\s*\/\s*km\b/gi, '')
|
||||
.replace(/\bkA\b/gi, '')
|
||||
.replace(/\bmm\b/gi, '')
|
||||
.replace(/\bkv\b/gi, '')
|
||||
.replace(/\b°?c\b/gi, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
v = v
|
||||
.replace(/\s*–\s*/g, '-')
|
||||
.replace(/\s*-\s*/g, '-')
|
||||
.replace(/\s*\/\s*/g, '/')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
return compactNumericForLocale(v, locale);
|
||||
}
|
||||
|
||||
function normalizeVoltageLabel(raw: string): string {
|
||||
const v = normalizeValue(raw);
|
||||
if (!v) return '';
|
||||
const cleaned = v.replace(/\s+/g, ' ');
|
||||
if (/\bkv\b/i.test(cleaned)) return cleaned.replace(/\bkv\b/i, 'kV');
|
||||
const num = cleaned.match(/\d+(?:[\.,]\d+)?(?:\s*\/\s*\d+(?:[\.,]\d+)?)?/);
|
||||
if (!num) return cleaned;
|
||||
if (/[a-z]/i.test(cleaned)) return cleaned;
|
||||
return `${cleaned} kV`;
|
||||
}
|
||||
|
||||
function parseVoltageSortKey(voltageLabel: string): number {
|
||||
const v = normalizeVoltageLabel(voltageLabel);
|
||||
const nums = v
|
||||
.replace(/,/g, '.')
|
||||
.match(/\d+(?:\.\d+)?/g)
|
||||
?.map(n => Number(n))
|
||||
.filter(n => Number.isFinite(n));
|
||||
if (!nums || nums.length === 0) return Number.POSITIVE_INFINITY;
|
||||
return nums[nums.length - 1];
|
||||
}
|
||||
|
||||
function normalizeExcelKey(value: string): string {
|
||||
return String(value || '')
|
||||
.toUpperCase()
|
||||
.replace(/-\d+$/g, '')
|
||||
.replace(/[^A-Z0-9]+/g, '');
|
||||
}
|
||||
|
||||
function loadExcelRows(filePath: string): ExcelRow[] {
|
||||
const out = execSync(`npx -y xlsx-cli -j "${filePath}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
||||
const trimmed = out.trim();
|
||||
const jsonStart = trimmed.indexOf('[');
|
||||
if (jsonStart < 0) return [];
|
||||
const jsonText = trimmed.slice(jsonStart);
|
||||
try {
|
||||
return JSON.parse(jsonText) as ExcelRow[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function getExcelIndex(): Map<string, ExcelMatch> {
|
||||
if (EXCEL_INDEX) return EXCEL_INDEX;
|
||||
const idx = new Map<string, ExcelMatch>();
|
||||
for (const file of EXCEL_SOURCE_FILES) {
|
||||
if (!fs.existsSync(file)) continue;
|
||||
const rows = loadExcelRows(file);
|
||||
|
||||
const unitsRow = rows.find(r => r && r['Part Number'] === 'Units') || null;
|
||||
const units: Record<string, string> = {};
|
||||
if (unitsRow) {
|
||||
for (const [k, v] of Object.entries(unitsRow)) {
|
||||
if (k === 'Part Number') continue;
|
||||
const unit = normalizeValue(String(v ?? ''));
|
||||
if (unit) units[k] = unit;
|
||||
}
|
||||
}
|
||||
|
||||
for (const r of rows) {
|
||||
const pn = r?.['Part Number'];
|
||||
if (!pn || pn === 'Units') continue;
|
||||
const key = normalizeExcelKey(String(pn));
|
||||
if (!key) continue;
|
||||
const cur = idx.get(key);
|
||||
if (!cur) {
|
||||
idx.set(key, { rows: [r], units });
|
||||
} else {
|
||||
cur.rows.push(r);
|
||||
if (Object.keys(cur.units).length < Object.keys(units).length) cur.units = units;
|
||||
}
|
||||
}
|
||||
}
|
||||
EXCEL_INDEX = idx;
|
||||
return idx;
|
||||
}
|
||||
|
||||
function findExcelForProduct(product: ProductData): ExcelMatch | null {
|
||||
const idx = getExcelIndex();
|
||||
const candidates = [
|
||||
product.name,
|
||||
product.slug ? product.slug.replace(/-\d+$/g, '') : '',
|
||||
product.sku,
|
||||
product.translationKey,
|
||||
].filter(Boolean) as string[];
|
||||
|
||||
for (const c of candidates) {
|
||||
const key = normalizeExcelKey(c);
|
||||
const match = idx.get(key);
|
||||
if (match && match.rows.length) return match;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function guessColumnKey(row: ExcelRow, patterns: RegExp[]): string | null {
|
||||
const keys = Object.keys(row || {});
|
||||
|
||||
for (const re of patterns) {
|
||||
const k = keys.find(x => {
|
||||
const key = String(x);
|
||||
if (re.test('conductor') && /ross section conductor/i.test(key)) return false;
|
||||
if (re.test('insulation thickness') && /Diameter over insulation/i.test(key)) return false;
|
||||
if (re.test('conductor') && !/^conductor$/i.test(key)) return false;
|
||||
if (re.test('insulation') && !/^insulation$/i.test(key)) return false;
|
||||
if (re.test('sheath') && !/^sheath$/i.test(key)) return false;
|
||||
if (re.test('norm') && !/^norm$/i.test(key)) return false;
|
||||
return re.test(key);
|
||||
});
|
||||
if (k) return k;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function buildExcelModel(args: {
|
||||
product: ProductData;
|
||||
locale: 'en' | 'de';
|
||||
}): { ok: boolean; technicalItems: KeyValueItem[]; voltageTables: VoltageTableModel[] } {
|
||||
const match = findExcelForProduct(args.product);
|
||||
if (!match || match.rows.length === 0) return { ok: false, technicalItems: [], voltageTables: [] };
|
||||
|
||||
const units = match.units || {};
|
||||
const rows = match.rows;
|
||||
|
||||
let sample = rows.find(r => r && Object.keys(r).length > 0) || {};
|
||||
let maxColumns = Object.keys(sample).filter(k => k && k !== 'Part Number' && k !== 'Units').length;
|
||||
|
||||
for (const r of rows) {
|
||||
const cols = Object.keys(r).filter(k => k && k !== 'Part Number' && k !== 'Units').length;
|
||||
if (cols > maxColumns) {
|
||||
sample = r;
|
||||
maxColumns = cols;
|
||||
}
|
||||
}
|
||||
|
||||
const columnMapping: Record<string, { header: string; unit: string; key: string }> = {
|
||||
'number of cores and cross-section': { header: 'Cross-section', unit: '', key: 'cross_section' },
|
||||
'ross section conductor': { header: 'Cross-section', unit: '', key: 'cross_section' },
|
||||
'diameter over insulation': { header: 'DI', unit: 'mm', key: 'DI' },
|
||||
'diameter over insulation (approx.)': { header: 'DI', unit: 'mm', key: 'DI' },
|
||||
'dc resistance at 20 °C': { header: 'RI', unit: 'Ohm/km', key: 'RI' },
|
||||
'dc resistance at 20°C': { header: 'RI', unit: 'Ohm/km', key: 'RI' },
|
||||
'resistance conductor': { header: 'RI', unit: 'Ohm/km', key: 'RI' },
|
||||
'maximum resistance of conductor': { header: 'RI', unit: 'Ohm/km', key: 'RI' },
|
||||
'insulation thickness': { header: 'Wi', unit: 'mm', key: 'Wi' },
|
||||
'nominal insulation thickness': { header: 'Wi', unit: 'mm', key: 'Wi' },
|
||||
'current ratings in air, trefoil': { header: 'Ibl', unit: 'A', key: 'Ibl' },
|
||||
'current ratings in air, trefoil*': { header: 'Ibl', unit: 'A', key: 'Ibl' },
|
||||
'current ratings in ground, trefoil': { header: 'Ibe', unit: 'A', key: 'Ibe' },
|
||||
'current ratings in ground, trefoil*': { header: 'Ibe', unit: 'A', key: 'Ibe' },
|
||||
'conductor shortcircuit current': { header: 'Ik', unit: 'kA', key: 'Ik_cond' },
|
||||
'screen shortcircuit current': { header: 'Ik', unit: 'kA', key: 'Ik_screen' },
|
||||
'sheath thickness': { header: 'Wm', unit: 'mm', key: 'Wm' },
|
||||
'minimum sheath thickness': { header: 'Wm', unit: 'mm', key: 'Wm' },
|
||||
'nominal sheath thickness': { header: 'Wm', unit: 'mm', key: 'Wm' },
|
||||
'bending radius': { header: 'Rbv', unit: 'mm', key: 'Rbv' },
|
||||
'bending radius (min.)': { header: 'Rbv', unit: 'mm', key: 'Rbv' },
|
||||
'outer diameter': { header: 'Ø', unit: 'mm', key: 'Ø' },
|
||||
'outer diameter (approx.)': { header: 'Ø', unit: 'mm', key: 'Ø' },
|
||||
'outer diameter of cable': { header: 'Ø', unit: 'mm', key: 'Ø' },
|
||||
'pulling force': { header: 'Fzv', unit: 'N', key: 'Fzv' },
|
||||
'max. pulling force': { header: 'Fzv', unit: 'N', key: 'Fzv' },
|
||||
'conductor aluminum': { header: 'Cond.', unit: '', key: 'Cond' },
|
||||
'conductor copper': { header: 'Cond.', unit: '', key: 'Cond' },
|
||||
'weight': { header: 'G', unit: 'kg/km', key: 'G' },
|
||||
'weight (approx.)': { header: 'G', unit: 'kg/km', key: 'G' },
|
||||
'cable weight': { header: 'G', unit: 'kg/km', key: 'G' },
|
||||
'conductor diameter (approx.)': { header: 'Conductor diameter', unit: 'mm', key: 'cond_diam' },
|
||||
'conductor diameter': { header: 'Conductor diameter', unit: 'mm', key: 'cond_diam' },
|
||||
'diameter conductor': { header: 'Conductor diameter', unit: 'mm', key: 'cond_diam' },
|
||||
'diameter over screen': { header: 'Diameter over screen', unit: 'mm', key: 'D_screen' },
|
||||
'metallic screen mm2': { header: 'Metallic screen', unit: 'mm2', key: 'S_screen' },
|
||||
'metallic screen': { header: 'Metallic screen', unit: 'mm2', key: 'S_screen' },
|
||||
'reactance': { header: 'Reactance', unit: 'Ohm/km', key: 'X' },
|
||||
'capacitance (approx.)': { header: 'Capacitance', unit: 'uF/km', key: 'cap' },
|
||||
'capacitance': { header: 'Capacitance', unit: 'uF/km', key: 'cap' },
|
||||
'inductance, trefoil (approx.)': { header: 'Inductance trefoil', unit: 'mH/km', key: 'ind_trefoil' },
|
||||
'inductance, trefoil': { header: 'Inductance trefoil', unit: 'mH/km', key: 'ind_trefoil' },
|
||||
'inductance in air, flat (approx.)': { header: 'Inductance air flat', unit: 'mH/km', key: 'ind_air_flat' },
|
||||
'inductance in air, flat': { header: 'Inductance air flat', unit: 'mH/km', key: 'ind_air_flat' },
|
||||
'inductance in ground, flat (approx.)': { header: 'Inductance ground flat', unit: 'mH/km', key: 'ind_ground_flat' },
|
||||
'inductance in ground, flat': { header: 'Inductance ground flat', unit: 'mH/km', key: 'ind_ground_flat' },
|
||||
'current ratings in air, flat': { header: 'Current air flat', unit: 'A', key: 'cur_air_flat' },
|
||||
'current ratings in air, flat*': { header: 'Current air flat', unit: 'A', key: 'cur_air_flat' },
|
||||
'current ratings in ground, flat': { header: 'Current ground flat', unit: 'A', key: 'cur_ground_flat' },
|
||||
'current ratings in ground, flat*': { header: 'Current ground flat', unit: 'A', key: 'cur_ground_flat' },
|
||||
'heating time constant, trefoil*': { header: 'Heating time trefoil', unit: 's', key: 'heat_trefoil' },
|
||||
'heating time constant, trefoil': { header: 'Heating time trefoil', unit: 's', key: 'heat_trefoil' },
|
||||
'heating time constant, flat*': { header: 'Heating time flat', unit: 's', key: 'heat_flat' },
|
||||
'heating time constant, flat': { header: 'Heating time flat', unit: 's', key: 'heat_flat' },
|
||||
'maximal operating conductor temperature': { header: 'Max operating temp', unit: '°C', key: 'max_op_temp' },
|
||||
'maximal short-circuit temperature': { header: 'Max short-circuit temp', unit: '°C', key: 'max_sc_temp' },
|
||||
'operating temperature range': { header: 'Operating temp range', unit: '°C', key: 'temp_range' },
|
||||
'minimal storage temperature': { header: 'Min storage temp', unit: '°C', key: 'min_store_temp' },
|
||||
'minimal temperature for laying': { header: 'Min laying temp', unit: '°C', key: 'min_lay_temp' },
|
||||
'test voltage': { header: 'Test voltage', unit: 'kV', key: 'test_volt' },
|
||||
'rated voltage': { header: 'Rated voltage', unit: 'kV', key: 'rated_volt' },
|
||||
'conductor': { header: 'Conductor', unit: '', key: 'conductor' },
|
||||
'copper wire screen and tape': { header: 'Copper screen', unit: '', key: 'copper_screen' },
|
||||
'CUScreen': { header: 'Copper screen', unit: '', key: 'copper_screen' },
|
||||
'conductive tape below screen': { header: 'Conductive tape below', unit: '', key: 'tape_below' },
|
||||
'non conducting tape above screen': { header: 'Non-conductive tape above', unit: '', key: 'tape_above' },
|
||||
'al foil': { header: 'Al foil', unit: '', key: 'al_foil' },
|
||||
'shape of conductor': { header: 'Conductor shape', unit: '', key: 'shape' },
|
||||
'colour of insulation': { header: 'Insulation color', unit: '', key: 'color_ins' },
|
||||
'colour of sheath': { header: 'Sheath color', unit: '', key: 'color_sheath' },
|
||||
'insulation': { header: 'Insulation', unit: '', key: 'insulation' },
|
||||
'sheath': { header: 'Sheath', unit: '', key: 'sheath' },
|
||||
'norm': { header: 'Norm', unit: '', key: 'norm' },
|
||||
'standard': { header: 'Standard', unit: '', key: 'standard' },
|
||||
'cpr class': { header: 'CPR class', unit: '', key: 'cpr' },
|
||||
'flame retardant': { header: 'Flame retardant', unit: '', key: 'flame' },
|
||||
'self-extinguishing of single cable': { header: 'Flame retardant', unit: '', key: 'flame' },
|
||||
'packaging': { header: 'Packaging', unit: '', key: 'packaging' },
|
||||
'ce-conformity': { header: 'CE conformity', unit: '', key: 'ce' },
|
||||
'rohs/reach': { header: 'RoHS/REACH', unit: '', key: 'rohs_reach' },
|
||||
};
|
||||
|
||||
const excelKeys = Object.keys(sample).filter(k => k && k !== 'Part Number' && k !== 'Units');
|
||||
|
||||
const matchedColumns: Array<{ excelKey: string; mapping: { header: string; unit: string; key: string } }> = [];
|
||||
for (const excelKey of excelKeys) {
|
||||
const normalized = normalizeValue(excelKey).toLowerCase();
|
||||
for (const [pattern, mapping] of Object.entries(columnMapping)) {
|
||||
if (normalized === pattern.toLowerCase() || new RegExp(pattern, 'i').test(normalized)) {
|
||||
matchedColumns.push({ excelKey, mapping });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const seenKeys = new Set<string>();
|
||||
const deduplicated: typeof matchedColumns = [];
|
||||
for (const item of matchedColumns) {
|
||||
if (!seenKeys.has(item.mapping.key)) {
|
||||
seenKeys.add(item.mapping.key);
|
||||
deduplicated.push(item);
|
||||
}
|
||||
}
|
||||
matchedColumns.length = 0;
|
||||
matchedColumns.push(...deduplicated);
|
||||
|
||||
const sampleKeys = Object.keys(sample).filter(k => k && k !== 'Part Number' && k !== 'Units').sort();
|
||||
const compatibleRows = rows.filter(r => {
|
||||
const rKeys = Object.keys(r).filter(k => k && k !== 'Part Number' && k !== 'Units').sort();
|
||||
return JSON.stringify(rKeys) === JSON.stringify(sampleKeys);
|
||||
});
|
||||
|
||||
if (compatibleRows.length === 0) return { ok: false, technicalItems: [], voltageTables: [] };
|
||||
|
||||
const csKey =
|
||||
guessColumnKey(sample, [/number of cores and cross-section/i, /cross.?section/i, /ross section conductor/i]) || null;
|
||||
const voltageKey =
|
||||
guessColumnKey(sample, [/rated voltage/i, /voltage rating/i, /nennspannung/i, /spannungs/i]) || null;
|
||||
|
||||
if (!csKey) return { ok: false, technicalItems: [], voltageTables: [] };
|
||||
|
||||
const byVoltage = new Map<string, number[]>();
|
||||
for (let i = 0; i < compatibleRows.length; i++) {
|
||||
const cs = normalizeValue(String(compatibleRows[i]?.[csKey] ?? ''));
|
||||
if (!cs) continue;
|
||||
const rawV = voltageKey ? normalizeValue(String(compatibleRows[i]?.[voltageKey] ?? '')) : '';
|
||||
const voltageLabel = normalizeVoltageLabel(rawV || '');
|
||||
const key = voltageLabel || (args.locale === 'de' ? 'Spannung unbekannt' : 'Voltage unknown');
|
||||
const arr = byVoltage.get(key) ?? [];
|
||||
arr.push(i);
|
||||
byVoltage.set(key, arr);
|
||||
}
|
||||
|
||||
const voltageTables: VoltageTableModel[] = [];
|
||||
const technicalItems: KeyValueItem[] = [];
|
||||
|
||||
const voltageKeysSorted = Array.from(byVoltage.keys()).sort((a, b) => {
|
||||
const na = parseVoltageSortKey(a);
|
||||
const nb = parseVoltageSortKey(b);
|
||||
if (na !== nb) return na - nb;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
const globalConstantColumns = new Set<string>();
|
||||
|
||||
for (const { excelKey, mapping } of matchedColumns) {
|
||||
const values = compatibleRows.map(r => normalizeValue(String(r?.[excelKey] ?? ''))).filter(Boolean);
|
||||
const unique = Array.from(new Set(values.map(v => v.toLowerCase())));
|
||||
|
||||
if (unique.length === 1 && values.length > 0) {
|
||||
globalConstantColumns.add(excelKey);
|
||||
|
||||
const unit = normalizeUnit(units[excelKey] || mapping.unit || '');
|
||||
const labelBase = technicalFullLabel({ key: mapping.key, excelKey, locale: args.locale });
|
||||
const label = formatExcelHeaderLabel(labelBase, unit);
|
||||
const value = compactCellForDenseTable(values[0], unit, args.locale);
|
||||
const existing = technicalItems.find(t => t.label === label);
|
||||
if (!existing) technicalItems.push({ label, value, unit });
|
||||
}
|
||||
}
|
||||
|
||||
const metaKeyPriority = [
|
||||
'test_volt',
|
||||
'temp_range',
|
||||
'max_op_temp',
|
||||
'max_sc_temp',
|
||||
'min_lay_temp',
|
||||
'min_store_temp',
|
||||
'cpr',
|
||||
'flame',
|
||||
];
|
||||
const metaKeyPrioritySet = new Set(metaKeyPriority);
|
||||
|
||||
for (const vKey of voltageKeysSorted) {
|
||||
const indices = byVoltage.get(vKey) || [];
|
||||
if (!indices.length) continue;
|
||||
|
||||
const crossSections = indices.map(idx => normalizeValue(String(compatibleRows[idx]?.[csKey] ?? '')));
|
||||
|
||||
const metaItems: KeyValueItem[] = [];
|
||||
const metaCandidates = new Map<string, KeyValueItem>();
|
||||
if (voltageKey) {
|
||||
const rawV = normalizeValue(String(compatibleRows[indices[0]]?.[voltageKey] ?? ''));
|
||||
metaItems.push({
|
||||
label: args.locale === 'de' ? 'Spannung' : 'Voltage',
|
||||
value: normalizeVoltageLabel(rawV || ''),
|
||||
});
|
||||
}
|
||||
|
||||
const tableColumns: Array<{ excelKey: string; mapping: { header: string; unit: string; key: string } }> = [];
|
||||
|
||||
const denseTableKeyOrder = [
|
||||
'Cond',
|
||||
'shape',
|
||||
'cap',
|
||||
'X',
|
||||
'DI',
|
||||
'RI',
|
||||
'Wi',
|
||||
'Ibl',
|
||||
'Ibe',
|
||||
'Ik_cond',
|
||||
'Wm',
|
||||
'Rbv',
|
||||
'Ø',
|
||||
'D_screen',
|
||||
'S_screen',
|
||||
'Fzv',
|
||||
'G',
|
||||
] as const;
|
||||
const denseTableKeys = new Set<string>(denseTableKeyOrder);
|
||||
|
||||
const bendingRadiusKey = matchedColumns.find(c => c.mapping.key === 'Rbv')?.excelKey || null;
|
||||
let bendUnitOverride = '';
|
||||
if (bendingRadiusKey) {
|
||||
const bendVals = indices
|
||||
.map(idx => normalizeValue(String(compatibleRows[idx]?.[bendingRadiusKey] ?? '')))
|
||||
.filter(Boolean);
|
||||
if (bendVals.some(v => /\bxD\b/i.test(v))) bendUnitOverride = 'xD';
|
||||
}
|
||||
|
||||
for (const { excelKey, mapping } of matchedColumns) {
|
||||
if (excelKey === csKey || excelKey === voltageKey) continue;
|
||||
|
||||
const values = indices.map(idx => normalizeValue(String(compatibleRows[idx]?.[excelKey] ?? ''))).filter(Boolean);
|
||||
|
||||
if (values.length > 0) {
|
||||
const unique = Array.from(new Set(values.map(v => v.toLowerCase())));
|
||||
let unit = normalizeUnit(units[excelKey] || mapping.unit || '');
|
||||
if (mapping.key === 'Rbv' && bendUnitOverride) unit = bendUnitOverride;
|
||||
|
||||
if (denseTableKeys.has(mapping.key)) {
|
||||
tableColumns.push({ excelKey, mapping });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (globalConstantColumns.has(excelKey) && !metaKeyPrioritySet.has(mapping.key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const value =
|
||||
unique.length === 1
|
||||
? compactCellForDenseTable(values[0], unit, args.locale)
|
||||
: summarizeSmartOptions(mapping.key, values);
|
||||
|
||||
const label = metaFullLabel({ key: mapping.key, excelKey, locale: args.locale });
|
||||
|
||||
metaCandidates.set(mapping.key, { label, value, unit });
|
||||
}
|
||||
}
|
||||
|
||||
for (const k of metaKeyPriority) {
|
||||
const item = metaCandidates.get(k);
|
||||
if (item && item.label && item.value) metaItems.push(item);
|
||||
}
|
||||
|
||||
const mappedByKey = new Map<string, { excelKey: string; mapping: { header: string; unit: string; key: string } }>();
|
||||
for (const c of tableColumns) {
|
||||
if (!mappedByKey.has(c.mapping.key)) mappedByKey.set(c.mapping.key, c);
|
||||
}
|
||||
|
||||
const outerDiameterKey = (mappedByKey.get('Ø')?.excelKey || '') || null;
|
||||
const sheathThicknessKey = (mappedByKey.get('Wm')?.excelKey || '') || null;
|
||||
|
||||
const canDeriveDenseKey = (k: (typeof denseTableKeyOrder)[number]): boolean => {
|
||||
if (k === 'DI') return Boolean(outerDiameterKey && sheathThicknessKey);
|
||||
if (k === 'Cond') return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
const orderedTableColumns = denseTableKeyOrder
|
||||
.filter(k => mappedByKey.has(k) || canDeriveDenseKey(k))
|
||||
.map(k => {
|
||||
const existing = mappedByKey.get(k);
|
||||
if (existing) return existing;
|
||||
return {
|
||||
excelKey: '',
|
||||
mapping: { header: k, unit: '', key: k },
|
||||
};
|
||||
});
|
||||
|
||||
const columns = orderedTableColumns.map(({ excelKey, mapping }) => {
|
||||
const defaultUnitByKey: Record<string, string> = {
|
||||
DI: 'mm',
|
||||
RI: 'Ohm/km',
|
||||
Wi: 'mm',
|
||||
Ibl: 'A',
|
||||
Ibe: 'A',
|
||||
Ik_cond: 'kA',
|
||||
Wm: 'mm',
|
||||
Rbv: 'mm',
|
||||
'Ø': 'mm',
|
||||
Fzv: 'N',
|
||||
G: 'kg/km',
|
||||
};
|
||||
|
||||
let unit = normalizeUnit((excelKey ? units[excelKey] : '') || mapping.unit || defaultUnitByKey[mapping.key] || '');
|
||||
if (mapping.key === 'Rbv' && bendUnitOverride) unit = bendUnitOverride;
|
||||
|
||||
return {
|
||||
key: mapping.key,
|
||||
label: denseAbbrevLabel({ key: mapping.key, locale: args.locale, unit, withUnit: true }) || formatExcelHeaderLabel(excelKey, unit),
|
||||
get: (rowIndex: number) => {
|
||||
const srcRowIndex = indices[rowIndex];
|
||||
const raw = excelKey ? normalizeValue(String(compatibleRows[srcRowIndex]?.[excelKey] ?? '')) : '';
|
||||
const unitLocal = unit;
|
||||
|
||||
if (mapping.key === 'DI' && !raw && outerDiameterKey && sheathThicknessKey) {
|
||||
const odRaw = normalizeValue(String(compatibleRows[srcRowIndex]?.[outerDiameterKey] ?? ''));
|
||||
const wmRaw = normalizeValue(String(compatibleRows[srcRowIndex]?.[sheathThicknessKey] ?? ''));
|
||||
const od = parseNumericOption(odRaw);
|
||||
const wm = parseNumericOption(wmRaw);
|
||||
if (od !== null && wm !== null) {
|
||||
const di = od - 2 * wm;
|
||||
if (Number.isFinite(di) && di > 0) return `~${compactNumericForLocale(String(di), args.locale)}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (mapping.key === 'Cond' && !raw) {
|
||||
const pn = normalizeExcelKey(args.product.name || args.product.slug || args.product.sku || '');
|
||||
if (/^NA/.test(pn)) return 'Al';
|
||||
if (/^N/.test(pn)) return 'Cu';
|
||||
}
|
||||
|
||||
if (mapping.key === 'Rbv' && /\bxD\b/i.test(raw)) return compactNumericForLocale(raw, args.locale);
|
||||
|
||||
if (mapping.key === 'Rbv' && unitLocal.toLowerCase() === 'mm') {
|
||||
const n = parseNumericOption(raw);
|
||||
const looksLikeMeters = n !== null && n > 0 && n < 50 && /[\.,]\d{1,3}/.test(raw) && !/\dxD/i.test(raw);
|
||||
if (looksLikeMeters) return compactNumericForLocale(String(Math.round(n * 1000)), args.locale);
|
||||
}
|
||||
|
||||
if (mapping.key === 'Fzv' && unitLocal.toLowerCase() === 'n') {
|
||||
const n = parseNumericOption(raw);
|
||||
const looksLikeKN = n !== null && n > 0 && n < 100 && !/\bN\b/i.test(raw) && !/\bkN\b/i.test(raw);
|
||||
if (looksLikeKN) return compactNumericForLocale(String(Math.round(n * 1000)), args.locale);
|
||||
}
|
||||
|
||||
return compactCellForDenseTable(raw, unitLocal, args.locale);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
voltageTables.push({ voltageLabel: vKey, metaItems, crossSections, columns });
|
||||
}
|
||||
|
||||
technicalItems.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
return { ok: true, technicalItems, voltageTables };
|
||||
}
|
||||
18
scripts/list-pages.ts
Normal file
18
scripts/list-pages.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { getPayload } from 'payload';
|
||||
import configPromise from '../payload.config';
|
||||
|
||||
async function listPages() {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const result = await payload.find({
|
||||
collection: 'pages',
|
||||
locale: 'de'
|
||||
});
|
||||
|
||||
console.log('Pages detected:');
|
||||
result.docs.forEach(d => {
|
||||
console.log(`- ${d.title} (slug: ${d.slug})`);
|
||||
});
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
listPages();
|
||||
Reference in New Issue
Block a user