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

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.
}