448 lines
22 KiB
TypeScript
448 lines
22 KiB
TypeScript
import { createHash } from 'node:crypto';
|
|
import type { ParsedSheet, ParsedWorkbook } from './asset-import-parser';
|
|
|
|
export type AssetImportProfileCode = 'MENDOZA_INVENTORY_V1' | 'MENDOZA_YACIMIENTOS_V1' | 'UNKNOWN';
|
|
export type AssetImportRowStatus = 'READY' | 'WARNING' | 'CONFLICT' | 'IGNORED';
|
|
export type AssetImportSuggestedAction = 'CREATE' | 'MATCH' | 'REVIEW' | 'IGNORE';
|
|
|
|
export interface ImportProfileDetection {
|
|
profileCode: AssetImportProfileCode;
|
|
confidence: number;
|
|
sheetName: string;
|
|
headerRow: number;
|
|
columnMap: Record<string, number>;
|
|
detectedHeaders: string[];
|
|
}
|
|
|
|
export interface ImportNormalizedRow {
|
|
profileCode: AssetImportProfileCode;
|
|
rowNumber: number;
|
|
sheetName: string;
|
|
raw: Record<string, string>;
|
|
normalized: Record<string, unknown>;
|
|
status: AssetImportRowStatus;
|
|
suggestedAction: AssetImportSuggestedAction;
|
|
issues: string[];
|
|
fingerprint: string;
|
|
}
|
|
|
|
export interface ImportAnalysis {
|
|
detection: ImportProfileDetection;
|
|
rows: ImportNormalizedRow[];
|
|
summary: {
|
|
totalRows: number;
|
|
readyRows: number;
|
|
warningRows: number;
|
|
conflictRows: number;
|
|
ignoredRows: number;
|
|
issueCounts: Record<string, number>;
|
|
};
|
|
}
|
|
|
|
function plain(value: string): string {
|
|
return value
|
|
.normalize('NFD')
|
|
.replace(/[\u0300-\u036f]/g, '')
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, ' ')
|
|
.trim()
|
|
.replace(/\s+/g, ' ');
|
|
}
|
|
|
|
function compact(value: string): string {
|
|
return plain(value).replace(/\s+/g, '');
|
|
}
|
|
|
|
const inventoryAliases: Record<string, string[]> = {
|
|
item: ['item', 'n item', 'numero item'],
|
|
areaOrField: ['area yacimiento', 'area/yacimiento', 'area y yacimiento', 'area yacimiento '],
|
|
installation: ['instalacion', 'intalacion'],
|
|
subInstallation: ['sub instalacion', 'subinstalacion'],
|
|
equipment: ['equipo'],
|
|
equipmentDenomination: ['denominacion del equipo', 'denominacion equipo'],
|
|
location: ['ubicacion del equipo o instalacion', 'ubicacion equipo o instalacion', 'ubicacion'],
|
|
inventoryId: ['n id inventario', 'n° id inventario', 'nº id inventario', 'id inventario', 'numero id inventario'],
|
|
quantity: ['cantidad'],
|
|
technicalSpecs: ['especificaciones tecnicas', 'especificacion tecnica'],
|
|
sourceStatus: ['estado en servicio fuera de servicio', 'estado', 'estado servicio'],
|
|
};
|
|
|
|
const fieldAliases: Record<string, string[]> = {
|
|
field: ['yacimiento', 'nombre yacimiento'],
|
|
area: ['area', 'area hidrocarburifera'],
|
|
department: ['departamento'],
|
|
rightType: ['tipo concesion', 'tipo de concesion', 'tipo permiso', 'tipo'],
|
|
operator: ['operadora', 'operador', 'empresa operadora'],
|
|
};
|
|
|
|
function aliasScore(header: string, aliases: string[]): number {
|
|
const normalized = plain(header);
|
|
const normalizedCompact = compact(header);
|
|
let best = 0;
|
|
for (const alias of aliases) {
|
|
const a = plain(alias);
|
|
const ac = compact(alias);
|
|
if (normalized === a || normalizedCompact === ac) best = Math.max(best, 10 + a.length / 100);
|
|
else if (normalized.includes(a) || a.includes(normalized)) best = Math.max(best, 6 + Math.min(a.length, normalized.length) / 100);
|
|
}
|
|
return best;
|
|
}
|
|
|
|
function mapHeaders(headers: string[], aliases: Record<string, string[]>): { map: Record<string, number>; score: number } {
|
|
const candidates: Array<{ field: string; index: number; score: number }> = [];
|
|
headers.forEach((header, index) => {
|
|
Object.entries(aliases).forEach(([field, values]) => {
|
|
const score = aliasScore(header, values);
|
|
if (score > 0) candidates.push({ field, index, score });
|
|
});
|
|
});
|
|
candidates.sort((a, b) => b.score - a.score);
|
|
const usedFields = new Set<string>();
|
|
const usedIndexes = new Set<number>();
|
|
const map: Record<string, number> = {};
|
|
let score = 0;
|
|
for (const candidate of candidates) {
|
|
if (usedFields.has(candidate.field) || usedIndexes.has(candidate.index)) continue;
|
|
usedFields.add(candidate.field);
|
|
usedIndexes.add(candidate.index);
|
|
map[candidate.field] = candidate.index;
|
|
score += candidate.score;
|
|
}
|
|
return { map, score };
|
|
}
|
|
|
|
function bestHeader(sheet: ParsedSheet, aliases: Record<string, string[]>): { row: number; map: Record<string, number>; score: number; headers: string[] } {
|
|
let best = { row: 0, map: {} as Record<string, number>, score: -1, headers: [] as string[] };
|
|
for (let index = 0; index < Math.min(sheet.rows.length, 30); index += 1) {
|
|
const headers = sheet.rows[index] ?? [];
|
|
const mapped = mapHeaders(headers, aliases);
|
|
if (mapped.score > best.score) best = { row: index + 1, map: mapped.map, score: mapped.score, headers };
|
|
}
|
|
return best;
|
|
}
|
|
|
|
export function detectImportProfile(workbook: ParsedWorkbook): ImportProfileDetection {
|
|
let best: ImportProfileDetection = { profileCode: 'UNKNOWN', confidence: 0, sheetName: workbook.sheets[0]?.name ?? 'Hoja', headerRow: 1, columnMap: {}, detectedHeaders: [] };
|
|
for (const sheet of workbook.sheets) {
|
|
const inventory = bestHeader(sheet, inventoryAliases);
|
|
const inventoryRequired = ['areaOrField', 'installation', 'equipment', 'inventoryId'];
|
|
const inventoryHits = inventoryRequired.filter((field) => inventory.map[field] !== undefined).length;
|
|
const inventoryConfidence = Math.min(100, Math.round((inventory.score / 95) * 100));
|
|
if (inventoryHits >= 3 && inventoryConfidence > best.confidence) {
|
|
best = { profileCode: 'MENDOZA_INVENTORY_V1', confidence: inventoryConfidence, sheetName: sheet.name, headerRow: inventory.row, columnMap: inventory.map, detectedHeaders: inventory.headers };
|
|
}
|
|
|
|
const fields = bestHeader(sheet, fieldAliases);
|
|
const fieldRequired = ['field', 'area', 'operator'];
|
|
const fieldHits = fieldRequired.filter((field) => fields.map[field] !== undefined).length;
|
|
const fieldConfidence = Math.min(100, Math.round((fields.score / 55) * 100));
|
|
if (fieldHits >= 2 && fieldConfidence > best.confidence) {
|
|
best = { profileCode: 'MENDOZA_YACIMIENTOS_V1', confidence: fieldConfidence, sheetName: sheet.name, headerRow: fields.row, columnMap: fields.map, detectedHeaders: fields.headers };
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
function text(row: string[], index: number | undefined): string {
|
|
return index === undefined ? '' : (row[index] ?? '').trim();
|
|
}
|
|
|
|
function asNumber(value: string): number | null {
|
|
if (!value.trim()) return null;
|
|
const normalized = value.trim().replace(/\s+/g, '').replace(',', '.');
|
|
const number = Number(normalized);
|
|
return Number.isFinite(number) ? number : null;
|
|
}
|
|
|
|
function inventoryStatusSuggestion(value: string): { operationalStatus?: string; conditionStatus?: string; ambiguous: boolean } {
|
|
const normalized = plain(value);
|
|
if (!normalized) return { ambiguous: false };
|
|
if (['en servicio', 'servicio', 'operativo', 'operativa'].includes(normalized)) return { operationalStatus: 'IN_SERVICE', ambiguous: false };
|
|
if (['fuera de servicio', 'f servicio', 'f serv', 'fs', 'f s'].includes(normalized)) return { operationalStatus: 'OUT_OF_SERVICE', ambiguous: false };
|
|
if (['bueno', 'buena'].includes(normalized)) return { conditionStatus: 'GOOD', ambiguous: true };
|
|
if (['regular'].includes(normalized)) return { conditionStatus: 'FAIR', ambiguous: true };
|
|
if (['malo', 'mala'].includes(normalized)) return { conditionStatus: 'POOR', ambiguous: true };
|
|
if (['si', 'no', 's', 'n'].includes(normalized)) return { ambiguous: true };
|
|
return { ambiguous: true };
|
|
}
|
|
|
|
function splitClassType(value: string): { sourceClass: string | null; sourceSubtype: string | null } {
|
|
const classMatch = /clase\s*:\s*([^;|]+)/i.exec(value)?.[1]?.trim() ?? null;
|
|
const typeMatch = /tipo\s*:\s*([^;|]+)/i.exec(value)?.[1]?.trim() ?? null;
|
|
return { sourceClass: classMatch, sourceSubtype: typeMatch };
|
|
}
|
|
|
|
function splitManufacturerModel(value: string): { manufacturer: string | null; model: string | null } {
|
|
const manufacturer = /fabricante\s*[;:]\s*([^;|]+)/i.exec(value)?.[1]?.trim() ?? null;
|
|
const model = /modelo\s*[;:]\s*([^;|]+)/i.exec(value)?.[1]?.trim() ?? null;
|
|
if (manufacturer || model) return { manufacturer, model };
|
|
const parts = value.split('|').map((part) => part.trim()).filter(Boolean);
|
|
if (parts.length >= 3) return { manufacturer: parts[1] ?? null, model: parts[2] ?? null };
|
|
return { manufacturer: null, model: null };
|
|
}
|
|
|
|
export interface TechnicalNormalizationSuggestion {
|
|
family: string | null;
|
|
subtype: string | null;
|
|
}
|
|
|
|
function technicalNormalizationSuggestion(
|
|
equipment: string,
|
|
denomination: string,
|
|
sourceClass: string | null,
|
|
sourceSubtype: string | null,
|
|
): TechnicalNormalizationSuggestion {
|
|
const source = plain(`${sourceClass ?? ''} ${sourceSubtype ?? ''} ${equipment} ${denomination}`);
|
|
const classCode = plain(sourceClass ?? '');
|
|
const subtypeCode = plain(sourceSubtype ?? '');
|
|
|
|
if (/rectificador/.test(plain(equipment))) return { family: 'equipo_electrico', subtype: 'rectificador' };
|
|
const equipmentOnly = plain(equipment);
|
|
if (/^psv$/.test(equipmentOnly)) return { family: 'valvula', subtype: 'seguridad' };
|
|
if (/^vpsv$|^vpv$/.test(equipmentOnly)) return { family: 'valvula', subtype: 'presion_vacio' };
|
|
if (/^aib$/.test(equipmentOnly)) return { family: 'sistema_extraccion', subtype: 'aib' };
|
|
if (/seccionador/.test(equipmentOnly)) return { family: 'equipo_electrico', subtype: 'seccionador' };
|
|
if (/interruptor/.test(equipmentOnly)) return { family: 'equipo_electrico', subtype: 'interruptor' };
|
|
if (/reconectador/.test(equipmentOnly)) return { family: 'equipo_electrico', subtype: 'reconectador' };
|
|
if (/^colector$/.test(equipmentOnly)) return { family: 'colector', subtype: null };
|
|
if (/punto de medicion/.test(equipmentOnly)) return { family: 'instrumentacion', subtype: 'punto_medicion' };
|
|
if (/sistema rci|^rci$/.test(equipmentOnly)) return { family: 'defensa_incendios', subtype: null };
|
|
if (/aeroenfriador/.test(equipmentOnly)) return { family: 'aeroenfriador', subtype: null };
|
|
if (/^bateria$/.test(equipmentOnly)) return { family: 'instalacion', subtype: 'bateria' };
|
|
if (/^planta$/.test(equipmentOnly)) return { family: 'instalacion', subtype: 'planta' };
|
|
if (classCode === 'bba' || /\bbomba\b/.test(source)) {
|
|
if (/\bcen\b|centrifug/.test(source)) return { family: 'bomba', subtype: 'centrifuga' };
|
|
if (/\btx\b|triplex|quintuplex|alternativa/.test(source)) return { family: 'bomba', subtype: 'alternativa' };
|
|
if (/tornillo/.test(source)) return { family: 'bomba', subtype: 'tornillo' };
|
|
if (/diafragma/.test(source)) return { family: 'bomba', subtype: 'diafragma' };
|
|
return { family: 'bomba', subtype: null };
|
|
}
|
|
if (classCode === 'tk' || /\btanque\b|\btk\b/.test(source)) return { family: 'tanque', subtype: null };
|
|
if (classCode === 'sep' || /separador/.test(source)) {
|
|
if (/sep b|bifasic/.test(source)) return { family: 'separador', subtype: 'bifasico' };
|
|
if (/sep g|gas/.test(source)) return { family: 'separador', subtype: 'gas' };
|
|
return { family: 'separador', subtype: null };
|
|
}
|
|
if (classCode === 'cald' || /\bcaldera\b/.test(source)) return { family: 'caldera', subtype: null };
|
|
if (classCode === 'cal' || /calentador|hot oil/.test(source)) return { family: 'calentador', subtype: null };
|
|
if (classCode === 'ant' || /antorcha|flare/.test(source)) return { family: 'antorcha', subtype: /frio/.test(source) ? 'venteo_frio' : null };
|
|
if (classCode === 'fil' || /\bfiltro\b/.test(source)) return { family: 'filtro', subtype: /arena/.test(source) ? 'arena' : null };
|
|
if (classCode === 'tra' || /transformador/.test(source)) return { family: 'equipo_electrico', subtype: 'transformador' };
|
|
if (classCode === 'moe' || /motor electr/.test(source)) return { family: 'motor', subtype: 'electrico' };
|
|
if (classCode === 'moex' || /motor de combustion|motor a explosion/.test(source)) return { family: 'motor', subtype: 'combustion' };
|
|
if (classCode === 'com' || /compresor|soplador/.test(source)) return { family: 'compresor', subtype: null };
|
|
if (classCode === 'va' || /valvula/.test(source)) {
|
|
if (/vpv|presion y vacio/.test(source)) return { family: 'valvula', subtype: 'presion_vacio' };
|
|
if (/\bvs\b|seguridad/.test(source)) return { family: 'valvula', subtype: 'seguridad' };
|
|
if (/\bvr\b|reguladora/.test(source)) return { family: 'valvula', subtype: 'reguladora' };
|
|
return { family: 'valvula', subtype: null };
|
|
}
|
|
if (classCode === 'caud' || /caudalimetro/.test(source)) return { family: 'instrumentacion', subtype: 'caudalimetro' };
|
|
if (classCode === 'eg' || /generador|motogenerador/.test(source)) return { family: 'generador', subtype: null };
|
|
if (classCode === 'cel' || /\bcelda\b/.test(source)) return { family: 'equipo_electrico', subtype: 'celda' };
|
|
if (classCode === 'pil' || /pileta/.test(source)) return { family: 'pileta', subtype: null };
|
|
if (classCode === 'aib' || /aparato individual de bombeo|rotaflex/.test(source)) {
|
|
const subtype = /rotaflex/.test(source) ? 'rotaflex' : /mark ii/.test(source) ? 'mark_ii' : /convencional/.test(source) ? 'convencional' : null;
|
|
return { family: 'sistema_extraccion', subtype };
|
|
}
|
|
if (classCode === 'pcp' || /\bpcp\b/.test(source)) return { family: 'sistema_extraccion', subtype: 'pcp' };
|
|
if (/\bbes\b|electrosumerg/.test(source)) return { family: 'sistema_extraccion', subtype: 'bes' };
|
|
if (/\bpozo\b/.test(source)) return { family: 'pozo', subtype: null };
|
|
if (/oleoducto/.test(source)) return { family: 'ducto', subtype: 'oleoducto' };
|
|
if (/gasoducto/.test(source)) return { family: 'ducto', subtype: 'gasoducto' };
|
|
if (/acueducto/.test(source)) return { family: 'ducto', subtype: 'acueducto' };
|
|
if (/caneria/.test(source)) return { family: 'ducto', subtype: 'caneria' };
|
|
return { family: null, subtype: null };
|
|
}
|
|
function rawObject(headers: string[], row: string[]): Record<string, string> {
|
|
const result: Record<string, string> = {};
|
|
headers.forEach((header, index) => {
|
|
if (!header.trim() && !row[index]?.trim()) return;
|
|
result[header.trim() || `Columna ${index + 1}`] = row[index]?.trim() ?? '';
|
|
});
|
|
return result;
|
|
}
|
|
|
|
function fingerprint(value: Record<string, unknown>): string {
|
|
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
}
|
|
|
|
function normalizeInventory(sheet: ParsedSheet, detection: ImportProfileDetection): ImportNormalizedRow[] {
|
|
const headers = sheet.rows[detection.headerRow - 1] ?? [];
|
|
const result: ImportNormalizedRow[] = [];
|
|
const map = detection.columnMap;
|
|
for (let index = detection.headerRow; index < sheet.rows.length; index += 1) {
|
|
const row = sheet.rows[index] ?? [];
|
|
if (!row.some((value) => value.trim())) continue;
|
|
const areaOrField = text(row, map.areaOrField);
|
|
const installation = text(row, map.installation);
|
|
const subInstallation = text(row, map.subInstallation);
|
|
const equipment = text(row, map.equipment);
|
|
const denomination = text(row, map.equipmentDenomination);
|
|
const location = text(row, map.location);
|
|
const inventoryId = text(row, map.inventoryId);
|
|
const quantityRaw = text(row, map.quantity);
|
|
const technicalSpecs = text(row, map.technicalSpecs);
|
|
const sourceStatus = text(row, map.sourceStatus);
|
|
const item = text(row, map.item);
|
|
const isHeaderRepeat = compact(areaOrField) === compact(headers[map.areaOrField ?? -1] ?? '') && compact(equipment) === compact(headers[map.equipment ?? -1] ?? '');
|
|
if (isHeaderRepeat) continue;
|
|
const issues: string[] = [];
|
|
if (!areaOrField) issues.push('MISSING_AREA_OR_YACIMIENTO');
|
|
if (!equipment && !denomination) issues.push('MISSING_EQUIPMENT_DESCRIPTION');
|
|
if (!inventoryId) issues.push('MISSING_INVENTORY_ID');
|
|
const quantity = asNumber(quantityRaw);
|
|
if (quantity !== null && (!Number.isInteger(quantity) || quantity <= 0)) issues.push('INVALID_QUANTITY');
|
|
if (quantity !== null && quantity > 1) issues.push('GROUPED_QUANTITY');
|
|
const sourceState = inventoryStatusSuggestion(sourceStatus);
|
|
if (sourceState.ambiguous && sourceStatus) issues.push('SOURCE_STATUS_REQUIRES_MAPPING');
|
|
const classType = splitClassType(denomination);
|
|
const manufacturerModel = splitManufacturerModel(technicalSpecs);
|
|
const technical = technicalNormalizationSuggestion(equipment, denomination, classType.sourceClass, classType.sourceSubtype);
|
|
const normalized: Record<string, unknown> = {
|
|
item: item || null,
|
|
areaOrField: areaOrField || null,
|
|
installation: installation || null,
|
|
subInstallation: subInstallation || null,
|
|
equipment: equipment || null,
|
|
sourceClassification: denomination || null,
|
|
location: location || null,
|
|
inventoryId: inventoryId || null,
|
|
quantity: quantity ?? (quantityRaw || null),
|
|
technicalSpecs: technicalSpecs || null,
|
|
sourceStatus: sourceStatus || null,
|
|
operationalStatusSuggestion: sourceState.operationalStatus ?? null,
|
|
conditionStatusSuggestion: sourceState.conditionStatus ?? null,
|
|
sourceClass: classType.sourceClass,
|
|
sourceSubtype: classType.sourceSubtype,
|
|
manufacturer: manufacturerModel.manufacturer,
|
|
model: manufacturerModel.model,
|
|
familySuggestion: technical.family,
|
|
normalizedFamily: technical.family,
|
|
normalizedSubtype: technical.subtype,
|
|
};
|
|
const conflict = issues.includes('MISSING_EQUIPMENT_DESCRIPTION') || issues.includes('INVALID_QUANTITY');
|
|
const status: AssetImportRowStatus = conflict ? 'CONFLICT' : issues.length ? 'WARNING' : 'READY';
|
|
result.push({
|
|
profileCode: 'MENDOZA_INVENTORY_V1',
|
|
rowNumber: index + 1,
|
|
sheetName: sheet.name,
|
|
raw: rawObject(headers, row),
|
|
normalized,
|
|
status,
|
|
suggestedAction: conflict ? 'REVIEW' : 'CREATE',
|
|
issues,
|
|
fingerprint: fingerprint(normalized),
|
|
});
|
|
}
|
|
return applyBatchDuplicateRules(result);
|
|
}
|
|
|
|
function applyBatchDuplicateRules(rows: ImportNormalizedRow[]): ImportNormalizedRow[] {
|
|
const groups = new Map<string, ImportNormalizedRow[]>();
|
|
for (const row of rows) {
|
|
const id = String(row.normalized.inventoryId ?? '').trim().toLowerCase();
|
|
if (!id) continue;
|
|
const area = plain(String(row.normalized.areaOrField ?? ''));
|
|
const key = `${area}|${id}`;
|
|
const current = groups.get(key) ?? [];
|
|
current.push(row);
|
|
groups.set(key, current);
|
|
}
|
|
for (const group of groups.values()) {
|
|
if (group.length < 2) continue;
|
|
const locations = new Set(group.map((row) => plain(`${row.normalized.installation ?? ''}|${row.normalized.subInstallation ?? ''}|${row.normalized.location ?? ''}`)));
|
|
const issue = locations.size > 1 ? 'INVENTORY_ID_MULTIPLE_LOCATIONS' : 'DUPLICATE_INVENTORY_ID_IN_BATCH';
|
|
for (const row of group) {
|
|
if (!row.issues.includes(issue)) row.issues.push(issue);
|
|
if (issue === 'INVENTORY_ID_MULTIPLE_LOCATIONS') {
|
|
row.status = 'CONFLICT';
|
|
row.suggestedAction = 'REVIEW';
|
|
} else if (row.status === 'READY') {
|
|
row.status = 'WARNING';
|
|
}
|
|
}
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
function normalizeFields(sheet: ParsedSheet, detection: ImportProfileDetection): ImportNormalizedRow[] {
|
|
const headers = sheet.rows[detection.headerRow - 1] ?? [];
|
|
const result: ImportNormalizedRow[] = [];
|
|
const map = detection.columnMap;
|
|
for (let index = detection.headerRow; index < sheet.rows.length; index += 1) {
|
|
const row = sheet.rows[index] ?? [];
|
|
if (!row.some((value) => value.trim())) continue;
|
|
const field = text(row, map.field);
|
|
const area = text(row, map.area);
|
|
const department = text(row, map.department);
|
|
const rightType = text(row, map.rightType);
|
|
const operator = text(row, map.operator);
|
|
const issues: string[] = [];
|
|
if (!field) issues.push('MISSING_FIELD');
|
|
if (!area) issues.push('MISSING_AREA');
|
|
if (!operator) issues.push('MISSING_OPERATOR');
|
|
const normalized: Record<string, unknown> = {
|
|
field: field || null,
|
|
area: area || null,
|
|
department: department || null,
|
|
rightType: rightType || null,
|
|
operator: operator || null,
|
|
};
|
|
const conflict = !field || !area;
|
|
result.push({
|
|
profileCode: 'MENDOZA_YACIMIENTOS_V1',
|
|
rowNumber: index + 1,
|
|
sheetName: sheet.name,
|
|
raw: rawObject(headers, row),
|
|
normalized,
|
|
status: conflict ? 'CONFLICT' : issues.length ? 'WARNING' : 'READY',
|
|
suggestedAction: conflict ? 'REVIEW' : 'CREATE',
|
|
issues,
|
|
fingerprint: fingerprint(normalized),
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function summarize(rows: ImportNormalizedRow[]): ImportAnalysis['summary'] {
|
|
const issueCounts: Record<string, number> = {};
|
|
rows.forEach((row) => row.issues.forEach((issue) => { issueCounts[issue] = (issueCounts[issue] ?? 0) + 1; }));
|
|
return {
|
|
totalRows: rows.length,
|
|
readyRows: rows.filter((row) => row.status === 'READY').length,
|
|
warningRows: rows.filter((row) => row.status === 'WARNING').length,
|
|
conflictRows: rows.filter((row) => row.status === 'CONFLICT').length,
|
|
ignoredRows: rows.filter((row) => row.status === 'IGNORED').length,
|
|
issueCounts,
|
|
};
|
|
}
|
|
|
|
export function analyzeImportWorkbook(workbook: ParsedWorkbook, forcedProfile?: AssetImportProfileCode): ImportAnalysis {
|
|
const detected = detectImportProfile(workbook);
|
|
const profileCode = forcedProfile && forcedProfile !== 'UNKNOWN' ? forcedProfile : detected.profileCode;
|
|
if (profileCode === 'UNKNOWN') {
|
|
return { detection: detected, rows: [], summary: { totalRows: 0, readyRows: 0, warningRows: 0, conflictRows: 0, ignoredRows: 0, issueCounts: { PROFILE_NOT_RECOGNIZED: 1 } } };
|
|
}
|
|
const aliases = profileCode === 'MENDOZA_YACIMIENTOS_V1' ? fieldAliases : inventoryAliases;
|
|
let selected: { sheet: ParsedSheet; header: ReturnType<typeof bestHeader> } | null = null;
|
|
for (const candidate of workbook.sheets) {
|
|
const header = bestHeader(candidate, aliases);
|
|
if (!selected || header.score > selected.header.score) selected = { sheet: candidate, header };
|
|
}
|
|
if (!selected) {
|
|
return { detection: detected, rows: [], summary: { totalRows: 0, readyRows: 0, warningRows: 0, conflictRows: 0, ignoredRows: 0, issueCounts: { PROFILE_NOT_RECOGNIZED: 1 } } };
|
|
}
|
|
const denominator = profileCode === 'MENDOZA_YACIMIENTOS_V1' ? 55 : 95;
|
|
const confidence = Math.min(100, Math.max(0, Math.round((selected.header.score / denominator) * 100)));
|
|
const detection: ImportProfileDetection = { profileCode, confidence, sheetName: selected.sheet.name, headerRow: selected.header.row, columnMap: selected.header.map, detectedHeaders: selected.header.headers };
|
|
const rows = profileCode === 'MENDOZA_YACIMIENTOS_V1' ? normalizeFields(selected.sheet, detection) : normalizeInventory(selected.sheet, detection);
|
|
return { detection, rows, summary: summarize(rows) };
|
|
}
|
|
|
|
export function profileLabel(code: AssetImportProfileCode): string {
|
|
if (code === 'MENDOZA_INVENTORY_V1') return 'Inventario de instalaciones · Mendoza';
|
|
if (code === 'MENDOZA_YACIMIENTOS_V1') return 'Tabla Área / Yacimiento · Mendoza';
|
|
return 'Formato no reconocido';
|
|
}
|