This commit is contained in:
2026-01-07 11:34:56 +01:00
parent ce26783d45
commit 5122deaf90
54 changed files with 639 additions and 13 deletions

BIN
public/datasheets.zip Normal file

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.

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.

View File

@@ -166,8 +166,22 @@ function findExcelRowsForProduct(product: ProductData): ExcelRow[] {
function guessColumnKey(row: ExcelRow, patterns: RegExp[]): string | null {
const keys = Object.keys(row || {});
// Try pattern-based matching first
for (const re of patterns) {
const k = keys.find(x => re.test(String(x)));
const k = keys.find(x => {
const key = String(x);
// Specific exclusions to prevent wrong matches
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;
@@ -240,15 +254,14 @@ function ensureExcelCrossSectionAttributes(product: ProductData, locale: 'en' |
return;
}
// Get all technical column keys using improved detection
const voltageKey = guessColumnKey(rows[0], [/rated voltage/i, /voltage rating/i, /spannungs/i, /nennspannung/i]);
const outerKey = guessColumnKey(rows[0], [/outer diameter\b/i, /outer diameter.*approx/i, /outer diameter of cable/i, /außen/i]);
const weightKey = guessColumnKey(rows[0], [/weight\b/i, /gewicht/i, /cable weight/i]);
const dcResKey = guessColumnKey(rows[0], [/dc resistance/i, /resistance conductor/i, /leiterwiderstand/i]);
// Additional technical columns that are often missing from WP exports.
// We add them as either constant attributes (if identical across all rows)
// or as small multi-value arrays (if they vary), so TECHNICAL DATA can render them.
const ratedVoltKey = guessColumnKey(rows[0], [/rated voltage/i, /voltage rating/i, /spannungs/i, /nennspannung/i]);
// Additional technical columns
const ratedVoltKey = voltageKey; // Already found above
const testVoltKey = guessColumnKey(rows[0], [/test voltage/i, /prüfspannung/i]);
const tempRangeKey = guessColumnKey(rows[0], [/operating temperature range/i, /temperature range/i, /temperaturbereich/i]);
const minLayKey = guessColumnKey(rows[0], [/minimal temperature for laying/i]);
@@ -258,7 +271,35 @@ function ensureExcelCrossSectionAttributes(product: ProductData, locale: 'en' |
const insThkKey = guessColumnKey(rows[0], [/nominal insulation thickness/i, /insulation thickness/i]);
const sheathThkKey = guessColumnKey(rows[0], [/nominal sheath thickness/i, /minimum sheath thickness/i]);
const maxResKey = guessColumnKey(rows[0], [/maximum resistance of conductor/i]);
// Material and specification columns
const conductorKey = guessColumnKey(rows[0], [/^conductor$/i]);
const insulationKey = guessColumnKey(rows[0], [/^insulation$/i]);
const sheathKey = guessColumnKey(rows[0], [/^sheath$/i]);
const normKey = guessColumnKey(rows[0], [/^norm$/i, /^standard$/i]);
const cprKey = guessColumnKey(rows[0], [/cpr class/i]);
const rohsKey = guessColumnKey(rows[0], [/^rohs$/i]);
const reachKey = guessColumnKey(rows[0], [/^reach$/i]);
const packagingKey = guessColumnKey(rows[0], [/^packaging$/i]);
const shapeKey = guessColumnKey(rows[0], [/shape of conductor/i]);
const flameKey = guessColumnKey(rows[0], [/flame retardant/i]);
const diamCondKey = guessColumnKey(rows[0], [/diameter conductor/i]);
const diamInsKey = guessColumnKey(rows[0], [/diameter over insulation/i]);
const diamScreenKey = guessColumnKey(rows[0], [/diameter over screen/i]);
const metalScreenKey = guessColumnKey(rows[0], [/metallic screen/i]);
const capacitanceKey = guessColumnKey(rows[0], [/capacitance/i]);
const reactanceKey = guessColumnKey(rows[0], [/reactance/i]);
const electricalStressKey = guessColumnKey(rows[0], [/electrical stress/i]);
const pullingForceKey = guessColumnKey(rows[0], [/max\. pulling force/i, /pulling force/i]);
const heatingTrefoilKey = guessColumnKey(rows[0], [/heating time constant.*trefoil/i]);
const heatingFlatKey = guessColumnKey(rows[0], [/heating time constant.*flat/i]);
const currentAirTrefoilKey = guessColumnKey(rows[0], [/current ratings in air.*trefoil/i]);
const currentAirFlatKey = guessColumnKey(rows[0], [/current ratings in air.*flat/i]);
const currentGroundTrefoilKey = guessColumnKey(rows[0], [/current ratings in ground.*trefoil/i]);
const currentGroundFlatKey = guessColumnKey(rows[0], [/current ratings in ground.*flat/i]);
const scCurrentCondKey = guessColumnKey(rows[0], [/conductor shortcircuit current/i]);
const scCurrentScreenKey = guessColumnKey(rows[0], [/screen shortcircuit current/i]);
const cfgName = locale === 'de' ? 'Anzahl der Adern und Querschnitt' : 'Number of cores and cross-section';
const cfgOptions = rows
.map(r => {
@@ -359,6 +400,139 @@ function ensureExcelCrossSectionAttributes(product: ProductData, locale: 'en' |
key: maxResKey,
});
// Add additional technical data from Excel files
addConstOrSmallList({
name: locale === 'de' ? 'Leiter' : 'Conductor',
existsRe: /conductor/i,
key: conductorKey,
});
addConstOrSmallList({
name: locale === 'de' ? 'Isolierung' : 'Insulation',
existsRe: /insulation/i,
key: insulationKey,
});
addConstOrSmallList({
name: locale === 'de' ? 'Mantel' : 'Sheath',
existsRe: /sheath/i,
key: sheathKey,
});
addConstOrSmallList({
name: locale === 'de' ? 'Norm' : 'Standard',
existsRe: /norm|standard|iec|vde/i,
key: normKey,
});
addConstOrSmallList({
name: locale === 'de' ? 'Leiterdurchmesser' : 'Conductor diameter',
existsRe: /diameter conductor|conductor diameter/i,
key: diamCondKey,
});
addConstOrSmallList({
name: locale === 'de' ? 'Isolierungsdurchmesser' : 'Insulation diameter',
existsRe: /diameter over insulation|diameter insulation/i,
key: diamInsKey,
});
addConstOrSmallList({
name: locale === 'de' ? 'Schirmdurchmesser' : 'Screen diameter',
existsRe: /diameter over screen|diameter screen/i,
key: diamScreenKey,
});
addConstOrSmallList({
name: locale === 'de' ? 'Metallischer Schirm' : 'Metallic screen',
existsRe: /metallic screen/i,
key: metalScreenKey,
});
addConstOrSmallList({
name: locale === 'de' ? 'Max. Zugkraft' : 'Max. pulling force',
existsRe: /max.*pulling force|pulling force/i,
key: pullingForceKey,
});
addConstOrSmallList({
name: locale === 'de' ? 'Elektrische Spannung Leiter' : 'Electrical stress conductor',
existsRe: /electrical stress conductor/i,
key: electricalStressKey,
});
addConstOrSmallList({
name: locale === 'de' ? 'Elektrische Spannung Isolierung' : 'Electrical stress insulation',
existsRe: /electrical stress insulation/i,
key: electricalStressKey,
});
addConstOrSmallList({
name: locale === 'de' ? 'Reaktanz' : 'Reactance',
existsRe: /reactance/i,
key: reactanceKey,
});
addConstOrSmallList({
name: locale === 'de' ? 'Heizzeitkonstante trefoil' : 'Heating time constant trefoil',
existsRe: /heating time constant.*trefoil/i,
key: heatingTrefoilKey,
});
addConstOrSmallList({
name: locale === 'de' ? 'Heizzeitkonstante flach' : 'Heating time constant flat',
existsRe: /heating time constant.*flat/i,
key: heatingFlatKey,
});
addConstOrSmallList({
name: locale === 'de' ? 'Flammhemmend' : 'Flame retardant',
existsRe: /flame retardant/i,
key: flameKey,
});
addConstOrSmallList({
name: locale === 'de' ? 'CPR-Klasse' : 'CPR class',
existsRe: /cpr class/i,
key: cprKey,
});
addConstOrSmallList({
name: locale === 'de' ? 'Verpackung' : 'Packaging',
existsRe: /packaging/i,
key: packagingKey,
});
addConstOrSmallList({
name: locale === 'de' ? 'Biegeradius' : 'Bending radius',
existsRe: /bending radius/i,
key: null, // Will be found in row-specific attributes
});
addConstOrSmallList({
name: locale === 'de' ? 'Leiterform' : 'Shape of conductor',
existsRe: /shape of conductor/i,
key: shapeKey,
});
addConstOrSmallList({
name: locale === 'de' ? 'Isolierungsfarbe' : 'Colour of insulation',
existsRe: /colour of insulation/i,
key: null, // Will be found in row-specific attributes
});
addConstOrSmallList({
name: locale === 'de' ? 'Mantelfarbe' : 'Colour of sheath',
existsRe: /colour of sheath/i,
key: null, // Will be found in row-specific attributes
});
addConstOrSmallList({
name: locale === 'de' ? 'RoHS/REACH' : 'RoHS/REACH',
existsRe: /rohs.*reach/i,
key: null, // Will be found in row-specific attributes
});
product.attributes = attrs;
if (process.env.PDF_DEBUG_EXCEL === '1') {
@@ -395,6 +569,21 @@ function ensureExcelRowSpecificAttributes(product: ProductData, locale: 'en' | '
const keyScCond = guessColumnKey(sample, [/conductor shortcircuit current/i]);
const keyScScreen = guessColumnKey(sample, [/screen shortcircuit current/i]);
const keyBend = guessColumnKey(sample, [/bending radius/i, /min\. bending radius/i]);
// Additional row-specific technical data
const keyConductorDiameter = guessColumnKey(sample, [/conductor diameter/i, /diameter conductor/i]);
const keyInsulationThickness = guessColumnKey(sample, [/nominal insulation thickness/i, /insulation thickness/i]);
const keySheathThickness = guessColumnKey(sample, [/nominal sheath thickness/i, /minimum sheath thickness/i, /sheath thickness/i]);
const keyCapacitance = guessColumnKey(sample, [/capacitance/i]);
const keyInductanceTrefoil = guessColumnKey(sample, [/inductance.*trefoil/i]);
const keyInductanceAirFlat = guessColumnKey(sample, [/inductance.*air.*flat/i]);
const keyInductanceGroundFlat = guessColumnKey(sample, [/inductance.*ground.*flat/i]);
const keyCurrentAirTrefoil = guessColumnKey(sample, [/current.*air.*trefoil/i]);
const keyCurrentAirFlat = guessColumnKey(sample, [/current.*air.*flat/i]);
const keyCurrentGroundTrefoil = guessColumnKey(sample, [/current.*ground.*trefoil/i]);
const keyCurrentGroundFlat = guessColumnKey(sample, [/current.*ground.*flat/i]);
const keyHeatingTimeTrefoil = guessColumnKey(sample, [/heating.*time.*trefoil/i]);
const keyHeatingTimeFlat = guessColumnKey(sample, [/heating.*time.*flat/i]);
const get = (k: string | null) => rows.map(r => normalizeValue(String(r?.[k ?? ''] ?? '')));
const withUnit = (vals: string[], unit: string) => vals.map(v => (v && looksNumeric(v) ? `${v} ${unit}` : v));
@@ -511,6 +700,111 @@ function ensureExcelRowSpecificAttributes(product: ProductData, locale: 'en' | '
expectedLen: rowCount,
existsRe: /bending\s*radius|biegeradius/i,
});
// Additional row-specific technical data
pushRowAttrIfMissing({
product,
name: locale === 'de' ? 'Leiterdurchmesser' : 'Conductor diameter',
options: withUnit(get(keyConductorDiameter), 'mm'),
expectedLen: rowCount,
existsRe: /conductor diameter|diameter conductor/i,
});
pushRowAttrIfMissing({
product,
name: locale === 'de' ? 'Isolationsdicke' : 'Insulation thickness',
options: withUnit(get(keyInsulationThickness), 'mm'),
expectedLen: rowCount,
existsRe: /insulation thickness|nominal insulation thickness/i,
});
pushRowAttrIfMissing({
product,
name: locale === 'de' ? 'Manteldicke' : 'Sheath thickness',
options: withUnit(get(keySheathThickness), 'mm'),
expectedLen: rowCount,
existsRe: /sheath thickness|nominal sheath thickness/i,
});
pushRowAttrIfMissing({
product,
name: locale === 'de' ? 'Kapazität' : 'Capacitance',
options: withUnit(get(keyCapacitance), 'μF/km'),
expectedLen: rowCount,
existsRe: /capacitance/i,
});
pushRowAttrIfMissing({
product,
name: locale === 'de' ? 'Induktivität trefoil' : 'Inductance trefoil',
options: withUnit(get(keyInductanceTrefoil), 'mH/km'),
expectedLen: rowCount,
existsRe: /inductance.*trefoil/i,
});
pushRowAttrIfMissing({
product,
name: locale === 'de' ? 'Induktivität Luft flach' : 'Inductance air flat',
options: withUnit(get(keyInductanceAirFlat), 'mH/km'),
expectedLen: rowCount,
existsRe: /inductance.*air.*flat/i,
});
pushRowAttrIfMissing({
product,
name: locale === 'de' ? 'Induktivität Erdreich flach' : 'Inductance ground flat',
options: withUnit(get(keyInductanceGroundFlat), 'mH/km'),
expectedLen: rowCount,
existsRe: /inductance.*ground.*flat/i,
});
pushRowAttrIfMissing({
product,
name: locale === 'de' ? 'Strombelastbarkeit Luft trefoil' : 'Current rating air trefoil',
options: withUnit(get(keyCurrentAirTrefoil), 'A'),
expectedLen: rowCount,
existsRe: /current.*air.*trefoil/i,
});
pushRowAttrIfMissing({
product,
name: locale === 'de' ? 'Strombelastbarkeit Luft flach' : 'Current rating air flat',
options: withUnit(get(keyCurrentAirFlat), 'A'),
expectedLen: rowCount,
existsRe: /current.*air.*flat/i,
});
pushRowAttrIfMissing({
product,
name: locale === 'de' ? 'Strombelastbarkeit Erdreich trefoil' : 'Current rating ground trefoil',
options: withUnit(get(keyCurrentGroundTrefoil), 'A'),
expectedLen: rowCount,
existsRe: /current.*ground.*trefoil/i,
});
pushRowAttrIfMissing({
product,
name: locale === 'de' ? 'Strombelastbarkeit Erdreich flach' : 'Current rating ground flat',
options: withUnit(get(keyCurrentGroundFlat), 'A'),
expectedLen: rowCount,
existsRe: /current.*ground.*flat/i,
});
pushRowAttrIfMissing({
product,
name: locale === 'de' ? 'Heizzeitkonstante trefoil' : 'Heating time constant trefoil',
options: withUnit(get(keyHeatingTimeTrefoil), 's'),
expectedLen: rowCount,
existsRe: /heating.*time.*trefoil/i,
});
pushRowAttrIfMissing({
product,
name: locale === 'de' ? 'Heizzeitkonstante flach' : 'Heating time constant flat',
options: withUnit(get(keyHeatingTimeFlat), 's'),
expectedLen: rowCount,
existsRe: /heating.*time.*flat/i,
});
}
function getProductUrl(product: ProductData): string | null {
@@ -1972,6 +2266,7 @@ async function generatePDF(product: ProductData, locale: 'en' | 'de'): Promise<B
// - show constant (non-row) attributes as key/value grid
// - show only a small configuration sample + total count
// - optionally render full tables with PDF_MODE=full
const pdfMode = process.env.PDF_MODE || 'compact'; // 'compact' or 'full'
// Prefer a curated list that matches website expectations.
// IMPORTANT: for row-specific arrays we don't attempt per-row mapping here; we summarize as ranges.
@@ -1982,6 +2277,12 @@ async function generatePDF(product: ProductData, locale: 'en' | 'de'): Promise<B
{ re: /temperature\s*range|operating\s*temperature|betriebstemperatur/i, fallbackLabel: locale === 'de' ? 'Temperaturbereich' : 'Temperature range' },
{ re: /bending\s*radius|biegeradius/i, fallbackLabel: locale === 'de' ? 'Biegeradius' : 'Bending radius' },
{ re: /cpr\s*class/i, fallbackLabel: locale === 'de' ? 'CPR-Klasse' : 'CPR class' },
{ re: /conductor/i, fallbackLabel: locale === 'de' ? 'Leiter' : 'Conductor' },
{ re: /insulation/i, fallbackLabel: locale === 'de' ? 'Isolierung' : 'Insulation' },
{ re: /sheath/i, fallbackLabel: locale === 'de' ? 'Mantel' : 'Sheath' },
{ re: /flame retardant|flammhemmend/i, fallbackLabel: locale === 'de' ? 'Flammhemmend' : 'Flame retardant' },
{ re: /packaging|verpackung/i, fallbackLabel: locale === 'de' ? 'Verpackung' : 'Packaging' },
{ re: /rohs.*reach/i, fallbackLabel: locale === 'de' ? 'RoHS/REACH' : 'RoHS/REACH' },
];
const picked = new Set<string>();
@@ -2145,9 +2446,13 @@ async function generatePDF(product: ProductData, locale: 'en' | 'de'): Promise<B
return a;
};
// Pull the two most important row-specific columns and show a small excerpt table.
// Pull the most important row-specific columns and show a small excerpt table.
const rowOuter = findRowAttr(/outer\s*diameter|außen\s*durchmesser|außen-?ø/i);
const rowWeight = findRowAttr(/\bweight\b|gewicht/i);
const rowDcRes = findRowAttr(/dc resistance|leiterwiderstand/i);
const rowCap = findRowAttr(/capacitance|kapazit/i);
const rowCurrentAir = findRowAttr(/current.*air/i);
const rowCurrentGround = findRowAttr(/current.*ground/i);
const yAfterCross = drawCrossSectionChipsRow({
title: labels.crossSection,
@@ -2190,11 +2495,25 @@ async function generatePDF(product: ProductData, locale: 'en' | 'de'): Promise<B
}
// Compact per-configuration excerpt (only if it fits).
if (rowOuter && rowWeight) {
// Build columns dynamically based on what's available
const availableColumns: Array<{ key: string; label: string; attr: ProductData['attributes'][number] | null; unit: string }> = [];
if (rowOuter) availableColumns.push({ key: 'outer', label: locale === 'de' ? 'Außen-Ø' : 'Outer Ø', attr: rowOuter, unit: 'mm' });
if (rowWeight) availableColumns.push({ key: 'weight', label: locale === 'de' ? 'Gewicht' : 'Weight', attr: rowWeight, unit: 'kg/km' });
if (rowDcRes) availableColumns.push({ key: 'dcres', label: locale === 'de' ? 'Widerstand' : 'Resistance', attr: rowDcRes, unit: 'Ω/km' });
if (rowCap) availableColumns.push({ key: 'cap', label: locale === 'de' ? 'Kapazität' : 'Capacitance', attr: rowCap, unit: 'μF/km' });
if (rowCurrentAir) availableColumns.push({ key: 'curair', label: locale === 'de' ? 'Strom Luft' : 'Current Air', attr: rowCurrentAir, unit: 'A' });
if (rowCurrentGround) availableColumns.push({ key: 'curground', label: locale === 'de' ? 'Strom Erdreich' : 'Current Ground', attr: rowCurrentGround, unit: 'A' });
if (availableColumns.length >= 2) {
// Use first two available columns for the preview table
const col1 = availableColumns[0];
const col2 = availableColumns[1];
const previewRows = configRows.map((cfg, i) => ({
config: normalizeValue(cfg),
col1: formatMaybeWithUnit(getAttrCellValue(rowOuter ?? undefined, i, rowCount), 'mm'),
col2: formatMaybeWithUnit(getAttrCellValue(rowWeight ?? undefined, i, rowCount), 'kg/km'),
col1: formatMaybeWithUnit(getAttrCellValue(col1.attr ?? undefined, i, rowCount), col1.unit),
col2: formatMaybeWithUnit(getAttrCellValue(col2.attr ?? undefined, i, rowCount), col2.unit),
}));
const previewTitle = locale === 'de' ? 'Konfigurationswerte (Auszug)' : 'Configuration values (excerpt)';
@@ -2203,8 +2522,8 @@ async function generatePDF(product: ProductData, locale: 'en' | 'de'): Promise<B
rows: previewRows,
headers: {
config: locale === 'de' ? 'Konfiguration' : 'Configuration',
col1: locale === 'de' ? 'Außen-Ø' : 'Outer Ø',
col2: locale === 'de' ? 'Gewicht' : 'Weight',
col1: col1.label,
col2: col2.label,
},
getPage: () => page,
page,
@@ -2221,6 +2540,39 @@ async function generatePDF(product: ProductData, locale: 'en' | 'de'): Promise<B
});
if (yAfterPreview >= contentMinY) y = yAfterPreview;
}
// Full table mode: show more technical columns if space allows and mode is enabled
if (pdfMode === 'full' && availableColumns.length > 2) {
// Try to show additional columns in a chunked table
const additionalColumns = availableColumns.slice(2).map(col => ({
key: col.key,
label: col.label,
get: (i: number) => formatMaybeWithUnit(getAttrCellValue(col.attr ?? undefined, i, rowCount), col.unit),
}));
if (additionalColumns.length > 0 && y - 100 >= contentMinY) {
y = drawTableChunked({
title: locale === 'de' ? 'Technische Daten (alle)' : 'Technical data (all)',
configRows,
columns: additionalColumns,
locale,
newPage,
getPage: () => page,
page,
y,
margin,
contentWidth,
contentMinY,
font,
fontBold,
navy,
darkGray,
lightGray,
almostWhite,
maxDataColsPerTable: 3,
});
}
}
} else {
// If there is no cross-section data, do not render the section at all.
}

173
test-enrichment.js Normal file
View File

@@ -0,0 +1,173 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Copy the key functions from the PDF script
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'),
];
function normalizeExcelKey(value) {
return String(value || '')
.toUpperCase()
.replace(/-\d+$/g, '')
.replace(/[^A-Z0-9]+/g, '');
}
function loadExcelRows(filePath) {
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);
} catch {
return [];
}
}
function getExcelIndex() {
if (getExcelIndex.cached) return getExcelIndex.cached;
const idx = new Map();
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 = {};
if (unitsRow) {
for (const [k, v] of Object.entries(unitsRow)) {
if (k === 'Part Number') continue;
const unit = String(v ?? '').trim();
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;
}
}
}
getExcelIndex.cached = idx;
return idx;
}
function findExcelForProduct(product) {
const idx = getExcelIndex();
const candidates = [
product.name,
product.slug ? product.slug.replace(/-\d+$/g, '') : '',
product.sku,
product.translationKey,
].filter(Boolean);
for (const c of candidates) {
const key = normalizeExcelKey(c);
const match = idx.get(key);
if (match && match.rows.length) return match;
}
return null;
}
function findExcelRowsForProduct(product) {
const match = findExcelForProduct(product);
return match?.rows || [];
}
function guessColumnKey(row, patterns) {
const keys = Object.keys(row || {});
for (const re of patterns) {
const k = keys.find(x => re.test(String(x)));
if (k) return k;
}
return null;
}
function normalizeValue(value) {
return String(value || '')
.replace(/<[^>]*>/g, '')
.replace(/\s+/g, ' ')
.trim();
}
function getUniqueNonEmpty(options) {
const uniq = [];
const seen = new Set();
for (const v of options.map(normalizeValue).filter(Boolean)) {
const k = v.toLowerCase();
if (seen.has(k)) continue;
seen.add(k);
uniq.push(v);
}
return uniq;
}
function looksNumeric(value) {
const v = normalizeValue(value).replace(/,/g, '.');
return /^-?\d+(?:\.\d+)?$/.test(v);
}
// Test the enrichment for a specific product
const products = JSON.parse(fs.readFileSync('data/processed/products.json', 'utf8'));
const testProduct = products.find(p => p.slug === 'na2xsfl2y-3');
if (testProduct) {
console.log('=== Original Product ===');
console.log('ID:', testProduct.id);
console.log('Slug:', testProduct.slug);
console.log('Name:', testProduct.name);
console.log('Attributes:', testProduct.attributes?.length || 0);
const rows = findExcelRowsForProduct(testProduct);
console.log('\n=== Excel Rows Found ===');
console.log('Rows:', rows.length);
if (rows.length > 0) {
console.log('\nFirst row columns:', Object.keys(rows[0]));
console.log('\nFirst row sample:', JSON.stringify(rows[0], null, 2).substring(0, 500));
// Test cross-section detection
const csKey = guessColumnKey(rows[0], [
/number of cores and cross-section/i,
/cross.?section/i,
/ross section conductor/i,
]);
console.log('\nCross-section key:', csKey);
if (csKey) {
const cfgOptions = rows
.map(r => normalizeValue(String(r?.[csKey] ?? '')))
.filter(Boolean);
console.log('Configurations found:', cfgOptions.length);
console.log('Sample configs:', cfgOptions.slice(0, 5));
}
// Test additional columns
const conductorKey = guessColumnKey(rows[0], [/conductor/i]);
const insulationKey = guessColumnKey(rows[0], [/insulation/i]);
const sheathKey = guessColumnKey(rows[0], [/sheath/i]);
const normKey = guessColumnKey(rows[0], [/norm|standard|iec|vde/i]);
console.log('\nAdditional column keys:');
console.log(' Conductor:', conductorKey);
console.log(' Insulation:', insulationKey);
console.log(' Sheath:', sheathKey);
console.log(' Norm:', normKey);
if (conductorKey) {
const values = getUniqueNonEmpty(rows.map(r => normalizeValue(String(r?.[conductorKey] ?? ''))));
console.log('\nConductor values:', values);
}
}
}

101
test-excel.js Normal file
View File

@@ -0,0 +1,101 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('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'),
];
function normalizeExcelKey(value) {
return String(value || '')
.toUpperCase()
.replace(/-\d+$/g, '')
.replace(/[^A-Z0-9]+/g, '');
}
function loadExcelRows(filePath) {
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);
} catch {
return [];
}
}
function getExcelIndex() {
const idx = new Map();
for (const file of EXCEL_SOURCE_FILES) {
if (!fs.existsSync(file)) continue;
const rows = loadExcelRows(file);
console.log(`[excel] loaded ${rows.length} rows from ${path.relative(process.cwd(), file)}`);
const unitsRow = rows.find(r => r && r['Part Number'] === 'Units') || null;
const units = {};
if (unitsRow) {
for (const [k, v] of Object.entries(unitsRow)) {
if (k === 'Part Number') continue;
const unit = String(v ?? '').trim();
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;
}
}
}
return idx;
}
const idx = getExcelIndex();
console.log('\n=== Excel Index Keys ===');
for (const [key, match] of idx.entries()) {
console.log(`Key: ${key} | Rows: ${match.rows.length}`);
if (match.rows.length > 0) {
console.log(' Sample columns:', Object.keys(match.rows[0]).slice(0, 10).join(', '));
}
}
// Test specific products
const products = JSON.parse(fs.readFileSync('data/processed/products.json', 'utf8'));
const testProducts = ['na2xsfl2y-3', 'h1z2z2-k', 'na2xsfl2y', 'h1z2z2k'];
console.log('\n=== Product Lookup Tests ===');
testProducts.forEach(slug => {
const product = products.find(p => p.slug === slug);
if (product) {
const candidates = [
product.name,
product.slug ? product.slug.replace(/-\d+$/g, '') : '',
product.sku,
product.translationKey,
].filter(Boolean);
const keys = candidates.map(c => normalizeExcelKey(c));
const matches = keys.map(k => idx.get(k)).filter(Boolean);
console.log(`\nProduct: ${slug} (${product.name})`);
console.log(` Candidates: ${candidates.join(', ')}`);
console.log(` Keys: ${keys.join(', ')}`);
console.log(` Excel matches: ${matches.length}`);
if (matches.length > 0) {
console.log(` Rows found: ${matches[0].rows.length}`);
}
}
});