import * as fs from 'fs'; import * as path from 'path'; import { execSync } from 'child_process'; import type { ProductData } from './types'; import { normalizeValue } from './utils'; type ExcelRow = Record; export type ExcelMatch = { rows: ExcelRow[]; units: Record }; export type MediumVoltageCrossSectionExcelMatch = { headerRow: ExcelRow; rows: ExcelRow[]; units: Record; partNumberKey: string; crossSectionKey: string; ratedVoltageKey: string | null; }; const EXCEL_SOURCE_FILES = [ path.join(process.cwd(), 'data/excel/high-voltage.xlsx'), path.join(process.cwd(), 'data/excel/medium-voltage-KM.xlsx'), path.join(process.cwd(), 'data/excel/low-voltage-KM.xlsx'), path.join(process.cwd(), 'data/excel/solar-cables.xlsx'), ]; // Medium-voltage cross-section table (new format with multi-row header). // IMPORTANT: this must NOT be used for the technical data table. const MV_CROSS_SECTION_FILE = path.join(process.cwd(), 'data/excel/medium-voltage-KM 170126.xlsx'); type MediumVoltageCrossSectionIndex = { headerRow: ExcelRow; units: Record; partNumberKey: string; crossSectionKey: string; ratedVoltageKey: string | null; rowsByDesignation: Map; }; let EXCEL_INDEX: Map | null = null; let MV_CROSS_SECTION_INDEX: MediumVoltageCrossSectionIndex | null = null; export function normalizeExcelKey(value: string): string { return String(value || '') .toUpperCase() .replace(/-\d+$/g, '') .replace(/[^A-Z0-9]+/g, ''); } function loadExcelRows(filePath: string): ExcelRow[] { const out = execSync(`npx -y xlsx-cli -j "${filePath}"`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], }); const trimmed = out.trim(); const jsonStart = trimmed.indexOf('['); if (jsonStart < 0) return []; const jsonText = trimmed.slice(jsonStart); try { return JSON.parse(jsonText) as ExcelRow[]; } catch { return []; } } function findKeyByHeaderValue(headerRow: ExcelRow, pattern: RegExp): string | null { for (const [k, v] of Object.entries(headerRow || {})) { const text = normalizeValue(String(v ?? '')); if (!text) continue; if (pattern.test(text)) return k; } return null; } function getMediumVoltageCrossSectionIndex(): MediumVoltageCrossSectionIndex { if (MV_CROSS_SECTION_INDEX) return MV_CROSS_SECTION_INDEX; const rows = fs.existsSync(MV_CROSS_SECTION_FILE) ? loadExcelRows(MV_CROSS_SECTION_FILE) : []; const headerRow = (rows[0] || {}) as ExcelRow; const partNumberKey = findKeyByHeaderValue(headerRow, /^part\s*number$/i) || '__EMPTY'; const crossSectionKey = findKeyByHeaderValue(headerRow, /querschnitt|cross.?section/i) || ''; const ratedVoltageKey = findKeyByHeaderValue(headerRow, /rated voltage|voltage rating|nennspannung/i) || null; const unitsRow = rows.find(r => normalizeValue(String((r as ExcelRow)?.[partNumberKey] ?? '')) === 'Units') || null; const units: Record = {}; if (unitsRow) { for (const [k, v] of Object.entries(unitsRow)) { if (k === partNumberKey) continue; const unit = normalizeValue(String(v ?? '')); if (unit) units[k] = unit; } } const rowsByDesignation = new Map(); for (const r of rows) { if (r === headerRow) continue; const pn = normalizeValue(String((r as ExcelRow)?.[partNumberKey] ?? '')); if (!pn || pn === 'Units' || pn === 'Part Number') continue; const key = normalizeExcelKey(pn); if (!key) continue; const cur = rowsByDesignation.get(key) || []; cur.push(r); rowsByDesignation.set(key, cur); } MV_CROSS_SECTION_INDEX = { headerRow, units, partNumberKey, crossSectionKey, ratedVoltageKey, rowsByDesignation }; return MV_CROSS_SECTION_INDEX; } export function getExcelIndex(): Map { if (EXCEL_INDEX) return EXCEL_INDEX; 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: Record = {}; if (unitsRow) { for (const [k, v] of Object.entries(unitsRow)) { if (k === 'Part Number') continue; const unit = normalizeValue(String(v ?? '')); if (unit) units[k] = unit; } } for (const r of rows) { const pn = r?.['Part Number']; if (!pn || pn === 'Units') continue; const key = normalizeExcelKey(String(pn)); if (!key) continue; const cur = idx.get(key); if (!cur) { idx.set(key, { rows: [r], units }); } else { cur.rows.push(r); if (Object.keys(cur.units).length < Object.keys(units).length) cur.units = units; } } } EXCEL_INDEX = idx; return idx; } export function findExcelForProduct(product: ProductData): ExcelMatch | null { const idx = getExcelIndex(); const candidates = [ product.name, product.slug ? product.slug.replace(/-\d+$/g, '') : '', product.sku, product.translationKey, ].filter(Boolean) as string[]; for (const c of candidates) { const key = normalizeExcelKey(c); const match = idx.get(key); if (match && match.rows.length) return match; } return null; } export function findMediumVoltageCrossSectionExcelForProduct(product: ProductData): MediumVoltageCrossSectionExcelMatch | null { const idx = getMediumVoltageCrossSectionIndex(); const candidates = [ product.name, product.slug ? product.slug.replace(/-\d+$/g, '') : '', product.sku, product.translationKey, ].filter(Boolean) as string[]; for (const c of candidates) { const key = normalizeExcelKey(c); const rows = idx.rowsByDesignation.get(key) || []; if (rows.length) { return { headerRow: idx.headerRow, rows, units: idx.units, partNumberKey: idx.partNumberKey, crossSectionKey: idx.crossSectionKey, ratedVoltageKey: idx.ratedVoltageKey, }; } } return null; }