import { SearchableSelect } from '../components/SearchableSelect'; import { useEffect, useMemo, useState } from 'react'; import type { FormEvent } from 'react'; import { useAuth } from '../auth/AuthContext'; import { Alert, EmptyState, LoadingBlock, errorMessage } from '../components/Feedback'; import { Icon } from '../components/Icon'; import { AssetImportReviewsPanel } from '../features/assets/AssetImportReviewsPanel'; import { applyAssetImportPlan, applySafeAssetImportPlan, cancelAssetImport, generateAssetImportPlan, getAssetImportBatch, getAssetImportPlan, listAssetImportBatches, listAssetImportReviews, listAssetImportOrganizations, listAssetImportPlanItems, listAssetImportRows, listAssets, reconcileAssetImport, resolveAssetImportPlanItem, rollbackAssetImportPlan, uploadAssetImport, } from '../lib/api'; import type { AssetImportBatch, AssetImportBatchDetail, AssetImportOrganizationOption, AssetImportPlan, AssetImportPlanAction, AssetImportPlanEntityKind, AssetImportPlanItem, AssetImportProfileCode, AssetImportRow, AssetImportRowStatus, AssetImportReviewItem, AssetListItem, } from '../lib/api'; const ISSUE_LABELS: Record = { MISSING_AREA_OR_YACIMIENTO: 'Falta Área/Yacimiento', MISSING_EQUIPMENT_DESCRIPTION: 'Falta descripción del equipo', MISSING_INVENTORY_ID: 'Sin ID de inventario', INVALID_QUANTITY: 'Cantidad inválida', GROUPED_QUANTITY: 'Registro agrupado: requiere decisión humana', SOURCE_STATUS_REQUIRES_MAPPING: 'Estado de fuente ambiguo', INVENTORY_ID_MULTIPLE_LOCATIONS: 'ID repetido en ubicaciones diferentes', DUPLICATE_INVENTORY_ID_IN_BATCH: 'ID repetido en el archivo', MISSING_FIELD: 'Falta Yacimiento', MISSING_AREA: 'Falta Área', MISSING_OPERATOR: 'Falta Operadora', PROFILE_NOT_RECOGNIZED: 'Formato no reconocido', MATCH_MULTIPLE_ASSETS: 'Varias coincidencias exactas en los inventarios', MATCH_MULTIPLE_ORGANIZATIONS: 'Varias organizaciones coinciden con la operadora', SOURCE_ROW_WARNING: 'La fila de origen tiene una advertencia', SOURCE_ROW_CONFLICT: 'La fila de origen tiene un conflicto', PLAN_MULTIPLE_ORGANIZATIONS: 'Varias organizaciones posibles', PLAN_MULTIPLE_AREAS: 'Varias áreas posibles', PLAN_MULTIPLE_DEPARTMENTS: 'Varios Departamentos coinciden', PLAN_DEPARTMENT_REVIEW_REQUIRED: 'El Departamento requiere revisión', PLAN_LEGAL_RIGHT_TYPE_MISSING: 'Falta tipo de derecho/concesión', PLAN_LEGAL_RIGHT_TYPE_UNRECOGNIZED: 'Tipo de derecho no reconocido', PLAN_MULTIPLE_LEGAL_RIGHTS: 'Varios derechos vigentes coinciden', PLAN_LEGAL_RIGHT_REVIEW_REQUIRED: 'El derecho/concesión requiere revisión', PLAN_MULTIPLE_FIELDS: 'Varios yacimientos posibles', PLAN_MULTIPLE_CONTAINERS: 'Varias instalaciones posibles', PLAN_MULTIPLE_LOCAL_STRUCTURES: 'Varias estructuras locales posibles', PLAN_MULTIPLE_NAMESPACE_MATCHES: 'El ID externo coincide con varios registros', PLAN_GLOBAL_MATCH_REQUIRES_NAMESPACE: 'Coincidencia previa sin namespace confirmado', PLAN_AREA_REVIEW_REQUIRED: 'El Área requiere revisión', PLAN_ORGANIZATION_REVIEW_REQUIRED: 'La Operadora requiere revisión', PLAN_FIELD_OPERATOR_AMBIGUOUS: 'La operadora del Yacimiento es ambigua', PLAN_FIELD_OPERATOR_MISSING: 'No hay una operadora identificable para el Yacimiento', PLAN_FIELD_OPERATOR_CONTEXT_MISMATCH: 'El Yacimiento existente tiene otra asignación operativa; corregir el inventario o la fuente', PLAN_MATCH_OPERATIONAL_CONTEXT_MISMATCH: 'El registro identificado existe pero pertenece a otra Área/Operadora', PLAN_TERRITORY_AMBIGUOUS: 'Área/Yacimiento ambiguo para este inventario', PLAN_TERRITORY_CONTEXT_REQUIRED: 'Primero hay que elegir Área/Yacimiento', PLAN_TERRITORY_OVERRIDE_STALE: 'La elección territorial guardada ya no es válida', PLAN_CONTEXT_DECISION_REQUIRED: 'Depende de elegir el contexto territorial', PLAN_TERRITORY_NOT_FOUND: 'Área/Yacimiento no existe en los inventarios para esta operadora', PLAN_CONTAINER_REVIEW_REQUIRED: 'La instalación padre requiere revisión', PLAN_CONTAINER_NOT_IDENTIFIED: 'No se identificó una instalación física segura', PLAN_LOCAL_STRUCTURE_NOT_IDENTIFIED: 'No se pudo formar una estructura local desde la fuente', PLAN_PARENT_NOT_RESOLVED: 'No se pudo resolver el padre físico', PLAN_PROFILE_UNSUPPORTED: 'Perfil todavía no soportado para aplicación', PLAN_SOURCE_GROUP_REQUIRES_DECISION: 'La fuente sólo informa una categoría/sector; confirmar contenedor', PLAN_INVENTORY_ID_TOO_LONG: 'ID de inventario supera el máximo permitido', }; const ENTITY_LABELS: Record = { DEPARTMENT: 'Departamentos', ORGANIZATION: 'Organizaciones', AREA: 'Áreas', AREA_DEPARTMENT_RELATION: 'Relaciones Área ↔ Departamento', FIELD: 'Yacimientos', OPERATOR_RELATION: 'Relaciones Área ↔ Operadora', LEGAL_RIGHT: 'Derechos / concesiones', LEGAL_RIGHT_ORGANIZATION: 'Derecho ↔ Organización', INSTALLATION: 'Instalaciones', LOCAL_STRUCTURE: 'Estructura local según fuente', TECHNICAL_ASSET: 'Elementos técnicos', }; const PLAN_GROUPS: Array<{ label: string; kinds: AssetImportPlanEntityKind[] }> = [ { label: 'Territorio', kinds: ['DEPARTMENT','AREA','AREA_DEPARTMENT_RELATION','FIELD'] }, { label: 'Organizaciones', kinds: ['ORGANIZATION','OPERATOR_RELATION'] }, { label: 'Marco jurídico', kinds: ['LEGAL_RIGHT','LEGAL_RIGHT_ORGANIZATION'] }, { label: 'Inventario técnico', kinds: ['LOCAL_STRUCTURE','INSTALLATION','TECHNICAL_ASSET'] }, ]; const PLAN_ACTION_LABELS: Record = { CREATE: 'Crear nuevo', MATCH: 'Usar existente', REVIEW: 'Requiere revisión', IGNORE: 'Ignorar', }; const MATCH_CRITERIA: Record = { DEPARTMENT: 'Mismo Departamento normalizado dentro de Mendoza.', ORGANIZATION: 'Mismo nombre o razón social normalizada.', AREA: 'Mismo nombre de Área en los inventarios.', AREA_DEPARTMENT_RELATION: 'La relación Área ↔ Departamento ya está vigente.', FIELD: 'Mismo Yacimiento dentro de la misma Área y contexto operativo.', OPERATOR_RELATION: 'La relación Área ↔ Operadora ya está vigente.', LEGAL_RIGHT: 'Mismo derecho/concesión vigente para el Área.', LEGAL_RIGHT_ORGANIZATION: 'La relación Derecho ↔ Organización ya está vigente.', INSTALLATION: 'Mismo tipo y nombre bajo el mismo padre físico.', LOCAL_STRUCTURE: 'Misma nomenclatura local bajo el mismo Área/Yacimiento y la misma operadora.', TECHNICAL_ASSET: 'ID externo exacto dentro del namespace seleccionado y mismo contexto operativo.', }; type ReviewGuidance = { title: string; detail: string; decision: string; dependency?: boolean }; const REVIEW_GUIDANCE: Record = { MISSING_AREA_OR_YACIMIENTO:{title:'Falta ubicación territorial',detail:'La fila no informa Área o Yacimiento suficiente para ubicar el registro.',decision:'Completá la ubicación en la fuente o vinculala con el contexto correcto.'}, MISSING_EQUIPMENT_DESCRIPTION:{title:'Falta identificar el equipo',detail:'No hay una descripción suficiente para saber qué registro representa la fila.',decision:'Completá la descripción antes de crear un registro.'}, MISSING_INVENTORY_ID:{title:'Falta ID de inventario',detail:'La fuente no aporta un identificador estable para este equipo.',decision:'Confirmá si debe crearse sin ID externo o corregí la fuente.'}, INVALID_QUANTITY:{title:'Cantidad inválida',detail:'La cantidad informada no puede interpretarse con seguridad.',decision:'Corregí o confirmá la cantidad antes de importar.'}, GROUPED_QUANTITY:{title:'La fila agrupa varias unidades',detail:'La fuente representa más de una unidad en una sola fila y el sistema no debe separarlas automáticamente.',decision:'Confirmá si se conserva como registro agrupado o si la fuente debe dividirse en registros individuales.'}, SOURCE_STATUS_REQUIRES_MAPPING:{title:'Estado de fuente ambiguo',detail:'El texto de estado no permite decidir automáticamente entre estado operativo y condición.',decision:'Revisá qué significa el estado antes de importar.'}, INVENTORY_ID_MULTIPLE_LOCATIONS:{title:'El mismo ID aparece en ubicaciones distintas',detail:'Un identificador de inventario se repite con contextos territoriales diferentes.',decision:'Verificá si es el mismo registro trasladado o un error de identificación.'}, DUPLICATE_INVENTORY_ID_IN_BATCH:{title:'ID repetido dentro del archivo',detail:'El mismo ID de inventario aparece más de una vez en este lote.',decision:'Confirmá si las filas describen el mismo registro o corregí el identificador.'}, MISSING_FIELD:{title:'Falta Yacimiento',detail:'No se pudo identificar el Yacimiento de la fila.',decision:'Completá o confirmá el Yacimiento correcto.'}, MISSING_AREA:{title:'Falta Área',detail:'No se pudo identificar el Área operativa de la fila.',decision:'Completá o confirmá el Área correcta.'}, MISSING_OPERATOR:{title:'Falta Operadora',detail:'La fuente no permite determinar una organización operadora.',decision:'Confirmá la operadora o mantené explícitamente el registro sin operadora cuando corresponda.'}, PROFILE_NOT_RECOGNIZED:{title:'Formato no reconocido',detail:'El archivo no coincide con un perfil de importación conocido.',decision:'Usá un perfil soportado o ajustá el archivo.'}, MATCH_MULTIPLE_ASSETS:{title:'Hay varios registros posibles',detail:'Más de un registro cumple los criterios exactos disponibles.',decision:'Elegí manualmente cuál registro del inventario corresponde.'}, MATCH_MULTIPLE_ORGANIZATIONS:{title:'Hay varias organizaciones posibles',detail:'La operadora informada coincide con más de una organización.',decision:'Seleccioná la organización legal correcta.'}, SOURCE_ROW_WARNING:{title:'Advertencia en la fila de origen',detail:'La fila contiene información válida pero con una condición que requiere control humano.',decision:'Revisá las observaciones de origen y confirmá la decisión.'}, SOURCE_ROW_CONFLICT:{title:'Conflicto en la fila de origen',detail:'La fila contiene datos que se contradicen o no pueden resolverse automáticamente.',decision:'Corregí la fuente o resolvé manualmente identidad y contexto.'}, PLAN_MULTIPLE_ORGANIZATIONS:{title:'Varias organizaciones coinciden',detail:'No existe una única organización segura para reutilizar.',decision:'Elegí la organización correcta o corregí el inventario.'}, PLAN_MULTIPLE_AREAS:{title:'Varias Áreas coinciden',detail:'El nombre normalizado no identifica una sola Área.',decision:'Elegí el Área correcta o corregí la duplicidad en el inventario.'}, PLAN_MULTIPLE_DEPARTMENTS:{title:'Varios Departamentos coinciden',detail:'El Departamento no queda identificado de manera única.',decision:'Elegí o corregí el Departamento correspondiente.'}, PLAN_DEPARTMENT_REVIEW_REQUIRED:{title:'Depende de un Departamento pendiente',detail:'Esta relación no puede resolverse hasta decidir el Departamento relacionado.',decision:'Resolvé primero el Departamento y luego regenerá el plan.',dependency:true}, PLAN_LEGAL_RIGHT_TYPE_MISSING:{title:'Falta tipo de derecho',detail:'La fuente no informa el tipo de derecho/concesión necesario.',decision:'Completá o confirmá el tipo jurídico.'}, PLAN_LEGAL_RIGHT_TYPE_UNRECOGNIZED:{title:'Tipo de derecho no reconocido',detail:'El valor de fuente no puede mapearse con seguridad al catálogo jurídico.',decision:'Elegí el tipo correcto o corregí la fuente.'}, PLAN_MULTIPLE_LEGAL_RIGHTS:{title:'Varios derechos vigentes coinciden',detail:'Hay más de un derecho/concesión posible para el mismo contexto.',decision:'Seleccioná el derecho correcto.'}, PLAN_LEGAL_RIGHT_REVIEW_REQUIRED:{title:'Depende de un derecho pendiente',detail:'Este elemento depende de un derecho/concesión todavía no resuelto.',decision:'Resolvé primero el derecho y regenerá el plan.',dependency:true}, PLAN_MULTIPLE_FIELDS:{title:'Varios Yacimientos posibles',detail:'El nombre y contexto no identifican un único Yacimiento.',decision:'Elegí el Yacimiento correcto o corregí duplicidades.'}, PLAN_MULTIPLE_CONTAINERS:{title:'Varias instalaciones posibles',detail:'Más de una instalación física coincide bajo el mismo contexto.',decision:'Elegí la instalación correcta.'}, PLAN_MULTIPLE_LOCAL_STRUCTURES:{title:'Varias estructuras locales posibles',detail:'La misma nomenclatura local aparece más de una vez dentro del mismo contexto.',decision:'Revisá si son duplicados o estructuras distintas antes de continuar.'}, PLAN_MULTIPLE_NAMESPACE_MATCHES:{title:'El ID externo apunta a varios registros',detail:'Dentro del namespace seleccionado el mismo ID externo está asociado a más de un registro.',decision:'Corregí la duplicidad del identificador.'}, PLAN_GLOBAL_MATCH_REQUIRES_NAMESPACE:{title:'Coincidencia previa sin namespace seguro',detail:'Existe una coincidencia histórica, pero no está respaldada por el namespace externo seleccionado.',decision:'Verificá manualmente el registro o completá el identificador externo.'}, PLAN_AREA_REVIEW_REQUIRED:{title:'Depende de un Área pendiente',detail:'Este elemento no puede resolverse hasta definir el Área relacionada.',decision:'Resolvé primero el Área y regenerá el plan.',dependency:true}, PLAN_ORGANIZATION_REVIEW_REQUIRED:{title:'Depende de una organización pendiente',detail:'Este elemento necesita una organización que todavía no está resuelta.',decision:'Resolvé primero la organización y regenerá el plan.',dependency:true}, PLAN_FIELD_OPERATOR_AMBIGUOUS:{title:'Operadora del Yacimiento ambigua',detail:'La fuente permite más de una interpretación para la operadora del Yacimiento.',decision:'Confirmá la organización operadora correcta.'}, PLAN_FIELD_OPERATOR_MISSING:{title:'Yacimiento sin operadora identificable',detail:'No se pudo obtener una operadora segura para el Yacimiento.',decision:'Confirmá si realmente está sin operadora o completá la organización.'}, PLAN_FIELD_OPERATOR_CONTEXT_MISMATCH:{title:'Yacimiento con otro contexto operativo',detail:'El Yacimiento existente está asociado a una operadora diferente de la fuente.',decision:'Corregí el inventario o la fuente; no fuerces una coincidencia incorrecta.'}, PLAN_MATCH_OPERATIONAL_CONTEXT_MISMATCH:{title:'El registro existe en otro contexto',detail:'El identificador coincide, pero Área u Operadora no coinciden con la fuente.',decision:'Verificá traslado, error de fuente o contexto del inventario.'}, PLAN_TERRITORY_AMBIGUOUS:{title:'Elegir Área/Yacimiento',detail:'El mismo nombre existe en más de un contexto territorial válido. El sistema no va a adivinar cuál corresponde.',decision:'Elegí una de las alternativas detectadas. La decisión se guardará para este lote y luego se regenera el plan.'}, PLAN_TERRITORY_CONTEXT_REQUIRED:{title:'Depende del contexto territorial',detail:'No se analiza todavía la estructura local porque primero hay que decidir a qué Área/Yacimiento pertenece la fila.',decision:'Resolvé la tarjeta “Elegir Área/Yacimiento” y regenerá el plan.',dependency:true}, PLAN_CONTEXT_DECISION_REQUIRED:{title:'Esperando Área/Yacimiento',detail:'La instalación y el padre no son conflictos separados: se recalcularán cuando se elija el contexto territorial.',decision:'No resuelvas este registro individualmente; resolvé primero el contexto y regenerá.',dependency:true}, PLAN_TERRITORY_OVERRIDE_STALE:{title:'La elección territorial quedó obsoleta',detail:'El inventario cambió y el contexto elegido anteriormente ya no está entre las alternativas válidas.',decision:'Elegí nuevamente el Área/Yacimiento correcto.'}, PLAN_TERRITORY_NOT_FOUND:{title:'Área/Yacimiento no encontrado para esta operadora',detail:'La ubicación de la fila no existe dentro de las Áreas operadas por la organización seleccionada.',decision:'Importá/configurá primero el territorio correcto o corregí la fuente.'}, PLAN_CONTAINER_REVIEW_REQUIRED:{title:'Depende de una instalación pendiente',detail:'El registro necesita una instalación padre que todavía requiere decisión.',decision:'Resolvé primero la instalación y regenerá el plan.',dependency:true}, PLAN_CONTAINER_NOT_IDENTIFIED:{title:'No se identificó instalación física',detail:'La fila no aporta una instalación concreta y el equipo necesita un padre físico.',decision:'Identificá la instalación real o corregí la estructura de la fuente.'}, PLAN_LOCAL_STRUCTURE_NOT_IDENTIFIED:{title:'Falta estructura local utilizable',detail:'La fuente no aporta un nivel local suficiente para ubicar el registro debajo del Área/Yacimiento.',decision:'Revisá instalación, subinstalación o ubicación y confirmá el nivel estructural correcto.'}, PLAN_PARENT_NOT_RESOLVED:{title:'Padre físico sin resolver',detail:'El sistema no puede incorporar el registro sin un padre físico seguro.',decision:'Resolvé Área/Yacimiento/instalación padre y luego regenerá el plan.',dependency:true}, PLAN_PROFILE_UNSUPPORTED:{title:'Perfil no soportado para incorporación',detail:'El análisis puede leerse, pero este perfil todavía no tiene una incorporación segura al inventario.',decision:'No apliques el lote hasta contar con soporte específico.'}, PLAN_SOURCE_GROUP_REQUIRES_DECISION:{title:'Nomenclatura local de la fuente',detail:'Este plan utiliza un criterio anterior para clasificar la estructura local.',decision:'Regenerá el plan con la versión actual antes de resolver este caso.'}, PLAN_INVENTORY_ID_TOO_LONG:{title:'ID de inventario demasiado largo',detail:'El identificador supera el máximo permitido para persistirlo de forma segura.',decision:'Corregí o normalizá el ID en la fuente.'}, }; function reviewGuidance(code:string):ReviewGuidance{return REVIEW_GUIDANCE[code]??{title:ISSUE_LABELS[code]??code,detail:'El sistema no puede tomar esta decisión automáticamente.',decision:'Revisá el registro y resolvelo manualmente antes de aplicar.'};} function isDependencyReview(item:AssetImportPlanItem){return item.reviewCodes.length>0&&item.reviewCodes.every((code)=>reviewGuidance(code).dependency===true);} function provisionalDisplayName(name:string){return name.replace(/^\[A VALIDAR\]\s*/i,'').trim()||name;} function isProvisionalName(name:string){return /^\[A VALIDAR\]/i.test(name.trim());} function matchedObjectId(item:AssetImportPlanItem){if(item.matchedAssetId)return item.matchedAssetId;for(const key of ['matchedDepartmentId','matchedRelationId','matchedLegalRightId']){const current=item.payload[key];if(typeof current==='string'&¤t)return current;}return null;} function reviewFacts(item:AssetImportPlanItem){const p=item.payload;const entries:Array<[string,unknown]>=[['Área / Yacimiento',p.areaOrField],['Instalación',p.installation??p.sourceInstallation],['Sector / subinstalación',p.subInstallation??p.sourceSubInstallation],['Equipo',p.equipment],['Familia normalizada',p.normalizedFamily],['Subtipo',p.normalizedSubtype],['ID inventario',p.inventoryId],['Cantidad',p.quantity],['Estado fuente',p.sourceStatus]];return entries.filter(([,v])=>v!=null&&v!=='').slice(0,7).map(([label,v])=>[label,String(v)] as const);} type InventoryContextCandidate = { id:string; code:string; name:string; typeCode:string; parentName?:string|null }; function inventoryContextCandidates(item:AssetImportPlanItem):InventoryContextCandidate[]{ if(item.payload.inventoryContextDecision!==true||!Array.isArray(item.payload.candidateOptions))return []; return item.payload.candidateOptions.flatMap((value)=>{ const current=record(value);if(!current)return []; const id=String(current.id??'');const code=String(current.code??'');const name=String(current.name??'');const typeCode=String(current.typeCode??''); if(!id||!name||!typeCode)return []; return [{id,code,name,typeCode,parentName:current.parentName?String(current.parentName):null}]; }); } function profileLabel(code: AssetImportProfileCode) { if (code === 'MENDOZA_INVENTORY_V1') return 'Inventario de instalaciones · Mendoza'; if (code === 'MENDOZA_YACIMIENTOS_V1') return 'Tabla Área / Yacimiento · Mendoza'; return 'Formato no reconocido'; } function record(value: unknown): Record | null { return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : null; } function executionStatus(batch: AssetImportBatch) { return record(batch.analysis.importExecution)?.status; } function batchStatus(batch: AssetImportBatch) { const execution = executionStatus(batch); if (execution === 'APPLIED') return { label: 'Importado', className: 'active' }; if (execution === 'PARTIAL') return { label: 'Importado parcialmente', className: 'pending' }; if (execution === 'ROLLED_BACK') return { label: 'Revertido', className: 'pending' }; if (batch.status === 'CANCELLED') return { label: 'Cancelado', className: 'inactive' }; if (batch.status === 'REVIEW_REQUIRED') return { label: 'Requiere revisión', className: 'observed' }; if (batch.status === 'FAILED') return { label: 'Fallido', className: 'inactive' }; return { label: 'Analizado', className: 'active' }; } function rowStatus(status: AssetImportRowStatus) { if (status === 'READY') return { label: 'Listo', className: 'active' }; if (status === 'WARNING') return { label: 'Advertencia', className: 'pending' }; if (status === 'CONFLICT') return { label: 'Conflicto', className: 'observed' }; return { label: 'Ignorado', className: 'inactive' }; } function planState(plan: AssetImportPlan) { if (plan.status === 'READY') return { label: 'Listo para confirmar', className: 'active' }; if (plan.status === 'REVIEW_REQUIRED' && summaryNumber(plan, 'appliedCreateItems') > 0) return { label: 'Importado parcialmente', className: 'pending' }; if (plan.status === 'REVIEW_REQUIRED') return { label: 'Decisiones pendientes', className: 'observed' }; if (plan.status === 'APPLIED') return { label: 'Aplicado', className: 'active' }; if (plan.status === 'ROLLED_BACK') return { label: 'Revertido', className: 'pending' }; if (plan.status === 'FAILED') return { label: 'Fallido', className: 'inactive' }; return { label: plan.status, className: 'inactive' }; } function value(source: Record, key: string) { const current = source[key]; return current == null || current === '' ? '—' : String(current); } function formatBytes(bytes: number) { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / 1024 / 1024).toFixed(1)} MB`; } function summaryNumber(plan: AssetImportPlan, key: string) { return Number(plan.summary[key] ?? 0); } function directReviewCount(plan: AssetImportPlan) { const value = plan.summary.directReviewItems; return value == null ? plan.reviewItems.filter((item) => !isDependencyReview(item)).length : Number(value); } function dependencyReviewCount(plan: AssetImportPlan) { const value = plan.summary.dependencyReviewItems; return value == null ? plan.reviewItems.filter((item) => isDependencyReview(item)).length : Number(value); } function sourceRowsLabel(rows: number[]) { if (!rows.length) return 'Contexto del inventario'; if (rows.length <= 4) return `Fila${rows.length === 1 ? '' : 's'} ${rows.join(', ')}`; return `${rows.length.toLocaleString('es-AR')} filas · ${rows.slice(0, 3).join(', ')}…`; } export function AssetImportsPage() { const { hasPermission } = useAuth(); const canManage = hasPermission('asset_imports.manage'); const canApply = hasPermission('asset_imports.apply'); const [batches, setBatches] = useState([]); const [selectedId, setSelectedId] = useState(null); const [detail, setDetail] = useState(null); const [plan, setPlan] = useState(null); const [organizations, setOrganizations] = useState([]); const [operatorAssetId, setOperatorAssetId] = useState(''); const [externalNamespace, setExternalNamespace] = useState(''); const [rows, setRows] = useState([]); const [rowMeta, setRowMeta] = useState({ page: 1, pageSize: 50, total: 0, totalPages: 0 }); const [rowStatusFilter, setRowStatusFilter] = useState(''); const [rowSearch, setRowSearch] = useState(''); const [file, setFile] = useState(null); const [sourceLabel, setSourceLabel] = useState(''); const [notes, setNotes] = useState(''); const [profileCode, setProfileCode] = useState<'' | Exclude>(''); const [loading, setLoading] = useState(true); const [detailLoading, setDetailLoading] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const [matchItem, setMatchItem] = useState(null); const [matchSearch, setMatchSearch] = useState(''); const [matchResults, setMatchResults] = useState([]); const [matchLoading, setMatchLoading] = useState(false); const [planDetailKind, setPlanDetailKind] = useState(null); const [planDetailAction, setPlanDetailAction] = useState(null); const [planDetailItems, setPlanDetailItems] = useState([]); const [planDetailMeta, setPlanDetailMeta] = useState({ page: 1, pageSize: 50, total: 0, totalPages: 0 }); const [planDetailLoading, setPlanDetailLoading] = useState(false); const [section, setSection] = useState<'BATCHES' | 'REVIEWS'>('BATCHES'); const [reviews, setReviews] = useState([]); const [reviewKind, setReviewKind] = useState<'ALL' | 'DIRECT' | 'DEPENDENCY'>('ALL'); const [reviewMeta, setReviewMeta] = useState({ page: 1, pageSize: 30, total: 0, totalPages: 0 }); const [reviewsLoading, setReviewsLoading] = useState(false); const [scrollToReviews, setScrollToReviews] = useState(false); const loadPlanDetail = async (kind: AssetImportPlanEntityKind | null, page = 1, action: AssetImportPlanAction | null = null) => { if (!detail) return; setPlanDetailKind(kind); setPlanDetailAction(action); setPlanDetailLoading(true); try { const response = await listAssetImportPlanItems(detail.id, { entityKind: kind ?? undefined, action: action ?? undefined, page, pageSize: 50 }); setPlanDetailItems(response.data); setPlanDetailMeta(response.meta); } catch (current) { setError(errorMessage(current)); } finally { setPlanDetailLoading(false); } }; const loadReviews = async (page = 1, kind = reviewKind) => { setReviewsLoading(true); try { const response = await listAssetImportReviews({ page, pageSize: 30, kind }); setReviews(response.data); setReviewMeta(response.meta); } catch (current) { setError(errorMessage(current)); } finally { setReviewsLoading(false); } }; const loadBatches = async (preferId?: string) => { const response = await listAssetImportBatches({ pageSize: 100 }); setBatches(response.data); const next = preferId ?? selectedId ?? response.data[0]?.id ?? null; setSelectedId(next); }; const loadOrganizations = async () => { const response = await listAssetImportOrganizations(); setOrganizations(response.data); }; const loadDetail = async (id: string, page = 1) => { setDetailLoading(true); setError(''); try { const [batch, rowResponse, currentPlan] = await Promise.all([ getAssetImportBatch(id), listAssetImportRows(id, { page, pageSize: 50, status: rowStatusFilter, search: rowSearch.trim() || undefined }), getAssetImportPlan(id), ]); setDetail(batch); setRows(rowResponse.data); setRowMeta(rowResponse.meta); setPlan(currentPlan); setPlanDetailKind(null); setPlanDetailAction(null); setPlanDetailItems([]); if (currentPlan?.operatorAssetId) setOperatorAssetId(currentPlan.operatorAssetId); if (currentPlan?.externalIdNamespace) setExternalNamespace(currentPlan.externalIdNamespace); if (batch.profileCode === 'MENDOZA_INVENTORY_V1') await loadOrganizations(); } catch (requestError) { setError(errorMessage(requestError)); } finally { setDetailLoading(false); } }; useEffect(() => { Promise.all([loadBatches(), loadReviews(1, 'ALL')]).catch((requestError) => setError(errorMessage(requestError))).finally(() => setLoading(false)); }, []); useEffect(() => { setOperatorAssetId(''); setExternalNamespace(''); setMatchItem(null); setMatchResults([]); setPlanDetailAction(null); if (selectedId) void loadDetail(selectedId, 1); else { setDetail(null); setPlan(null); setRows([]); } }, [selectedId, rowStatusFilter]); useEffect(() => { if (!scrollToReviews || section !== 'BATCHES' || !detail || detail.id !== selectedId || plan?.status !== 'REVIEW_REQUIRED') return; const target = document.getElementById('import-review-panel'); if (!target) return; target.scrollIntoView({ behavior: 'smooth', block: 'start' }); setScrollToReviews(false); }, [scrollToReviews, section, detail, selectedId, plan]); const analyze = async (event: FormEvent) => { event.preventDefault(); if (!file) { setError('Seleccioná un archivo XLSX o CSV'); return; } setSaving(true); setError(''); setSuccess(''); try { const created = await uploadAssetImport({ file, sourceLabel, notes, profileCode: profileCode || undefined }); setFile(null); setSourceLabel(''); setNotes(''); setProfileCode(''); await loadBatches(created.id); setSelectedId(created.id); setSuccess(`Archivo analizado: ${created.totalRows.toLocaleString('es-AR')} filas. Los inventarios no fueron modificados.`); } catch (requestError) { setError(errorMessage(requestError)); } finally { setSaving(false); } }; const reconcile = async () => { if (!detail || detail.status === 'CANCELLED' || plan?.status === 'APPLIED' || executionStatus(detail) === 'PARTIAL') return; setSaving(true); setError(''); setSuccess(''); try { await reconcileAssetImport(detail.id); await loadBatches(detail.id); await loadDetail(detail.id, rowMeta.page); await loadReviews(1, reviewKind); setSuccess('Conciliación completada. Las advertencias quedan para revisión y no se autoaprueban.'); } catch (requestError) { setError(errorMessage(requestError)); } finally { setSaving(false); } }; const generatePlan = async () => { if (!detail || !detail.analysis.reconciliation) return; if (detail.profileCode === 'MENDOZA_INVENTORY_V1' && !operatorAssetId) { setError('Seleccioná la organización operadora antes de generar el plan.'); return; } setSaving(true); setError(''); setSuccess(''); try { const created = await generateAssetImportPlan(detail.id, { operatorAssetId: detail.profileCode === 'MENDOZA_INVENTORY_V1' ? operatorAssetId : undefined, externalIdNamespace: detail.profileCode === 'MENDOZA_INVENTORY_V1' && externalNamespace.trim() ? externalNamespace.trim() : undefined, }); setPlan(created); await loadBatches(detail.id); await loadDetail(detail.id, rowMeta.page); await loadReviews(1, reviewKind); setSuccess(created.status === 'READY' ? 'Plan generado por entidades únicas. Está listo para confirmación.' : `Revisión obligatoria: quedan ${directReviewCount(created).toLocaleString('es-AR')} decisión/es humana/s. ${dependencyReviewCount(created).toLocaleString('es-AR')} dependencia/s se recalcularán automáticamente.`); } catch (requestError) { setError(errorMessage(requestError)); } finally { setSaving(false); } }; const resolveReview = async (item: AssetImportPlanItem, action: 'CREATE' | 'IGNORE', reasonLabel: string) => { if (!detail) return; const reason = window.prompt(`Motivo para ${reasonLabel.toLowerCase()} “${item.displayName}”:`, reasonLabel); if (!reason || reason.trim().length < 5) return; setSaving(true); setError(''); setSuccess(''); try { const updated = await resolveAssetImportPlanItem(detail.id, item.id, { action, reason: reason.trim() }); setPlan(updated); await loadReviews(1, reviewKind); setSuccess(`Revisión resuelta: ${item.displayName}.`); } catch (requestError) { setError(errorMessage(requestError)); } finally { setSaving(false); } }; const searchMatch = async (item: AssetImportPlanItem, query?: string) => { const search = (query ?? item.displayName).replace(/ · .+$/, '').trim(); setMatchItem(item); setMatchSearch(search); setMatchResults([]); setMatchLoading(true); setError(''); try { const response = await listAssets({ pageSize: 20, search: search || undefined }); setMatchResults(response.data.filter((asset) => !item.assetTypeCode || asset.type.code === item.assetTypeCode)); } catch (requestError) { setError(errorMessage(requestError)); } finally { setMatchLoading(false); } }; const chooseMatch = async (asset: AssetListItem) => { if (!detail || !matchItem) return; if (!window.confirm(`¿Vincular “${matchItem.displayName}” con ${asset.code} · ${asset.name}?`)) return; const reason = window.prompt('Motivo de la vinculación manual:', 'Vinculación con existente verificada manualmente'); if (!reason || reason.trim().length < 5) return; setSaving(true); setError(''); try { const updated = await resolveAssetImportPlanItem(detail.id, matchItem.id, { action: 'MATCH', matchedAssetId: asset.id, reason: reason.trim() }); setPlan(updated); await loadReviews(1, reviewKind); setMatchItem(null); setMatchResults([]); setSuccess(`Se usará el registro existente ${asset.code} · ${asset.name}.`); } catch (requestError) { setError(errorMessage(requestError)); } finally { setSaving(false); } }; const chooseContextCandidate = async (item: AssetImportPlanItem, candidate: InventoryContextCandidate) => { if (!detail) return; const label = `${candidate.name}${candidate.parentName ? ` · Área ${candidate.parentName}` : ''}`; if (!window.confirm(`¿Confirmar “${label}” como contexto territorial para este inventario? La elección se guarda sólo para este lote. Al confirmar, el sistema recalculará automáticamente la estructura local, padres y equipos dependientes.`)) return; const reason = window.prompt('Motivo de la elección territorial:', 'Contexto territorial verificado contra la fuente'); if (!reason || reason.trim().length < 5) return; setSaving(true); setError(''); setSuccess(''); try { await resolveAssetImportPlanItem(detail.id, item.id, { action: 'MATCH', matchedAssetId: candidate.id, reason: reason.trim() }); const regenerated = await generateAssetImportPlan(detail.id, { operatorAssetId: detail.profileCode === 'MENDOZA_INVENTORY_V1' ? operatorAssetId : undefined, externalIdNamespace: detail.profileCode === 'MENDOZA_INVENTORY_V1' && externalNamespace.trim() ? externalNamespace.trim() : undefined, }); setPlan(regenerated); await loadBatches(detail.id); await loadDetail(detail.id, rowMeta.page); await loadReviews(1, reviewKind); setSuccess(regenerated.status === 'READY' ? `Contexto confirmado: ${label}. El sistema recalculó las dependencias y el plan quedó listo para confirmar.` : `Contexto confirmado: ${label}. El sistema recalculó las dependencias; quedan ${directReviewCount(regenerated).toLocaleString('es-AR')} decisión/es humana/s.`); } catch (requestError) { setError(errorMessage(requestError)); } finally { setSaving(false); } }; const applySafePlan = async () => { if (!detail || !plan || plan.status !== 'REVIEW_REQUIRED' || !canApply || plan.masterStateStale || summaryNumber(plan, 'safeCreateItems') <= 0) return; const confirmation = window.prompt(`Se incorporarán únicamente los registros independientes que ya son seguros. Listos para crear ahora: ${summaryNumber(plan, 'safeCreateItems').toLocaleString('es-AR')} Ramas que esperan una revisión: ${summaryNumber(plan, 'blockedCreateItems').toLocaleString('es-AR')} Decisiones pendientes: ${directReviewCount(plan).toLocaleString('es-AR')} Dependencias pendientes: ${dependencyReviewCount(plan).toLocaleString('es-AR')} Las ramas pendientes no se modificarán. Escribí IMPORTAR para continuar:`); if (confirmation !== 'IMPORTAR') return; setSaving(true); setError(''); setSuccess(''); try { const updated = await applySafeAssetImportPlan(detail.id, plan.planHash); setPlan(updated); await loadBatches(detail.id); await loadDetail(detail.id, 1); await loadOrganizations(); await loadReviews(1, reviewKind); setSuccess(`Importación parcial completada. Los registros seguros se incorporaron y las revisiones pendientes quedaron guardadas para resolver más adelante.`); } catch (requestError) { setError(errorMessage(requestError)); } finally { setSaving(false); } }; const applyPlan = async () => { if (!detail || !plan || plan.status !== 'READY' || !canApply || plan.masterStateStale) return; const confirmation = window.prompt(`Esta acción incorporará registros al inventario en una única transacción.\n\nCrear nuevos: ${summaryNumber(plan, 'createItems').toLocaleString('es-AR')}\nUsar existentes: ${summaryNumber(plan, 'matchItems').toLocaleString('es-AR')}\nIgnorar: ${summaryNumber(plan, 'ignoreItems').toLocaleString('es-AR')}\n\nEscribí APLICAR para confirmar:`); if (confirmation !== 'APLICAR') return; setSaving(true); setError(''); setSuccess(''); try { const applied = await applyAssetImportPlan(detail.id, plan.planHash); setPlan(applied); await loadBatches(detail.id); await loadDetail(detail.id, 1); await loadOrganizations(); await loadReviews(1, reviewKind); setSuccess('Importación aplicada transaccionalmente. Los registros y relaciones quedaron vinculados al lote de origen.'); } catch (requestError) { setError(errorMessage(requestError)); } finally { setSaving(false); } }; const rollbackPlan = async () => { if (!detail || !plan || (plan.status !== 'APPLIED' && executionStatus(detail) !== 'PARTIAL') || !canApply) return; const reason = window.prompt('Motivo de la reversión lógica del lote:'); if (!reason || reason.trim().length < 8) return; if (!window.confirm('La reversión inactivará únicamente entidades creadas por este lote y se bloqueará si ya tienen uso posterior. ¿Continuar?')) return; setSaving(true); setError(''); setSuccess(''); try { const rolledBack = await rollbackAssetImportPlan(detail.id, reason.trim()); setPlan(rolledBack); await loadBatches(detail.id); await loadDetail(detail.id, 1); await loadOrganizations(); await loadReviews(1, reviewKind); setSuccess('Lote revertido lógicamente. No se eliminaron registros históricos.'); } catch (requestError) { setError(errorMessage(requestError)); } finally { setSaving(false); } }; const cancel = async () => { if (!detail || detail.status === 'CANCELLED' || plan?.status === 'APPLIED' || executionStatus(detail) === 'PARTIAL') return; const reason = window.prompt('Motivo de cancelación del lote:'); if (!reason || reason.trim().length < 5) return; setSaving(true); setError(''); try { await cancelAssetImport(detail.id, reason.trim()); await loadBatches(detail.id); await loadDetail(detail.id); setSuccess('Lote cancelado. El archivo queda conservado para auditoría.'); } catch (requestError) { setError(errorMessage(requestError)); } finally { setSaving(false); } }; const filteredIssueCount = useMemo(() => detail?.topIssues.reduce((sum, item) => sum + item.count, 0) ?? 0, [detail]); const reconciliation = detail ? record(detail.analysis.reconciliation) : null; const planByKind = plan ? record(plan.summary.byKind) : null; const applied = plan?.status === 'APPLIED'; const planReady = plan?.status === 'READY'; const planMasterStale = Boolean(plan?.masterStateStale); const directReviewItems = plan?.reviewItems.filter((item) => !isDependencyReview(item)) ?? []; const dependencyReviewItems = plan?.reviewItems.filter((item) => isDependencyReview(item)) ?? []; const execution = detail ? record(detail.analysis.importExecution) : null; const partiallyApplied = execution?.status === 'PARTIAL'; const appliedCreateItems = Number(execution?.appliedCreateItems ?? plan?.summary.appliedCreateItems ?? 0); const safeCreateItems = plan ? summaryNumber(plan, 'safeCreateItems') : 0; const blockedCreateItems = plan ? summaryNumber(plan, 'blockedCreateItems') : 0; if (loading) return ; return
INVENTARIOS · DATOS REALES

Centro de importaciones

Convertí inventarios masivos en un plan auditable antes de modificar los inventarios.

{error && {error}} {success && {success}}
{section === 'BATCHES' ? <> Proceso de importación: las decisiones pendientes no frenan las ramas independientes. Podés importar lo seguro y dejar el resto en Revisiones para resolverlo más adelante.
1 · Análisis 2 · Normalización 3 · Conciliación 4 · Revisiones 5 · Aplicación segura 6 · Cierre
{canManage &&
NUEVO LOTE

Analizar XLSX / CSV

}
Lotes analizados{batches.length} visibles
{batches.length === 0 ? :
{batches.map((batch) => { const state = batchStatus(batch); return ; })}
}
{!selectedId ? : detailLoading && !detail ? : detail && <>
ANÁLISIS

{detail.originalName}

{canManage && detail.status !== 'CANCELLED' && !applied && !partiallyApplied && }{canManage && detail.status !== 'CANCELLED' && !applied && !partiallyApplied && }
Filas{detail.totalRows.toLocaleString('es-AR')}
Analizables{detail.readyRows.toLocaleString('es-AR')}
Advertencias{detail.warningRows.toLocaleString('es-AR')}
Conflictos{detail.conflictRows.toLocaleString('es-AR')}
Perfil detectado{profileLabel(detail.profileCode)}Confianza {detail.profileConfidence}%
Hoja / encabezado{detail.worksheetName ?? '—'}Fila {detail.headerRow ?? '—'}
Archivo{formatBytes(detail.sizeBytes)}SHA {detail.sha256.slice(0, 12)}…
{reconciliation &&
Conciliación por filas
Nuevas {Number(reconciliation.createRows ?? 0).toLocaleString('es-AR')}Ya existentes {Number(reconciliation.matchRows ?? 0).toLocaleString('es-AR')}Revisar {Number(reconciliation.reviewRows ?? 0).toLocaleString('es-AR')}Ignorar {Number(reconciliation.ignoreRows ?? 0).toLocaleString('es-AR')}
Es una señal preliminar. El Plan siguiente deduplica Áreas, Yacimientos, Organizaciones, instalaciones y elementos antes de cualquier incorporación.
} {detail.duplicateFiles.length > 0 && Este mismo archivo (mismo SHA-256) ya fue analizado {detail.duplicateFiles.length} vez/veces anteriormente.} {detail.topIssues.length > 0 &&
Principales observaciones · {filteredIssueCount.toLocaleString('es-AR')}
{detail.topIssues.map((issue) => {ISSUE_LABELS[issue.code] ?? issue.code} {issue.count})}
}
{reconciliation && detail.status !== 'CANCELLED' &&
PLAN DE IMPORTACIÓN

Entidades únicas y operaciones

Las filas repetidas se agrupan antes de crear Áreas, Yacimientos, relaciones e instalaciones.

{plan && (() => { const state = planState(plan); return {state.label}; })()}
{detail.profileCode === 'MENDOZA_INVENTORY_V1' && !applied &&
} {!applied && canManage &&
} {plan && <>
Revisión #{plan.revision}{plan.externalIdNamespace && Namespace {plan.externalIdNamespace}}Hash {plan.planHash.slice(0, 12)}…
{summaryNumber(plan, 'explicitUnassignedOperatorFields') > 0 && La fuente declara {summaryNumber(plan, 'explicitUnassignedOperatorFields').toLocaleString('es-AR')} Yacimientos sin empresa operadora. Se conservarán como borradores dentro de su Área, sin inventar una organización ni asignar contexto operativo.} {planByKind &&
Plan relacional por entidadLas filas se convierten en entidades únicas y relaciones; no se duplican catálogos.
{summaryNumber(plan, 'relationalTerritoryPlan') > 0 &&
Departamentos únicos{summaryNumber(plan, 'normalizedDepartments').toLocaleString('es-AR')}
Tipos de derecho{summaryNumber(plan, 'normalizedLegalRightTypes').toLocaleString('es-AR')}
Área ↔ Departamento{summaryNumber(plan, 'areaDepartmentRelations').toLocaleString('es-AR')}
Derechos / concesiones{summaryNumber(plan, 'legalRights').toLocaleString('es-AR')}
} {PLAN_GROUPS.map((group) => { const visibleKinds = group.kinds.filter((kind) => Number(record(planByKind[kind])?.total ?? 0) > 0); if (!visibleKinds.length) return null; return

{group.label}

{visibleKinds.map((kind) => { const counts = record(planByKind[kind])!; return ; })}
EntidadCrear nuevosUsar existentesRevisarIgnorarTotal
{ENTITY_LABELS[kind]}{Number(counts.create ?? 0).toLocaleString('es-AR')}{Number(counts.match ?? 0).toLocaleString('es-AR')}{Number(counts.review ?? 0).toLocaleString('es-AR')}{Number(counts.ignore ?? 0).toLocaleString('es-AR')}{Number(counts.total ?? 0).toLocaleString('es-AR')}
; })} {(planDetailKind || planDetailAction) &&
{planDetailKind ? ENTITY_LABELS[planDetailKind] : planDetailAction ? PLAN_ACTION_LABELS[planDetailAction] : 'Detalle del plan'}{planDetailMeta.total.toLocaleString('es-AR')} elementos únicos
{planDetailLoading ? : <>
{planDetailItems.map((item) => { const objectId = matchedObjectId(item); return ; })}
DecisiónEntidad / relaciónQué hará el sistemaFilas fuente
{PLAN_ACTION_LABELS[item.action]}{item.displayName}{ENTITY_LABELS[item.entityKind]}{item.entityKind === 'DEPARTMENT' && Código {String(item.payload.departmentCode ?? '—')}}{item.entityKind === 'LEGAL_RIGHT' && {String(item.payload.rightType ?? '—')} · estado inicial PENDING}{item.action === 'MATCH' ?
Se reutiliza un registro del inventario{MATCH_CRITERIA[item.entityKind]}{objectId && ID {objectId.slice(0, 8)}…}
: item.action === 'CREATE' ?
Se creará un registro nuevoNo se encontró una coincidencia exacta y segura con las reglas de este tipo.
: item.action === 'REVIEW' ?
{item.reviewCodes.slice(0, 3).map((code) => {reviewGuidance(code).title})}
: No se incorporará al inventario.}
{sourceRowsLabel(item.sourceRowNumbers)}
{planDetailMeta.totalPages > 1 &&
Página {planDetailMeta.page} de {planDetailMeta.totalPages}
}}
}
} {plan.status === 'REVIEW_REQUIRED' &&
REVISIONES PENDIENTESPodés continuar con los registros independientes y decidir esto más adelante

Las ramas que dependan de una decisión quedan pendientes. El resto puede incorporarse al inventario sin asumir datos ni jerarquías.

Ramas pendientes protegidasLas decisiones no se aplican hasta resolverlas
1Resolver decisiones{directReviewCount(plan).toLocaleString('es-AR')} pendientes
2Recalcular dependencias{dependencyReviewCount(plan).toLocaleString('es-AR')} automáticas
3Confirmar importaciónSólo con plan limpio
No resuelvas instalaciones o padres derivados. Si varias filas dependen del mismo Área/Yacimiento, elegís ese contexto una sola vez. El sistema vuelve a generar el plan y recalcula estructura local, padres y equipos. Nomenclatura de la operadora preservada. BATERIA, SET, PTC y otros nombres propios no se convierten en categorías universales de DH. Se conservan como Estructura local según fuente.
Vos tenés que decidir{directReviewCount(plan).toLocaleString('es-AR')}Decisiones humanas obligatorias
El sistema resuelve después{dependencyReviewCount(plan).toLocaleString('es-AR')}Dependencias automáticas
Motivos detectados
{Array.from(plan.reviewItems.reduce((map, item) => { item.reviewCodes.forEach((code) => map.set(code, (map.get(code) ?? 0) + 1)); return map; }, new Map()).entries()).sort((a, b) => b[1] - a[1]).slice(0, 6).map(([code, count]) => {reviewGuidance(code).title} {count})}
{directReviewItems.length === 0 ? No quedan decisiones humanas visibles en este plan. Si todavía aparecen dependencias, regenerá el plan para que el sistema las recalcule sobre las decisiones ya tomadas. : <>
ACCIÓN REQUERIDAEstas son las únicas tarjetas que tenés que resolver
{directReviewItems.length.toLocaleString('es-AR')} mostradas
{directReviewItems.map((item) => { const facts = reviewFacts(item); const provisional = isProvisionalName(item.displayName); const contextCandidates = inventoryContextCandidates(item); const contextDecision = item.payload.inventoryContextDecision === true; return
{contextDecision ? 'Elegir contexto territorial' : 'Decisión humana requerida'} {ENTITY_LABELS[item.entityKind]} · {sourceRowsLabel(item.sourceRowNumbers)}
{provisionalDisplayName(item.displayName)} {provisional && Nombre provisional de un plan anterior. Regenerá con la versión actual para preservar la nomenclatura local.} {facts.length > 0 &&
1 · Qué informa el archivo
{facts.map(([label, current]) =>
{label}
{current}
)}
}
2 · Por qué el sistema no decide solo{item.reviewCodes.map((code) => { const guidance = reviewGuidance(code); return

{guidance.title}. {guidance.detail}

; })}
3 · Qué tenés que decidir {contextDecision ? <>

El nombre de la fuente puede corresponder a más de un lugar. Compará el Área padre y elegí una sola alternativa. No se modifica el inventario al elegir.

{contextCandidates.map((candidate) => )}
{contextCandidates.length === 0 &&

No quedaron alternativas válidas. Regenerá el plan o revisá el inventario.

} Después: el plan se regenera automáticamente y recalcula todos los registros dependientes. : <>

{item.reviewCodes.map((code) => reviewGuidance(code).decision).join(' ')}

{item.payload.manualMatchAllowed !== false && item.assetTypeCode && item.entityKind !== 'OPERATOR_RELATION' && }{item.payload.manualCreateAllowed === true && Boolean(item.assetTypeCode) && }{item.entityKind === 'TECHNICAL_ASSET' && }
}
; })}
} {dependencyReviewItems.length > 0 &&
{dependencyReviewCount(plan).toLocaleString('es-AR')} dependencias automáticasNo requieren una decisión individualVer detalle
{dependencyReviewItems.map((item) =>
{provisionalDisplayName(item.displayName)}{ENTITY_LABELS[item.entityKind]} · {sourceRowsLabel(item.sourceRowNumbers)}
{item.reviewCodes.map((code) => reviewGuidance(code).title).join(' · ')}
)}
}
} {matchItem &&
Vincular “{provisionalDisplayName(matchItem.displayName)}” con un registro existente
setMatchSearch(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') { event.preventDefault(); void searchMatch(matchItem, matchSearch); } }} placeholder="Código o nombre en inventarios…" />
{matchLoading ? : matchResults.length === 0 ?

No se encontraron registros compatibles en los primeros resultados. Probá otro código o nombre.

:
{matchResults.map((asset) => )}
}
} {partiallyApplied && Importación parcial en curso. Ya se incorporaron {appliedCreateItems.toLocaleString('es-AR')} entidades del lote. Las revisiones y sus dependencias siguen pendientes y pueden resolverse desde la pestaña Revisiones.} {planReady && !planMasterStale && Plan listo: no quedan decisiones pendientes. La confirmación valida tanto el hash del plan como el estado de los inventarios antes de escribir.} {planReady && planMasterStale && Plan obsoleto. El inventario cambió desde que se generó esta revisión. Conciliá y regenerá el plan antes de aplicar para evitar duplicados.} {plan.status === 'APPLIED' && Importación aplicada el {plan.appliedAt ? new Date(plan.appliedAt).toLocaleString('es-AR') : '—'}. El lote permanece trazable y puede revertirse lógicamente mientras no existan dependencias posteriores.} {plan.status === 'ROLLED_BACK' && La importación fue revertida lógicamente. Las entidades creadas quedaron inactivas y se conservó su historial.}
{plan.status === 'REVIEW_REQUIRED' && canApply && safeCreateItems > 0 && } {plan.status === 'REVIEW_REQUIRED' && canApply && safeCreateItems === 0 && No quedan ramas independientes para importar. {blockedCreateItems.toLocaleString('es-AR')} registros esperan una revisión.} {planReady && canApply && } {(planReady || plan.status === 'REVIEW_REQUIRED') && !canApply && Sólo Director/Admin con permiso asset_imports.apply puede aplicar.} {(plan.status === 'APPLIED' || partiallyApplied) && canApply && }
}
}
setRowSearch(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') { event.preventDefault(); void loadDetail(detail.id, 1); } }} placeholder="Buscar ID, equipo, ubicación…" />
Vista previa normalizada{rowMeta.total.toLocaleString('es-AR')} filas
{detail.profileCode === 'MENDOZA_YACIMIENTOS_V1' ? <> {rows.map((row) => { const state = rowStatus(row.status); const rec = record(row.normalizedData.reconciliation); return ; })} : <> {rows.map((row) => { const state = rowStatus(row.status); const rec = record(row.normalizedData.reconciliation); return ; })} }
FilaResultadoÁreaYacimientoDepartamentoDerechoOperadoraConciliaciónObservaciones
{row.rowNumber}{state.label} {value(row.normalizedData, 'area')}{value(row.normalizedData, 'field')}{value(row.normalizedData, 'department')}{value(row.normalizedData, 'rightType')}{value(row.normalizedData, 'operator')} {row.importedAssetId ? Importado : row.matchedAssetId ? Ya existe : rec ? {row.suggestedAction === 'REVIEW' || Number(rec.candidateCount ?? 0) > 1 ? 'Revisar' : 'Nuevo'} : Sin conciliar}
{row.issues.length ? row.issues.slice(0, 3).map((issue) => {ISSUE_LABELS[issue] ?? issue}) : Sin observaciones}
FilaResultadoÁrea / YacimientoInstalación / sectorEquipoNormalizaciónID inventarioCant.Estado fuenteEstado sugeridoConciliaciónObservaciones
{row.rowNumber}{state.label} {value(row.normalizedData, 'areaOrField')} {value(row.normalizedData, 'installation')}{value(row.normalizedData, 'subInstallation')} {value(row.normalizedData, 'equipment')}{value(row.normalizedData, 'sourceClassification')} {value(row.normalizedData, 'normalizedFamily')}{value(row.normalizedData, 'normalizedSubtype')} {value(row.normalizedData, 'inventoryId')}{value(row.normalizedData, 'quantity')}{value(row.normalizedData, 'sourceStatus')} {value(row.normalizedData, 'operationalStatusSuggestion')}{value(row.normalizedData, 'conditionStatusSuggestion')} {row.importedAssetId ? Importado : row.matchedAssetId ? Ya existe : rec ? {row.suggestedAction === 'REVIEW' || Number(rec.candidateCount ?? 0) > 1 ? 'Revisar' : 'Nuevo'} : Sin conciliar}
{row.issues.length ? row.issues.slice(0, 3).map((issue) => {ISSUE_LABELS[issue] ?? issue}) : Sin observaciones}{row.issues.length > 3 && +{row.issues.length - 3}}
Página {rowMeta.page} de {Math.max(rowMeta.totalPages, 1)}
}
: { setReviewKind(kind); void loadReviews(1, kind); }} onPageChange={(page) => void loadReviews(page, reviewKind)} onOpenBatch={(batchId) => { setSection('BATCHES'); setScrollToReviews(true); setSelectedId(batchId); }} />}
; }