1123 lines
43 KiB
TypeScript
1123 lines
43 KiB
TypeScript
import * as fs from 'fs';
|
||
import * as path from 'path';
|
||
|
||
import type { DatasheetModel, DatasheetVoltageTable, KeyValueItem, ProductData } from './types';
|
||
import type { ExcelMatch, MediumVoltageCrossSectionExcelMatch } from './excel-index';
|
||
import { findExcelForProduct, findMediumVoltageCrossSectionExcelForProduct } from './excel-index';
|
||
import { getLabels, getProductUrl, normalizeValue, stripHtml } from './utils';
|
||
|
||
type ExcelRow = Record<string, unknown>;
|
||
type VoltageTableModel = {
|
||
voltageLabel: string;
|
||
metaItems: KeyValueItem[];
|
||
crossSections: string[];
|
||
columns: Array<{ key: string; label: string; get: (rowIndex: number) => string }>;
|
||
};
|
||
type BuildExcelModelResult = { ok: boolean; technicalItems: KeyValueItem[]; voltageTables: VoltageTableModel[] };
|
||
type AssetMap = Record<string, string>;
|
||
|
||
const ASSET_MAP_FILE = path.join(process.cwd(), 'data/processed/asset-map.json');
|
||
|
||
function readAssetMap(): AssetMap {
|
||
try {
|
||
if (!fs.existsSync(ASSET_MAP_FILE)) return {};
|
||
return JSON.parse(fs.readFileSync(ASSET_MAP_FILE, 'utf8')) as AssetMap;
|
||
} catch {
|
||
return {};
|
||
}
|
||
}
|
||
const ASSET_MAP: AssetMap = readAssetMap();
|
||
|
||
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 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 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 compactNumericForLocale(value: string, locale: 'en' | 'de'): string {
|
||
const v = normalizeValue(value);
|
||
if (!v) return '';
|
||
|
||
// Compact common bending-radius style: "15xD (Single core); 12xD (Multi core)" -> "15/12xD".
|
||
// Keep semantics, reduce width. Never truncate with ellipses.
|
||
if (/\d+xD/i.test(v)) {
|
||
const nums = Array.from(v.matchAll(/(\d+)xD/gi)).map(m => m[1]).filter(Boolean);
|
||
const unique: string[] = [];
|
||
for (const n of nums) {
|
||
if (!unique.includes(n)) unique.push(n);
|
||
}
|
||
if (unique.length) 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 resolveMediaToLocalPath(urlOrPath: string | null | undefined): string | null {
|
||
if (!urlOrPath) return null;
|
||
if (urlOrPath.startsWith('/')) return urlOrPath;
|
||
if (/^media\//i.test(urlOrPath)) return `/${urlOrPath}`;
|
||
const mapped = ASSET_MAP[urlOrPath];
|
||
if (mapped) {
|
||
if (mapped.startsWith('/')) return mapped;
|
||
if (/^public\//i.test(mapped)) return `/${mapped.replace(/^public\//i, '')}`;
|
||
if (/^media\//i.test(mapped)) return `/${mapped}`;
|
||
return mapped;
|
||
}
|
||
return urlOrPath;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
function technicalFullLabel(args: { key: string; excelKey: string; locale: 'en' | 'de' }): string {
|
||
const key = args.key;
|
||
if (args.locale === 'de') {
|
||
switch (key) {
|
||
case 'cond_mat': return 'Leitermaterial';
|
||
case 'cond_class': return 'Leiterklasse';
|
||
case 'core_ins': return 'Aderisolation';
|
||
case 'field_ctrl': return 'Feldsteuerung';
|
||
case 'screen': return 'Schirm';
|
||
case 'long_water': return 'Längswasserdichtigkeit';
|
||
case 'trans_water': return 'Querwasserdichtigkeit';
|
||
case 'sheath_mat': return 'Mantelmaterial';
|
||
case 'sheath_color': return 'Mantelfarbe';
|
||
case 'flame_ret': return 'Flammwidrigkeit';
|
||
case 'uv_res': return 'UV-beständig';
|
||
case 'max_cond_temp': return 'Max. zulässige Leitertemperatur';
|
||
case 'out_temp_fixed': return 'Zul. Kabelaußentemperatur, fest verlegt';
|
||
case 'out_temp_motion': return 'Zul. Kabelaußentemperatur, in Bewegung';
|
||
case 'max_sc_temp_val': return 'Maximale Kurzschlußtemperatur';
|
||
case 'min_bend_fixed': return 'Min. Biegeradius, fest verlegt';
|
||
case 'min_lay_temp_val': return 'Mindesttemperatur Verlegung';
|
||
case 'meter_mark': return 'Metermarkierung';
|
||
case 'partial_dis': return 'Teilentladung';
|
||
case 'cap': return 'Kapazität';
|
||
case 'X': return 'Reaktanz';
|
||
case 'test_volt': return 'Prüfspannung';
|
||
case 'rated_volt': return 'Nennspannung';
|
||
case 'temp_range': return 'Temperaturbereich';
|
||
case 'Wm': return 'Manteldicke';
|
||
case 'Wi': return 'Isolationsdicke';
|
||
case 'RI': return 'DC-Leiterwiderstand (20 °C)';
|
||
case 'Ø': return 'Außen-Ø';
|
||
case 'Rbv': return 'Biegeradius';
|
||
case 'cpr': return 'CPR-Klasse';
|
||
case 'flame': return 'Flammhemmend';
|
||
case 'G': return 'Gewicht';
|
||
case 'Fzv': return 'Zugkraft';
|
||
}
|
||
} else {
|
||
switch (key) {
|
||
case 'cond_mat': return 'Conductor material';
|
||
case 'cond_class': return 'Conductor class';
|
||
case 'core_ins': return 'Core insulation';
|
||
case 'field_ctrl': return 'Field control';
|
||
case 'screen': return 'Screen';
|
||
case 'long_water': return 'Longitudinal water tightness';
|
||
case 'trans_water': return 'Transverse water tightness';
|
||
case 'sheath_mat': return 'Sheath material';
|
||
case 'sheath_color': return 'Sheath color';
|
||
case 'flame_ret': return 'Flame retardancy';
|
||
case 'uv_res': return 'UV resistant';
|
||
case 'max_cond_temp': return 'Max. permissible conductor temperature';
|
||
case 'out_temp_fixed': return 'Permissible cable outer temperature, fixed';
|
||
case 'out_temp_motion': return 'Permissible cable outer temperature, in motion';
|
||
case 'max_sc_temp_val': return 'Maximum short-circuit temperature';
|
||
case 'min_bend_fixed': return 'Min. bending radius, fixed';
|
||
case 'min_lay_temp_val': return 'Minimum laying temperature';
|
||
case 'meter_mark': return 'Meter marking';
|
||
case 'partial_dis': return 'Partial discharge';
|
||
case 'cap': return 'Capacitance';
|
||
case 'X': return 'Reactance';
|
||
case 'test_volt': return 'Test voltage';
|
||
case 'rated_volt': return 'Rated voltage';
|
||
case 'temp_range': return 'Operating temperature range';
|
||
case 'Wm': return 'Sheath thickness';
|
||
case 'Wi': return 'Insulation thickness';
|
||
case 'RI': return 'DC resistance (20 °C)';
|
||
case 'Ø': return 'Outer diameter';
|
||
case 'Rbv': return 'Bending radius';
|
||
case 'cpr': return 'CPR class';
|
||
case 'flame': return 'Flame retardant';
|
||
case 'G': return 'Weight';
|
||
case 'Fzv': return 'Pulling force';
|
||
}
|
||
}
|
||
|
||
// Fallback for unmapped keys (should be rare if columnMapping is comprehensive)
|
||
const raw = normalizeValue(args.excelKey);
|
||
if (!raw) return '';
|
||
|
||
if (args.locale === 'de') {
|
||
return raw
|
||
.replace(/\(approx\.?\)/gi, '(ca.)')
|
||
.replace(/\bpackaging\b/gi, 'Verpackung')
|
||
.replace(/\bce\s*-?conformity\b/gi, 'CE-Konformität');
|
||
}
|
||
|
||
return raw
|
||
.replace(/\bpackaging\b/gi, 'Packaging')
|
||
.replace(/\bce\s*-?conformity\b/gi, 'CE conformity');
|
||
}
|
||
|
||
function technicalValueTranslation(args: { label: string; value: string; locale: 'en' | 'de' }): string {
|
||
const v = normalizeValue(args.value);
|
||
if (!v) return '';
|
||
|
||
if (args.locale === 'de') {
|
||
if (/^yes$/i.test(v)) return 'ja';
|
||
if (/^no$/i.test(v)) return 'nein';
|
||
if (/^copper$/i.test(v)) return 'Kupfer';
|
||
if (/^aluminum$/i.test(v)) return 'Aluminium';
|
||
if (/^black$/i.test(v)) return 'schwarz';
|
||
if (/^stranded$/i.test(v)) return 'mehrdrähtig';
|
||
if (/^(\d+)xD$/i.test(v)) return v.replace(/^(\d+)xD$/i, '$1 facher Durchmesser');
|
||
if (/^XLPE/i.test(v)) return v.replace(/^XLPE/i, 'VPE');
|
||
if (/^yes, with swelling tape$/i.test(v)) return 'ja, mit Quellvliess';
|
||
if (/^yes, Al-tape$/i.test(v)) return 'ja, Al-Band';
|
||
if (/^Polyethylene/i.test(v)) return v.replace(/^Polyethylene/i, 'Polyethylen');
|
||
if (/^Class 2 stranded$/i.test(v)) return 'Klasse 2 mehrdrähtig';
|
||
if (/^VPE DIX8$/i.test(v)) return 'VPE DIX8';
|
||
if (/^inner and outer semiconducting layer made of semiconducting plastic - 3-fold extruded$/i.test(v)) return 'innere und äußere Leitschicht aus halbleitendem Kunststoff - 3-fach-extrudiert';
|
||
if (/^copper wires \+ transverse conductive helix$/i.test(v)) return 'Kupferdrähte + Querleitwendel';
|
||
if (/^Polyethylene DMP2$/i.test(v)) return 'Polyethylen DMP2';
|
||
if (/^15 times diameter$/i.test(v)) return '15 facher Durchmesser';
|
||
|
||
// Fallback for partial matches or common terms
|
||
return v
|
||
.replace(/\bcopper\b/gi, 'Kupfer')
|
||
.replace(/\baluminum\b/gi, 'Aluminium')
|
||
.replace(/\bblack\b/gi, 'schwarz')
|
||
.replace(/\bstranded\b/gi, 'mehrdrähtig')
|
||
.replace(/\byes\b/gi, 'ja')
|
||
.replace(/\bno\b/gi, 'nein')
|
||
.replace(/\bPolyethylene\b/gi, 'Polyethylen')
|
||
.replace(/\bXLPE\b/gi, 'VPE');
|
||
}
|
||
|
||
if (/^ja$/i.test(v)) return 'yes';
|
||
if (/^nein$/i.test(v)) return 'no';
|
||
if (/^kupfer$/i.test(v)) return 'Copper';
|
||
if (/^aluminium$/i.test(v)) return 'Aluminum';
|
||
if (/^schwarz$/i.test(v)) return 'black';
|
||
if (/^mehrdrähtig$/i.test(v)) return 'stranded';
|
||
if (/^(\d+)xD$/i.test(v)) return v.replace(/^(\d+)xD$/i, '$1 times diameter');
|
||
if (/^VPE/i.test(v)) return v.replace(/^VPE/i, 'XLPE');
|
||
if (/^ja, mit Quellvliess$/i.test(v)) return 'yes, with swelling tape';
|
||
if (/^ja, Al-Band$/i.test(v)) return 'yes, Al-tape';
|
||
if (/^Polyethylen/i.test(v)) return v.replace(/^Polyethylen/i, 'Polyethylene');
|
||
if (/^Klasse 2 mehrdrähtig$/i.test(v)) return 'Class 2 stranded';
|
||
if (/^innere und äußere Leitschicht aus halbleitendem Kunststoff - 3-fach-extrudiert$/i.test(v)) return 'inner and outer semiconducting layer made of semiconducting plastic - 3-fold extruded';
|
||
if (/^Kupferdrähte \+ Querleitwendel$/i.test(v)) return 'copper wires + transverse conductive helix';
|
||
if (/^Polyethylen DMP2$/i.test(v)) return 'Polyethylene DMP2';
|
||
if (/^15 facher Durchmesser$/i.test(v)) return '15 times diameter';
|
||
|
||
// Fallback for partial matches or common terms
|
||
return v
|
||
.replace(/\bkupfer\b/gi, 'Copper')
|
||
.replace(/\baluminium\b/gi, 'Aluminum')
|
||
.replace(/\bschwarz\b/gi, 'black')
|
||
.replace(/\bmehrdrähtig\b/gi, 'stranded')
|
||
.replace(/\bja\b/gi, 'yes')
|
||
.replace(/\bnein\b/gi, 'no')
|
||
.replace(/\bPolyethylen\b/gi, 'Polyethylene')
|
||
.replace(/\bVPE\b/gi, 'XLPE');
|
||
}
|
||
|
||
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 denseAbbrevLabel(args: { key: string; locale: 'en' | 'de'; unit?: string }): string {
|
||
const u = normalizeUnit(args.unit || '');
|
||
const unitSafe = u.replace(/Ω/gi, 'Ohm').replace(/[\u00B5\u03BC]/g, 'u');
|
||
const suffix = 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';
|
||
// Electrical
|
||
case 'cap':
|
||
// Capacitance. Use a clear label; lowercase "cap" looks like an internal key.
|
||
return `Cap${suffix}`;
|
||
case 'X':
|
||
return `X${suffix}`;
|
||
case 'test_volt':
|
||
return `U_test${suffix}`;
|
||
case 'rated_volt':
|
||
return `U0/U${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}`;
|
||
default:
|
||
return args.key || '';
|
||
}
|
||
}
|
||
|
||
function translateAbbreviation(abbrev: string, description: string, locale: 'en' | 'de'): string {
|
||
const normalizedDesc = normalizeValue(description);
|
||
if (!normalizedDesc) return description;
|
||
|
||
// German translations for common abbreviations
|
||
if (locale === 'de') {
|
||
switch (abbrev) {
|
||
case 'DI':
|
||
return 'Durchmesser über Isolation';
|
||
case 'RI':
|
||
return 'Widerstand Leiter';
|
||
case 'Wi':
|
||
return 'Isolationsdicke';
|
||
case 'Ibl':
|
||
return 'Strombelastbarkeit Luft';
|
||
case 'Ibe':
|
||
return 'Strombelastbarkeit Erde';
|
||
case 'Wm':
|
||
return 'Manteldicke';
|
||
case 'Rbv':
|
||
return 'Biegeradius';
|
||
case 'Fzv':
|
||
return 'Zugkraft';
|
||
case 'G':
|
||
return 'Gewicht';
|
||
case 'Ik_cond':
|
||
return 'Kurzschlussstrom Leiter';
|
||
case 'Ik_screen':
|
||
return 'Kurzschlussstrom Schirm';
|
||
case 'Ø':
|
||
return 'Außen-Ø';
|
||
case 'Cond':
|
||
return 'Leiter';
|
||
case 'shape':
|
||
return 'Form';
|
||
case 'cap':
|
||
return 'Kapazität';
|
||
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';
|
||
default:
|
||
return normalizedDesc;
|
||
}
|
||
}
|
||
|
||
// English translations for common abbreviations
|
||
switch (abbrev) {
|
||
case 'DI':
|
||
return 'Diameter over insulation';
|
||
case 'RI':
|
||
return 'DC resistance';
|
||
case 'Wi':
|
||
return 'Insulation thickness';
|
||
case 'Ibl':
|
||
return 'Current rating in air';
|
||
case 'Ibe':
|
||
return 'Current rating in ground';
|
||
case 'Wm':
|
||
return 'Sheath thickness';
|
||
case 'Rbv':
|
||
return 'Bending radius';
|
||
case 'Fzv':
|
||
return 'Pulling force';
|
||
case 'G':
|
||
return 'Weight';
|
||
case 'Ik_cond':
|
||
return 'Short-circuit current conductor';
|
||
case 'Ik_screen':
|
||
return 'Short-circuit current screen';
|
||
case 'Ø':
|
||
return 'Outer diameter';
|
||
case 'Cond':
|
||
return 'Conductor';
|
||
case 'shape':
|
||
return 'Shape';
|
||
case 'cap':
|
||
return 'Capacitance';
|
||
case 'X':
|
||
return 'Reactance';
|
||
case 'test_volt':
|
||
return 'Test voltage';
|
||
case 'rated_volt':
|
||
return 'Rated voltage';
|
||
case 'temp_range':
|
||
return 'Operating temperature range';
|
||
case 'max_op_temp':
|
||
return 'Max operating temperature';
|
||
case 'max_sc_temp':
|
||
return 'Max short-circuit temperature';
|
||
case 'min_store_temp':
|
||
return 'Min storage temperature';
|
||
case 'min_lay_temp':
|
||
return 'Min laying temperature';
|
||
case 'cpr':
|
||
return 'CPR class';
|
||
case 'flame':
|
||
return 'Flame retardant';
|
||
default:
|
||
return normalizedDesc;
|
||
}
|
||
}
|
||
|
||
function summarizeOptions(options: string[] | undefined): 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];
|
||
// Never use ellipsis truncation in datasheets. Prefer full value list.
|
||
// (Long values should be handled by layout; if needed we can later add wrapping rules.)
|
||
return uniq.join(' / ');
|
||
}
|
||
|
||
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];
|
||
const fmt = (n: number) => (Number.isInteger(n) ? String(n) : String(n)).replace(/\.0+$/, '');
|
||
return { ok: true, text: `${fmt(min)}–${fmt(max)}` };
|
||
}
|
||
|
||
function summarizeSmartOptions(_label: string, options: string[] | undefined): string {
|
||
const range = summarizeNumericRange(options);
|
||
if (range.ok) return range.text;
|
||
return summarizeOptions(options);
|
||
}
|
||
|
||
function normalizeDesignation(value: string): string {
|
||
return String(value || '')
|
||
.toUpperCase()
|
||
.replace(/-\d+$/g, '')
|
||
.replace(/[^A-Z0-9]+/g, '');
|
||
}
|
||
|
||
function buildExcelModel(args: { product: ProductData; locale: 'en' | 'de' }): BuildExcelModelResult {
|
||
const match = findExcelForProduct(args.product) as ExcelMatch | null;
|
||
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' },
|
||
|
||
'shape of conductor': { header: 'Conductor shape', unit: '', key: 'shape' },
|
||
'operating temperature range': { header: 'Operating temp range', unit: '°C', key: 'temp_range' },
|
||
'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' },
|
||
'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' },
|
||
'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' },
|
||
|
||
'conductor material': { header: 'Conductor material', unit: '', key: 'cond_mat' },
|
||
'conductor class': { header: 'Conductor class', unit: '', key: 'cond_class' },
|
||
'core insulation': { header: 'Core insulation', unit: '', key: 'core_ins' },
|
||
'field control': { header: 'Field control', unit: '', key: 'field_ctrl' },
|
||
'screen': { header: 'Screen', unit: '', key: 'screen' },
|
||
'longitudinal water tightness': { header: 'Longitudinal water tightness', unit: '', key: 'long_water' },
|
||
'transverse water tightness': { header: 'Transverse water tightness', unit: '', key: 'trans_water' },
|
||
'sheath material': { header: 'Sheath material', unit: '', key: 'sheath_mat' },
|
||
'sheath color': { header: 'Sheath color', unit: '', key: 'sheath_color' },
|
||
'flame retardancy': { header: 'Flame retardancy', unit: '', key: 'flame_ret' },
|
||
'uv resistant': { header: 'UV resistant', unit: '', key: 'uv_res' },
|
||
'max. permissible conductor temperature': { header: 'Max. permissible conductor temperature', unit: '°C', key: 'max_cond_temp' },
|
||
'permissible cable outer temperature, fixed': { header: 'Permissible cable outer temperature, fixed', unit: '°C', key: 'out_temp_fixed' },
|
||
'permissible cable outer temperature, in motion': { header: 'Permissible cable outer temperature, in motion', unit: '°C', key: 'out_temp_motion' },
|
||
'maximum short-circuit temperature': { header: 'Maximum short-circuit temperature', unit: '°C', key: 'max_sc_temp_val' },
|
||
'min. bending radius, fixed': { header: 'Min. bending radius, fixed', unit: '', key: 'min_bend_fixed' },
|
||
'minimum laying temperature': { header: 'Minimum laying temperature', unit: '°C', key: 'min_lay_temp_val' },
|
||
'meter marking': { header: 'Meter marking', unit: '', key: 'meter_mark' },
|
||
'partial discharge': { header: 'Partial discharge', unit: 'pC', key: 'partial_dis' },
|
||
|
||
// High-value electrical/screen columns
|
||
'capacitance (approx.)': { header: 'Capacitance', unit: 'uF/km', key: 'cap' },
|
||
'capacitance': { header: 'Capacitance', unit: 'uF/km', key: 'cap' },
|
||
'reactance': { header: 'Reactance', unit: 'Ohm/km', key: 'X' },
|
||
'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' },
|
||
};
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
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 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 technicalItems: KeyValueItem[] = [];
|
||
const globalConstantColumns = new Set<string>();
|
||
|
||
for (const { excelKey, mapping } of deduplicated) {
|
||
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 rawValue = compactCellForDenseTable(values[0], unit, args.locale);
|
||
const value = technicalValueTranslation({ label: labelBase, value: rawValue, locale: args.locale });
|
||
if (!technicalItems.find(t => t.label === label)) technicalItems.push({ label, value, unit });
|
||
}
|
||
}
|
||
const TECHNICAL_DATA_ORDER_DE = [
|
||
'Leitermaterial',
|
||
'Leiterklasse',
|
||
'Aderisolation',
|
||
'Feldsteuerung',
|
||
'Schirm',
|
||
'Längswasserdichtigkeit',
|
||
'Querwasserdichtigkeit',
|
||
'Mantelmaterial',
|
||
'Mantelfarbe',
|
||
'Flammwidrigkeit',
|
||
'UV-beständig',
|
||
'Max. zulässige Leitertemperatur',
|
||
'Zul. Kabelaußentemperatur, fest verlegt',
|
||
'Zul. Kabelaußentemperatur, in Bewegung',
|
||
'Maximale Kurzschlußtemperatur',
|
||
'Min. Biegeradius, fest verlegt',
|
||
'Mindesttemperatur Verlegung',
|
||
'Metermarkierung',
|
||
'Teilentladung',
|
||
];
|
||
|
||
const TECHNICAL_DATA_ORDER_EN = [
|
||
'Conductor material',
|
||
'Conductor class',
|
||
'Core insulation',
|
||
'Field control',
|
||
'Screen',
|
||
'Longitudinal water tightness',
|
||
'Transverse water tightness',
|
||
'Sheath material',
|
||
'Sheath color',
|
||
'Flame retardancy',
|
||
'UV resistant',
|
||
'Max. permissible conductor temperature',
|
||
'Permissible cable outer temperature, fixed',
|
||
'Permissible cable outer temperature, in motion',
|
||
'Maximum short-circuit temperature',
|
||
'Min. bending radius, fixed',
|
||
'Minimum laying temperature',
|
||
'Meter marking',
|
||
'Partial discharge',
|
||
];
|
||
|
||
const order = args.locale === 'de' ? TECHNICAL_DATA_ORDER_DE : TECHNICAL_DATA_ORDER_EN;
|
||
|
||
technicalItems.sort((a, b) => {
|
||
const indexA = order.findIndex(label => a.label.startsWith(label));
|
||
const indexB = order.findIndex(label => b.label.startsWith(label));
|
||
|
||
if (indexA !== -1 && indexB !== -1) return indexA - indexB;
|
||
if (indexA !== -1) return -1;
|
||
if (indexB !== -1) return 1;
|
||
return a.label.localeCompare(b.label);
|
||
});
|
||
|
||
const voltageTables: VoltageTableModel[] = [];
|
||
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 metaKeyPriority = [
|
||
'test_volt',
|
||
'temp_range',
|
||
'max_op_temp',
|
||
'max_sc_temp',
|
||
'min_lay_temp',
|
||
'min_store_temp',
|
||
'cpr',
|
||
'flame',
|
||
];
|
||
const metaKeyPrioritySet = new Set(metaKeyPriority);
|
||
|
||
const denseTableKeyOrder = [
|
||
'Cond',
|
||
'shape',
|
||
// Electrical properties (when present)
|
||
'cap',
|
||
'X',
|
||
// Dimensions and ratings
|
||
'DI',
|
||
'RI',
|
||
'Wi',
|
||
'Ibl',
|
||
'Ibe',
|
||
'Ik_cond',
|
||
'Wm',
|
||
'Rbv',
|
||
'Ø',
|
||
// Screen data (when present)
|
||
'D_screen',
|
||
'S_screen',
|
||
'Fzv',
|
||
'G',
|
||
] as const;
|
||
const denseTableKeys = new Set<string>(denseTableKeyOrder);
|
||
|
||
const tableColumns: Array<{ excelKey: string; mapping: { header: string; unit: string; key: string } }> = [];
|
||
for (const { excelKey, mapping } of deduplicated) {
|
||
if (excelKey === csKey || excelKey === voltageKey) continue;
|
||
const values = indices.map(idx => normalizeValue(String(compatibleRows[idx]?.[excelKey] ?? ''))).filter(Boolean);
|
||
if (!values.length) continue;
|
||
const unique = Array.from(new Set(values.map(v => v.toLowerCase())));
|
||
const unit = normalizeUnit(units[excelKey] || mapping.unit || '');
|
||
|
||
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(excelKey, 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);
|
||
}
|
||
|
||
// If conductor material is missing in Excel, derive it from designation.
|
||
// NA... => Al, N... => Cu (common for this dataset).
|
||
if (!mappedByKey.has('Cond')) {
|
||
mappedByKey.set('Cond', {
|
||
excelKey: '',
|
||
mapping: { header: 'Cond.', unit: '', key: 'Cond' },
|
||
});
|
||
}
|
||
|
||
const orderedTableColumns = denseTableKeyOrder
|
||
.filter(k => mappedByKey.has(k))
|
||
.map(k => mappedByKey.get(k)!)
|
||
.map(({ excelKey, mapping }) => {
|
||
const unit = normalizeUnit((excelKey ? units[excelKey] : '') || mapping.unit || '');
|
||
return {
|
||
key: mapping.key,
|
||
label: denseAbbrevLabel({ key: mapping.key, locale: args.locale, unit }) || formatExcelHeaderLabel(excelKey, unit),
|
||
get: (rowIndex: number) => {
|
||
const srcRowIndex = indices[rowIndex];
|
||
|
||
if (mapping.key === 'Cond' && !excelKey) {
|
||
const pn = normalizeDesignation(args.product.name || args.product.slug || args.product.sku || '');
|
||
if (/^NA/.test(pn)) return 'Al';
|
||
if (/^N/.test(pn)) return 'Cu';
|
||
return '';
|
||
}
|
||
|
||
const raw = excelKey ? normalizeValue(String(compatibleRows[srcRowIndex]?.[excelKey] ?? '')) : '';
|
||
return compactCellForDenseTable(raw, unit, args.locale);
|
||
},
|
||
};
|
||
});
|
||
|
||
voltageTables.push({ voltageLabel: vKey, metaItems, crossSections, columns: orderedTableColumns });
|
||
}
|
||
|
||
return { ok: true, technicalItems, voltageTables };
|
||
}
|
||
|
||
function isMediumVoltageProduct(product: ProductData): boolean {
|
||
const hay = [product.slug, product.path, product.translationKey, ...(product.categories || []).map(c => c.name)]
|
||
.filter(Boolean)
|
||
.join(' ');
|
||
return /medium[-\s]?voltage|mittelspannung/i.test(hay);
|
||
}
|
||
|
||
type AbbrevColumn = { colKey: string; unit: string };
|
||
|
||
function isAbbreviatedHeaderKey(key: string): boolean {
|
||
const k = normalizeValue(key);
|
||
if (!k) return false;
|
||
if (/^__EMPTY/i.test(k)) return false;
|
||
|
||
// Examples from the MV sheet: "LD mm", "RI Ohm", "G kg", "SBL 30", "SBE 20", "BK", "BR", "LF".
|
||
// Keep this permissive but focused on compact, non-sentence identifiers.
|
||
if (k.length > 12) return false;
|
||
if (/[a-z]{4,}/.test(k)) return false;
|
||
if (!/[A-ZØ]/.test(k)) return false;
|
||
return true;
|
||
}
|
||
|
||
function extractAbbrevColumnsFromMediumVoltageHeader(args: {
|
||
headerRow: Record<string, unknown>;
|
||
units: Record<string, string>;
|
||
partNumberKey: string;
|
||
crossSectionKey: string;
|
||
ratedVoltageKey: string | null;
|
||
}): AbbrevColumn[] {
|
||
const out: AbbrevColumn[] = [];
|
||
|
||
for (const colKey of Object.keys(args.headerRow || {})) {
|
||
if (!colKey) continue;
|
||
if (colKey === args.partNumberKey) continue;
|
||
if (colKey === args.crossSectionKey) continue;
|
||
if (args.ratedVoltageKey && colKey === args.ratedVoltageKey) continue;
|
||
|
||
if (!isAbbreviatedHeaderKey(colKey)) continue;
|
||
|
||
const unit = normalizeUnit(args.units[colKey] || '');
|
||
out.push({ colKey, unit });
|
||
}
|
||
|
||
return out;
|
||
}
|
||
|
||
function buildMediumVoltageCrossSectionTableFromNewExcel(args: {
|
||
product: ProductData;
|
||
locale: 'en' | 'de';
|
||
}): BuildExcelModelResult & { legendItems: KeyValueItem[] } {
|
||
const mv = findMediumVoltageCrossSectionExcelForProduct(args.product) as MediumVoltageCrossSectionExcelMatch | null;
|
||
if (!mv || !mv.rows.length) return { ok: false, technicalItems: [], voltageTables: [], legendItems: [] };
|
||
if (!mv.crossSectionKey) return { ok: false, technicalItems: [], voltageTables: [], legendItems: [] };
|
||
|
||
const abbrevCols = extractAbbrevColumnsFromMediumVoltageHeader({
|
||
headerRow: mv.headerRow,
|
||
units: mv.units,
|
||
partNumberKey: mv.partNumberKey,
|
||
crossSectionKey: mv.crossSectionKey,
|
||
ratedVoltageKey: mv.ratedVoltageKey,
|
||
});
|
||
if (!abbrevCols.length) return { ok: false, technicalItems: [], voltageTables: [], legendItems: [] };
|
||
|
||
// Collect legend items: abbreviation -> description from header row
|
||
const legendItems: KeyValueItem[] = [];
|
||
for (const col of abbrevCols) {
|
||
const description = normalizeValue(String(mv.headerRow[col.colKey] || ''));
|
||
if (description && description !== col.colKey) {
|
||
const translatedDescription = translateAbbreviation(col.colKey, description, args.locale);
|
||
legendItems.push({
|
||
label: col.colKey,
|
||
value: translatedDescription,
|
||
});
|
||
}
|
||
}
|
||
|
||
const byVoltage = new Map<string, number[]>();
|
||
for (let i = 0; i < mv.rows.length; i++) {
|
||
const cs = normalizeValue(String((mv.rows[i] as Record<string, unknown>)?.[mv.crossSectionKey] ?? ''));
|
||
if (!cs) continue;
|
||
|
||
const rawV = mv.ratedVoltageKey
|
||
? normalizeValue(String((mv.rows[i] as Record<string, unknown>)?.[mv.ratedVoltageKey] ?? ''))
|
||
: '';
|
||
|
||
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 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 voltageTables: VoltageTableModel[] = [];
|
||
for (const vKey of voltageKeysSorted) {
|
||
const indices = byVoltage.get(vKey) || [];
|
||
if (!indices.length) continue;
|
||
|
||
const crossSections = indices.map(idx =>
|
||
normalizeValue(String((mv.rows[idx] as Record<string, unknown>)?.[mv.crossSectionKey] ?? '')),
|
||
);
|
||
|
||
const metaItems: KeyValueItem[] = [];
|
||
if (mv.ratedVoltageKey) {
|
||
const rawV = normalizeValue(String((mv.rows[indices[0]] as Record<string, unknown>)?.[mv.ratedVoltageKey] ?? ''));
|
||
metaItems.push({
|
||
label: args.locale === 'de' ? 'Spannung' : 'Voltage',
|
||
value: normalizeVoltageLabel(rawV || ''),
|
||
});
|
||
}
|
||
|
||
const columns = abbrevCols.map(col => {
|
||
return {
|
||
key: col.colKey,
|
||
// Use the abbreviated title from the first row as the table header.
|
||
label: normalizeValue(col.colKey),
|
||
get: (rowIndex: number) => {
|
||
const srcRowIndex = indices[rowIndex];
|
||
const raw = normalizeValue(String((mv.rows[srcRowIndex] as Record<string, unknown>)?.[col.colKey] ?? ''));
|
||
return compactCellForDenseTable(raw, col.unit, args.locale);
|
||
},
|
||
};
|
||
});
|
||
|
||
voltageTables.push({ voltageLabel: vKey, metaItems, crossSections, columns });
|
||
}
|
||
|
||
return { ok: true, technicalItems: [], voltageTables, legendItems };
|
||
}
|
||
|
||
export function buildDatasheetModel(args: { product: ProductData; locale: 'en' | 'de' }): DatasheetModel {
|
||
const labels = getLabels(args.locale);
|
||
const categoriesLine = (args.product.categories || []).map(c => stripHtml(c.name)).join(' • ');
|
||
const descriptionText = stripHtml(args.product.shortDescriptionHtml || args.product.descriptionHtml || '');
|
||
const heroSrc = resolveMediaToLocalPath(args.product.featuredImage || args.product.images?.[0] || null);
|
||
const productUrl = getProductUrl(args.product);
|
||
|
||
// Technical data MUST stay sourced from the existing Excel index (legacy sheets).
|
||
const excelModel = buildExcelModel({ product: args.product, locale: args.locale });
|
||
|
||
// Cross-section tables: for medium voltage only, prefer the new MV sheet (abbrev columns in header row).
|
||
const crossSectionModel = isMediumVoltageProduct(args.product)
|
||
? buildMediumVoltageCrossSectionTableFromNewExcel({ product: args.product, locale: args.locale })
|
||
: { ok: false, technicalItems: [], voltageTables: [], legendItems: [] };
|
||
|
||
const voltageTablesSrc = crossSectionModel.ok
|
||
? crossSectionModel.voltageTables
|
||
: excelModel.ok
|
||
? excelModel.voltageTables
|
||
: [];
|
||
|
||
const voltageTables: DatasheetVoltageTable[] = voltageTablesSrc.map(t => {
|
||
const columns = t.columns.map(c => ({ key: c.key, label: c.label }));
|
||
const rows = t.crossSections.map((configuration, rowIndex) => ({
|
||
configuration,
|
||
cells: t.columns.map(c => compactNumericForLocale(c.get(rowIndex), args.locale)),
|
||
}));
|
||
return {
|
||
voltageLabel: t.voltageLabel,
|
||
metaItems: t.metaItems,
|
||
columns,
|
||
rows,
|
||
};
|
||
});
|
||
|
||
return {
|
||
locale: args.locale,
|
||
product: {
|
||
id: args.product.id,
|
||
name: stripHtml(args.product.name),
|
||
sku: args.product.sku,
|
||
categoriesLine,
|
||
descriptionText,
|
||
heroSrc,
|
||
productUrl,
|
||
},
|
||
labels,
|
||
technicalItems: [
|
||
...(excelModel.ok ? excelModel.technicalItems : []),
|
||
...(isMediumVoltageProduct(args.product)
|
||
? args.locale === 'de'
|
||
? [
|
||
{ label: 'Prüfspannung 6/10 kV', value: '21 kV' },
|
||
{ label: 'Prüfspannung 12/20 kV', value: '42 kV' },
|
||
{ label: 'Prüfspannung 18/30 kV', value: '63 kV' },
|
||
]
|
||
: [
|
||
{ label: 'Test voltage 6/10 kV', value: '21 kV' },
|
||
{ label: 'Test voltage 12/20 kV', value: '42 kV' },
|
||
{ label: 'Test voltage 18/30 kV', value: '63 kV' },
|
||
]
|
||
: []),
|
||
],
|
||
voltageTables,
|
||
legendItems: crossSectionModel.legendItems || [],
|
||
};
|
||
}
|