Compare commits

...

2 Commits

3 changed files with 820 additions and 250 deletions

View File

@@ -56,3 +56,12 @@ export const downloadAcuseEdocument = async (edocId) => {
window.URL.revokeObjectURL(url); window.URL.revokeObjectURL(url);
document.body.removeChild(a); document.body.removeChild(a);
}; };
export const resetAcuseEdocument = async (edocId) => {
const response = await fetchWithAuth(
`${API_BASE_URL}/customs/edocuments/${edocId}/reset-acuse/`,
{ method: 'POST' }
);
if (!response.ok) throw new Error(await extractApiError(response));
return response.json();
};

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { getWithAuth, postWithAuth, fetchWithAuth } from '../fetchWithAuth'; import { getWithAuth, postWithAuth, fetchWithAuth } from '../fetchWithAuth';
import { useNotification } from '../context/NotificationContext'; import { useNotification } from '../context/NotificationContext';
@@ -34,15 +34,43 @@ const PROCESAMIENTO_URL_MAP = {
function AuditResultModal({ modal, iniciandoProcesamiento, onClose, onIniciarProcesamiento }) { function AuditResultModal({ modal, iniciandoProcesamiento, onClose, onIniciarProcesamiento }) {
const { tipo, pedimento_app, data } = modal; const { tipo, pedimento_app, data } = modal;
// si el backend no retorna 'estado' (ej. pedimento completo), tratamos como no completado
const esCompletado = data?.estado === 'completado' || data?.auditoria_completa == true; const esCompletado = tipo === 'pc'
// remesa sin remesas: no tiene nada que procesar, ocultar botón ? (!data?.hay_pendientes && !data?.hay_errores)
const sinNadaQueHacer = tipo === 'rm' && data?.tiene_remesas === false; : (data?.estado === 'completado' || data?.auditoria_completa == true);
const sinNadaQueHacer = (tipo === 'rm' && data?.tiene_remesas === false);
const mostrarBotonProcesar = !esCompletado && !sinNadaQueHacer; const mostrarBotonProcesar = !esCompletado && !sinNadaQueHacer;
const tituloTipo = TIPO_LABELS[tipo] || tipo; const tituloTipo = TIPO_LABELS[tipo] || tipo;
const hayErroresPC = tipo === 'pc' && data?.hay_errores;
const iconoEstado = esCompletado ? ( const headerBg = hayErroresPC
? 'bg-red-50 border-b border-red-100'
: esCompletado
? 'bg-green-50 border-b border-green-100'
: 'bg-amber-50 border-b border-amber-100';
const badge = hayErroresPC ? (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-semibold bg-red-100 text-red-700">
<span className="w-2 h-2 rounded-full bg-red-500"></span>Con errores
</span>
) : esCompletado ? (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-semibold bg-green-100 text-green-700">
<span className="w-2 h-2 rounded-full bg-green-500"></span>Completado
</span>
) : (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-semibold bg-amber-100 text-amber-700">
<span className="w-2 h-2 rounded-full bg-amber-500"></span>Pendiente
</span>
);
const iconoEstado = hayErroresPC ? (
<div className="flex items-center justify-center w-12 h-12 mx-auto mb-3 bg-red-100 rounded-full">
<svg className="w-7 h-7 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
) : esCompletado ? (
<div className="flex items-center justify-center w-12 h-12 mx-auto mb-3 bg-green-100 rounded-full"> <div className="flex items-center justify-center w-12 h-12 mx-auto mb-3 bg-green-100 rounded-full">
<svg className="w-7 h-7 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-7 h-7 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
@@ -56,144 +84,315 @@ function AuditResultModal({ modal, iniciandoProcesamiento, onClose, onIniciarPro
</div> </div>
); );
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"> <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
<div className="w-full max-w-lg bg-white rounded-2xl shadow-2xl border border-gray-100 overflow-hidden"> <div className="w-full max-w-lg bg-white rounded-2xl shadow-2xl border border-gray-100 overflow-hidden flex flex-col max-h-[90vh]">
{/* Header */} {/* Header */}
<div className={`px-6 py-4 ${esCompletado ? 'bg-green-50 border-b border-green-100' : 'bg-amber-50 border-b border-amber-100'}`}> <div className={`px-6 py-4 flex-shrink-0 ${headerBg}`}>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide">{tituloTipo}</p> <p className="text-xs font-medium text-gray-500 uppercase tracking-wide">{tituloTipo}</p>
<h3 className="text-lg font-bold text-gray-900 mt-0.5">{pedimento_app}</h3> <h3 className="text-lg font-bold text-gray-900 mt-0.5">{pedimento_app}</h3>
</div> </div>
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-semibold ${ {badge}
esCompletado
? 'bg-green-100 text-green-700'
: 'bg-amber-100 text-amber-700'
}`}>
<span className={`w-2 h-2 rounded-full ${esCompletado ? 'bg-green-500' : 'bg-amber-500'}`}></span>
{esCompletado ? 'Completado' : 'En proceso'}
</span>
</div> </div>
</div> </div>
{/* Cuerpo */} {/* Cuerpo scrolleable */}
<div className="px-6 py-5 space-y-4"> <div className="px-6 py-5 space-y-4 overflow-y-auto">
{iconoEstado} {iconoEstado}
{/* Mensaje principal */} {tipo === 'pc' ? (
<p className="text-center text-gray-700 font-medium">{data?.mensaje}</p> <>
{/* Información del XML */}
{/* Resumen numérico */} {data?.informacion_xml && (
{data?.resumen && ( <div className="rounded-xl border border-gray-100 overflow-hidden text-xs">
<div className="grid grid-cols-2 gap-3 mt-2"> <div className="px-4 py-2.5 bg-gray-50 border-b border-gray-100">
{Object.entries(data.resumen).map(([key, val]) => ( <p className="text-sm font-semibold text-gray-700">Información del pedimento</p>
<div key={key} className="flex flex-col items-center p-3 bg-gray-50 rounded-xl border border-gray-100">
<span className="text-2xl font-bold text-gray-900">{val}</span>
<span className="text-xs text-gray-500 mt-0.5 text-center capitalize">
{key.replace(/_/g, ' ')}
</span>
</div>
))}
</div>
)}
{/* Resumen de XMLs analizados — solo muestra lo relevante */}
{data?.xmls_analizados && (() => {
const total = data.xmls_analizados.length;
const conError = data.xmls_analizados.filter(x => x.informacion_extraida?.tiene_error === true);
const auditOk = data.auditoria_completa !== false;
return (
<div className="space-y-3">
{/* Contadores compactos */}
<div className="grid grid-cols-3 gap-2">
<div className="flex flex-col items-center p-3 bg-gray-50 rounded-xl border border-gray-100">
<span className="text-2xl font-bold text-gray-900">{data.archivos_xml_encontrados ?? total}</span>
<span className="text-xs text-gray-500 mt-0.5 text-center">Encontrados</span>
</div> </div>
<div className="flex flex-col items-center p-3 bg-gray-50 rounded-xl border border-gray-100"> <div className="divide-y divide-gray-50">
<span className="text-2xl font-bold text-gray-900">{total}</span> {[
<span className="text-xs text-gray-500 mt-0.5 text-center">Analizados</span> { label: 'Contribuyente', value: `${data.informacion_xml.contribuyente_rfc}${data.informacion_xml.contribuyente_nombre}` },
</div> { label: 'Tipo operación', value: `${data.informacion_xml.clave_pedimento} · ${data.informacion_xml.tipo_operacion}` },
<div className={`flex flex-col items-center p-3 rounded-xl border ${conError.length > 0 ? 'bg-red-50 border-red-100' : 'bg-green-50 border-green-100'}`}> // { label: 'Aduana', value: data.informacion_xml.aduana_clave },
<span className={`text-2xl font-bold ${conError.length > 0 ? 'text-red-700' : 'text-green-700'}`}>{conError.length}</span> // { label: 'Partidas', value: data.informacion_xml.numero_partidas },
<span className={`text-xs mt-0.5 text-center ${conError.length > 0 ? 'text-red-500' : 'text-green-500'}`}>Con error</span> { label: 'Fecha pago', value: data.informacion_xml.fecha_pago?.split('-06:00')[0] ?? '—' },
// { label: 'Fecha presentación', value: data.informacion_xml.fecha_presentacion?.split('-06:00')[0] ?? '—' },
{ label: 'Valor comercial', value: data.informacion_xml.valor_comercial_total != null ? `$${data.informacion_xml.valor_comercial_total.toLocaleString('es-MX')}` : '—' },
{ label: 'Tipo cambio', value: data.informacion_xml.tipo_cambio },
].map(({ label, value }) => (
<div key={label} className="flex items-start px-4 py-1.5 gap-3 bg-white">
<span className="w-32 flex-shrink-0 text-gray-500">{label}</span>
<span className="font-medium text-gray-800 break-all">{value ?? '—'}</span>
</div>
))}
</div> </div>
</div> </div>
)}
{/* Advertencia si auditoría incompleta */} {/* Diagnóstico del PC cuando no está descargado */}
{!auditOk && ( {!data?.pc_descargado && (
<div className="flex items-center gap-2 px-3 py-2 bg-amber-50 border border-amber-100 rounded-lg text-sm text-amber-800"> <div className="rounded-xl border border-amber-100 overflow-hidden text-xs">
<svg className="w-4 h-4 flex-shrink-0 text-amber-500" fill="currentColor" viewBox="0 0 24 24"> <div className="px-4 py-2.5 bg-amber-50 border-b border-amber-100 flex items-center justify-between">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/> <p className="text-sm font-semibold text-amber-800">Pedimento completo</p>
<div className="flex items-center gap-2">
{data?.fuente && (
<span className={`px-2 py-0.5 rounded-full font-medium ${data.fuente === 'datastage' ? 'bg-purple-100 text-purple-700' : 'bg-gray-100 text-gray-600'}`}>
{data.fuente === 'datastage' ? 'Datastage' : 'Manual'}
</span>
)}
<span className="px-2 py-0.5 rounded-full bg-amber-100 text-amber-700 font-medium">No descargado</span>
</div>
</div>
<div className="divide-y divide-gray-50 bg-white">
{[
{ label: 'Aduana', value: data?.datos?.aduana, ok: data?.validacion?.aduana_valida },
{ label: 'Patente', value: data?.datos?.patente, ok: data?.validacion?.patente_valida },
{ label: 'Núm. pedimento', value: data?.datos?.numero_pedimento, ok: data?.validacion?.pedimento_valido },
{ label: 'Núm. operación', value: data?.datos?.numero_operacion, ok: data?.validacion?.numero_operacion_presente },
{ label: 'Contribuyente', value: data?.datos?.contribuyente_rfc, ok: data?.validacion?.tiene_contribuyente },
{ label: 'Credenciales VUCEM', value: data?.validacion?.tiene_credenciales_vucem ? 'Configuradas' : 'No configuradas', ok: data?.validacion?.tiene_credenciales_vucem },
].map(({ label, value, ok }) => (
<div key={label} className="flex items-center px-4 py-1.5 gap-3">
<span className="w-36 flex-shrink-0 text-gray-500">{label}</span>
<span className={`font-medium break-all ${ok === false ? 'text-red-600' : 'text-gray-800'}`}>
{value ?? <span className="text-gray-400 italic"></span>}
</span>
{ok === false && (
<svg className="w-3.5 h-3.5 flex-shrink-0 text-red-500 ml-auto" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>
)}
{ok === true && (
<svg className="w-3.5 h-3.5 flex-shrink-0 text-green-500 ml-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
</svg>
)}
</div>
))}
</div>
{!data?.puede_procesar && data?.razones_no_puede_procesar?.length > 0 && (
<div className="px-4 py-2.5 bg-red-50 border-t border-red-100 space-y-1">
{data.razones_no_puede_procesar.map((r, i) => (
<p key={i} className="text-xs text-red-700 font-medium"> {r}</p>
))}
</div>
)}
</div>
)}
{/* Errores detectados */}
{data?.hay_errores && data?.errores_detectados?.length > 0 && (
<details className="rounded-xl border border-red-200 overflow-hidden" open>
<summary className="flex items-center justify-between px-4 py-2.5 bg-red-50 text-sm font-semibold text-red-800 cursor-pointer list-none select-none">
<span>Errores detectados ({data.errores_detectados.length})</span>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
Auditoría incompleta algunos documentos no pudieron verificarse </summary>
<div className="divide-y divide-red-50 max-h-40 overflow-y-auto">
{data.errores_detectados.map((err, i) => (
<div key={i} className="px-4 py-2 bg-white text-xs text-red-700">{typeof err === 'string' ? err : JSON.stringify(err.nombre_archivo)}</div>
))}
</div> </div>
)} </details>
)}
{/* Detalle de XMLs con error */} {/* Nota de verificación manual cuando hay errores */}
{conError.length > 0 && ( {data?.hay_errores && (
<details className="rounded-xl border border-red-200 overflow-hidden" open> <div className="flex items-start gap-2.5 px-4 py-3 bg-red-50 border border-red-200 rounded-xl text-sm text-red-800">
<summary className="flex items-center justify-between px-4 py-2.5 bg-red-50 text-sm font-semibold text-red-800 cursor-pointer list-none select-none"> <svg className="w-4 h-4 flex-shrink-0 mt-0.5 text-red-500" fill="currentColor" viewBox="0 0 24 24">
<span>XMLs con error ({conError.length})</span> <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> </svg>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" /> <span>Se detectaron errores en las respuestas de VUCEM. Revisa y corrige los documentos de error antes de reintentar el procesamiento.</span>
</svg> </div>
</summary> )}
<div className="divide-y divide-red-50">
{conError.map((xml) => ( {/* Resultado del procesamiento PC */}
<div key={xml.documento_id} className="px-4 py-2 bg-white"> {data?.pc_resultado_procesamiento && (() => {
<p className="font-mono text-xs text-red-700 break-all">{xml.nombre_archivo}</p> const r = data.pc_resultado_procesamiento;
<p className="text-xs text-gray-500 mt-0.5">{xml.tipo_documento}</p> const isEncolado = r.estado === 'encolado';
const isYaDescargado = r.estado === 'ya_descargado';
const isNoPuede = r.estado === 'no_puede_procesar';
return (
<div className={`rounded-xl border overflow-hidden text-xs ${isEncolado ? 'border-green-200' : isYaDescargado ? 'border-blue-200' : 'border-red-200'}`}>
{/* Header del resultado */}
<div className={`px-4 py-2.5 flex items-center justify-between ${isEncolado ? 'bg-green-50' : isYaDescargado ? 'bg-blue-50' : 'bg-red-50'}`}>
<p className={`text-sm font-semibold ${isEncolado ? 'text-green-800' : isYaDescargado ? 'text-blue-800' : 'text-red-800'}`}>
{isEncolado ? 'Procesamiento encolado' : isYaDescargado ? 'Pedimento completo ya descargado' : 'No se puede procesar'}
</p>
<div className="flex items-center gap-2">
<span className={`px-2 py-0.5 rounded-full font-medium ${r.fuente === 'datastage' ? 'bg-purple-100 text-purple-700' : 'bg-gray-100 text-gray-600'}`}>
{r.fuente === 'datastage' ? 'Datastage' : 'Manual'}
</span>
</div>
</div>
{/* Datos del pedimento */}
<div className="divide-y divide-gray-50 bg-white">
{[
{ label: 'Aduana', value: r.datos?.aduana, ok: r.validacion?.aduana_valida },
{ label: 'Patente', value: r.datos?.patente, ok: r.validacion?.patente_valida },
{ label: 'Núm. pedimento', value: r.datos?.numero_pedimento, ok: r.validacion?.pedimento_valido },
{ label: 'Núm. operación', value: r.datos?.numero_operacion, ok: r.validacion?.numero_operacion_presente },
{ label: 'Contribuyente', value: r.datos?.contribuyente_rfc, ok: r.validacion?.tiene_contribuyente },
{ label: 'Credenciales VUCEM', value: r.validacion?.tiene_credenciales_vucem ? 'Configuradas' : 'No configuradas', ok: r.validacion?.tiene_credenciales_vucem },
{ label: 'Fecha pago', value: r.datos?.fecha_pago },
].map(({ label, value, ok }) => (
<div key={label} className="flex items-center px-4 py-1.5 gap-3">
<span className="w-36 flex-shrink-0 text-gray-500">{label}</span>
<span className={`font-medium break-all ${ok === false ? 'text-red-600' : ok === true ? 'text-gray-800' : 'text-gray-800'}`}>
{value ?? <span className="text-gray-400 italic"></span>}
</span>
{ok === false && (
<svg className="w-3.5 h-3.5 flex-shrink-0 text-red-500 ml-auto" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>
)}
{ok === true && (
<svg className="w-3.5 h-3.5 flex-shrink-0 text-green-500 ml-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
</svg>
)}
</div> </div>
))} ))}
</div> </div>
</details>
)}
{/* Todo bien */} {/* Razones por las que no puede procesar */}
{conError.length === 0 && auditOk && ( {isNoPuede && r.validacion?.razones_no_puede_procesar?.length > 0 && (
<p className="text-center text-sm text-green-700 font-medium">Todos los XMLs analizados están correctos</p> <div className="px-4 py-2.5 bg-red-50 border-t border-red-100 space-y-1">
)} {r.validacion.razones_no_puede_procesar.map((razon, i) => (
</div> <p key={i} className="text-red-700 font-medium"> {razon}</p>
); ))}
})()} </div>
)}
{/* Pendientes */} {/* Task ID si fue encolado */}
{data?.pendientes && data.pendientes.length > 0 && ( {isEncolado && r.task_id && (
<div className="mt-2"> <div className="flex items-center gap-2 px-4 py-2 bg-green-50 border-t border-green-100">
<p className="text-sm font-semibold text-gray-700 mb-2">Pendientes ({data.pendientes.length})</p> <span className="text-gray-500">Task ID:</span>
<div className="max-h-32 overflow-y-auto space-y-1"> <span className="font-mono bg-white px-2 py-0.5 rounded border border-green-200 text-green-700 select-all">{r.task_id}</span>
{data.pendientes.map((item, i) => ( </div>
<div key={i} className="flex items-center gap-2 px-3 py-1.5 bg-amber-50 rounded-lg border border-amber-100 text-sm text-amber-800"> )}
<svg className="w-3.5 h-3.5 flex-shrink-0 text-amber-500" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>
<span className="font-mono text-xs">{item}</span>
</div> </div>
))} );
</div> })()}
</div>
)}
{/* COVEs para remesa */} {/* Sin pendientes ni errores */}
{tipo === 'rm' && data?.coves && data.coves.length > 0 && ( {data?.pc_descargado && !data?.hay_errores && (
<div className="mt-2"> <p className="text-center text-sm text-green-700 font-medium">Pedimento completo descargado sin errores</p>
<p className="text-sm font-semibold text-gray-700 mb-2">COVEs registrados ({data.coves.length})</p> )}
<div className="flex flex-wrap gap-1.5"> </>
{data.coves.map((cove, i) => ( ) : (
<span key={i} className="px-2 py-0.5 bg-blue-50 text-blue-700 border border-blue-100 rounded text-xs font-mono"> <>
{cove} {/* Mensaje principal */}
</span> <p className="text-center text-gray-700 font-medium">{data?.mensaje}</p>
))}
</div> {/* Resumen numérico */}
</div> {data?.resumen && (
<div className="grid grid-cols-2 gap-3 mt-2">
{Object.entries(data.resumen).map(([key, val]) => (
<div key={key} className="flex flex-col items-center p-3 bg-gray-50 rounded-xl border border-gray-100">
<span className="text-2xl font-bold text-gray-900">{val}</span>
<span className="text-xs text-gray-500 mt-0.5 text-center capitalize">
{key.replace(/_/g, ' ')}
</span>
</div>
))}
</div>
)}
{/* Resumen de XMLs analizados */}
{data?.xmls_analizados && (() => {
const total = data.xmls_analizados.length;
const conError = data.xmls_analizados.filter(x => x.informacion_extraida?.tiene_error === true);
const auditOk = data.auditoria_completa !== false;
return (
<div className="space-y-3">
<div className="grid grid-cols-3 gap-2">
<div className="flex flex-col items-center p-3 bg-gray-50 rounded-xl border border-gray-100">
<span className="text-2xl font-bold text-gray-900">{data.archivos_xml_encontrados ?? total}</span>
<span className="text-xs text-gray-500 mt-0.5 text-center">Encontrados</span>
</div>
<div className="flex flex-col items-center p-3 bg-gray-50 rounded-xl border border-gray-100">
<span className="text-2xl font-bold text-gray-900">{total}</span>
<span className="text-xs text-gray-500 mt-0.5 text-center">Analizados</span>
</div>
<div className={`flex flex-col items-center p-3 rounded-xl border ${conError.length > 0 ? 'bg-red-50 border-red-100' : 'bg-green-50 border-green-100'}`}>
<span className={`text-2xl font-bold ${conError.length > 0 ? 'text-red-700' : 'text-green-700'}`}>{conError.length}</span>
<span className={`text-xs mt-0.5 text-center ${conError.length > 0 ? 'text-red-500' : 'text-green-500'}`}>Con error</span>
</div>
</div>
{!auditOk && (
<div className="flex items-center gap-2 px-3 py-2 bg-amber-50 border border-amber-100 rounded-lg text-sm text-amber-800">
<svg className="w-4 h-4 flex-shrink-0 text-amber-500" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>
Auditoría incompleta algunos documentos no pudieron verificarse
</div>
)}
{conError.length > 0 && (
<details className="rounded-xl border border-red-200 overflow-hidden" open>
<summary className="flex items-center justify-between px-4 py-2.5 bg-red-50 text-sm font-semibold text-red-800 cursor-pointer list-none select-none">
<span>XMLs con error ({conError.length})</span>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
</svg>
</summary>
<div className="divide-y divide-red-50 max-h-48 overflow-y-auto">
{conError.map((xml) => (
<div key={xml.documento_id} className="px-4 py-2 bg-white">
<p className="font-mono text-xs text-red-700 break-all">{xml.nombre_archivo}</p>
<p className="text-xs text-gray-500 mt-0.5">{xml.tipo_documento}</p>
</div>
))}
</div>
</details>
)}
{conError.length === 0 && auditOk && (
<p className="text-center text-sm text-green-700 font-medium">Todos los XMLs analizados están correctos</p>
)}
</div>
);
})()}
{/* Pendientes (lista plana para rm/ac/cove/edoc/pt) */}
{data?.pendientes && Array.isArray(data.pendientes) && data.pendientes.length > 0 && (
<div className="mt-2">
<p className="text-sm font-semibold text-gray-700 mb-2">Pendientes ({data.pendientes.length})</p>
<div className="max-h-32 overflow-y-auto space-y-1">
{data.pendientes.map((item, i) => (
<div key={i} className="flex items-center gap-2 px-3 py-1.5 bg-amber-50 rounded-lg border border-amber-100 text-sm text-amber-800">
<svg className="w-3.5 h-3.5 flex-shrink-0 text-amber-500" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>
<span className="font-mono text-xs">{item}</span>
</div>
))}
</div>
</div>
)}
{/* COVEs para remesa */}
{tipo === 'rm' && data?.coves && data.coves.length > 0 && (
<div className="mt-2">
<p className="text-sm font-semibold text-gray-700 mb-2">COVEs registrados ({data.coves.length})</p>
<div className="flex flex-wrap gap-1.5">
{data.coves.map((cove, i) => (
<span key={i} className="px-2 py-0.5 bg-blue-50 text-blue-700 border border-blue-100 rounded text-xs font-mono">
{cove}
</span>
))}
</div>
</div>
)}
</>
)} )}
</div> </div>
{/* Footer */} {/* Footer */}
<div className={`px-6 py-4 flex gap-3 ${mostrarBotonProcesar ? 'justify-between' : 'justify-end'} border-t border-gray-100 bg-gray-50`}> <div className={`px-6 py-4 flex-shrink-0 flex gap-3 ${mostrarBotonProcesar ? 'justify-between' : 'justify-end'} border-t border-gray-100 bg-gray-50`}>
{mostrarBotonProcesar && ( {mostrarBotonProcesar && (
<button <button
onClick={onIniciarProcesamiento} onClick={onIniciarProcesamiento}
@@ -500,6 +699,8 @@ function GlobalAuditModal({ modal, onClose, onConsultar, consultando, onIniciarP
function Auditor() { function Auditor() {
const { showMessage } = useNotification(); const { showMessage } = useNotification();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingTable, setLoadingTable] = useState(false);
const isFirstLoad = useRef(true);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [pedimentos, setPedimentos] = useState([]); const [pedimentos, setPedimentos] = useState([]);
const [showInstructions, setShowInstructions] = useState(false); const [showInstructions, setShowInstructions] = useState(false);
@@ -578,17 +779,28 @@ function Auditor() {
const handleIniciarProcesamiento = async () => { const handleIniciarProcesamiento = async () => {
if (!auditResultModal || iniciandoProcesamiento) return; if (!auditResultModal || iniciandoProcesamiento) return;
const { tipo, pedimento_id } = auditResultModal; const { tipo, pedimento_id } = auditResultModal;
const urlPath = PROCESAMIENTO_URL_MAP[tipo];
if (!urlPath) {
showMessage('No hay endpoint de procesamiento disponible para este tipo', 'info');
return;
}
try { try {
setIniciandoProcesamiento(true); setIniciandoProcesamiento(true);
const response = await postWithAuth(`${API_URL}/customs/pedimentos/${pedimento_id}/${urlPath}/`);
if (!response.ok) { if (tipo === 'pc') {
throw new Error(await extractApiError(response)); const response = await postWithAuth(`${API_URL}/customs/auditor/procesar-pedimento-completo/pedimento/`, {
pedimento_id,
});
if (!response.ok) throw new Error(await extractApiError(response));
const pcData = await response.json();
// Mantener el modal abierto y agregar el resultado del diagnóstico
setAuditResultModal(prev => ({ ...prev, data: { ...prev.data, pc_resultado_procesamiento: pcData } }));
if (pcData.estado === 'encolado') showMessage(pcData.mensaje || 'Procesamiento encolado', 'success');
return;
} }
const urlPath = PROCESAMIENTO_URL_MAP[tipo];
if (!urlPath) {
showMessage('No hay endpoint de procesamiento disponible para este tipo', 'info');
return;
}
const response = await postWithAuth(`${API_URL}/customs/pedimentos/${pedimento_id}/${urlPath}/`);
if (!response.ok) throw new Error(await extractApiError(response));
const data = await response.json(); const data = await response.json();
showMessage(data.status || 'Procesamiento iniciado correctamente', 'success'); showMessage(data.status || 'Procesamiento iniciado correctamente', 'success');
setAuditResultModal(null); setAuditResultModal(null);
@@ -1602,7 +1814,11 @@ function formatXml(xml) {
useEffect(() => { useEffect(() => {
const fetchPedimentos = async () => { const fetchPedimentos = async () => {
setLoading(true); if (isFirstLoad.current) {
setLoading(true);
} else {
setLoadingTable(true);
}
try { try {
const queryParams = new URLSearchParams({ const queryParams = new URLSearchParams({
page: page.toString(), page: page.toString(),
@@ -1618,14 +1834,14 @@ function formatXml(xml) {
setError(err.message); setError(err.message);
} finally { } finally {
setLoading(false); setLoading(false);
setLoadingTable(false);
isFirstLoad.current = false;
} }
}; };
// Aplicar debounce al fetchPedimentos // Debounce largo solo cuando hay filtro activo; cambios de página son inmediatos
const timeoutId = setTimeout(() => { const delay = pedimentoFilter ? 1000 : 0;
fetchPedimentos(); const timeoutId = setTimeout(fetchPedimentos, delay);
}, 300);
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
}, [page, itemsPerPage, pedimentoFilter]); }, [page, itemsPerPage, pedimentoFilter]);
@@ -1897,7 +2113,7 @@ function formatXml(xml) {
name="pedimento" name="pedimento"
id="pedimento" id="pedimento"
value={pedimentoFilter} value={pedimentoFilter}
onChange={(e) => setPedimentoFilter(e.target.value)} onChange={(e) => { setPedimentoFilter(e.target.value); setPage(1); }}
className="block w-full py-2 pl-3 pr-10 border-gray-300 rounded-md focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" className="block w-full py-2 pl-3 pr-10 border-gray-300 rounded-md focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="Buscar por pedimento..." placeholder="Buscar por pedimento..."
/> />
@@ -1950,7 +2166,16 @@ function formatXml(xml) {
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{pedimentos.map((pedimento) => ( {loadingTable ? (
<tr>
<td colSpan="99" className="py-10 text-center">
<div className="inline-flex flex-col items-center gap-2 text-gray-500">
<div className="w-8 h-8 border-b-2 border-blue-500 rounded-full animate-spin"></div>
<span className="text-xs">Buscando...</span>
</div>
</td>
</tr>
) : pedimentos.map((pedimento) => (
<tr key={pedimento.id} className="hover:bg-gray-50"> <tr key={pedimento.id} className="hover:bg-gray-50">
<td className="py-4 pl-4 pr-3 text-sm font-medium text-gray-900 whitespace-nowrap"> <td className="py-4 pl-4 pr-3 text-sm font-medium text-gray-900 whitespace-nowrap">
<Link to={`/expedientes/pedimento/${pedimento.id}`} className='hover:text-blue-500 hover:text-bold hover:text-underline'>{pedimento.pedimento_app}</Link> <Link to={`/expedientes/pedimento/${pedimento.id}`} className='hover:text-blue-500 hover:text-bold hover:text-underline'>{pedimento.pedimento_app}</Link>
@@ -1968,16 +2193,6 @@ function formatXml(xml) {
<path d="M8 5v14l11-7z" /> <path d="M8 5v14l11-7z" />
</svg> </svg>
</button> </button>
{/* nuevo botón “view” */}
<button className="inline-flex items-center justify-center w-8 h-8 transition-all duration-200 bg-white border border-gray-200 rounded-full shadow-sm hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 hover:scale-110"
onClick={() => handleModalPeticionRespuesta(pedimento,"pc")}
title='Ver Petición y Respuesta de Pedimento Completo'
>
<svg className="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</button>
</div> </div>
</td> </td>
{/* RM - Remesas */} {/* RM - Remesas */}

View File

@@ -22,7 +22,7 @@ import { fetchPedimentoCompleto} from '../api/pedimentoCompleto';
import { fetchWithAuth, postWithAuth, putWithAuth, postFormDataWithAuth } from '../fetchWithAuth'; import { fetchWithAuth, postWithAuth, putWithAuth, postFormDataWithAuth } from '../fetchWithAuth';
import { fetchTasks } from '../api/procesos.ts'; import { fetchTasks } from '../api/procesos.ts';
import { fetchPedimentoCoves, downloadCove, downloadAcuseCove } from '../api/coves'; import { fetchPedimentoCoves, downloadCove, downloadAcuseCove } from '../api/coves';
import { fetchPedimentoEdocuments, downloadEdocument, downloadAcuseEdocument } from '../api/edocuments'; import { fetchPedimentoEdocuments, downloadEdocument, downloadAcuseEdocument, resetAcuseEdocument } from '../api/edocuments';
import { getTaskStatusLabel, getTaskStatusColor, isTaskActionable, isTaskFinal } from '../api/taskStatus'; import { getTaskStatusLabel, getTaskStatusColor, isTaskActionable, isTaskFinal } from '../api/taskStatus';
import { useParams, Link } from 'react-router-dom'; import { useParams, Link } from 'react-router-dom';
import { useNotification } from '../context/NotificationContext'; import { useNotification } from '../context/NotificationContext';
@@ -338,6 +338,10 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
const [processingAcuseEdoc, setProcessingAcuseEdoc] = useState(null); const [processingAcuseEdoc, setProcessingAcuseEdoc] = useState(null);
// Modal de advertencia por documentos con errores en EDocs // Modal de advertencia por documentos con errores en EDocs
const [edocErrorModal, setEdocErrorModal] = useState({ open: false, edoc: null, tipo: null }); const [edocErrorModal, setEdocErrorModal] = useState({ open: false, edoc: null, tipo: null });
// Modal de advertencia por documentos con errores en COVEs
const [coveErrorModal, setCoveErrorModal] = useState({ open: false, cove: null, tipo: null });
// Modal de confirmación para reprocesar partidas ya descargadas
const [partidaModal, setPartidaModal] = useState({ open: false, partida: null });
// Agregar estado para el modal de documentos // Agregar estado para el modal de documentos
const [showDocumentsModal, setShowDocumentsModal] = useState(false); const [showDocumentsModal, setShowDocumentsModal] = useState(false);
@@ -346,8 +350,9 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
// Función para manejar la visualización de documentos COVE // Función para manejar la visualización de documentos COVE
const handleShowCoveDocuments = (cove) => { const handleShowCoveDocuments = (cove) => {
if (cove.documentos && cove.documentos.length > 0) { const buenos = (cove.documentos || []).filter(d => d.document_type >= 1 && d.document_type <= 9);
setSelectedVUDocuments(cove.documentos); if (buenos.length > 0) {
setSelectedVUDocuments(buenos);
setSelectedVUNumber(cove.numero_cove); setSelectedVUNumber(cove.numero_cove);
setShowDocumentsModal(true); setShowDocumentsModal(true);
} }
@@ -364,8 +369,9 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
// Función para manejar la visualización de Edocuments // Función para manejar la visualización de Edocuments
const handleShowEDocuments = (edocument) => { const handleShowEDocuments = (edocument) => {
if (edocument.documentos && edocument.documentos.length > 0) { const buenos = (edocument.documentos || []).filter(d => d.document_type >= 1 && d.document_type <= 9);
setSelectedVUDocuments(edocument.documentos); if (buenos.length > 0) {
setSelectedVUDocuments(buenos);
setSelectedVUNumber(edocument.numero_edocument); setSelectedVUNumber(edocument.numero_edocument);
setShowDocumentsModal(true); setShowDocumentsModal(true);
} }
@@ -2297,6 +2303,20 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
} }
}; };
// Restablece acuse_descargado=False (crea doc error tipo 26 para Errores VU)
// y lanza inmediatamente el reprocesamiento del acuse.
const handleResetAcuse = async (edoc) => {
try {
await resetAcuseEdocument(edoc.id);
showMessage(`Restableciendo acuse de ${edoc.numero_edocument}. Iniciando reprocesamiento...`, 'info');
await handleAcuseEdocProcess(edoc);
fetchEdocs(id, edocsPage, edocsPageSize, edocsFilters);
} catch (error) {
showMessage(`Error al restablecer el acuse: ${error.message}`, 'error');
fetchEdocs(id, edocsPage, edocsPageSize, edocsFilters);
}
};
const handleEdocRequest = async (edoc) => { const handleEdocRequest = async (edoc) => {
console.log('Request edoc:', edoc); console.log('Request edoc:', edoc);
showMessage(`Procesando petición para EDocs #${edoc.numero_edocument}...`, 'info'); showMessage(`Procesando petición para EDocs #${edoc.numero_edocument}...`, 'info');
@@ -4754,16 +4774,24 @@ useEffect(() => {
<div className="flex items-center justify-end space-x-2"> <div className="flex items-center justify-end space-x-2">
{/* Botón Petición (solo activo si está pendiente) */} {/* Botón Petición (solo activo si está pendiente) */}
<button <button
onClick={() => handlePartidaRequest(partida)} onClick={() => {
disabled={partida.descargado || processingPartida === partida.id} if (partida.descargado) {
setPartidaModal({ open: true, partida });
} else {
handlePartidaRequest(partida);
}
}}
disabled={processingPartida === partida.id}
className={`p-1 rounded transition-colors ${ className={`p-1 rounded transition-colors ${
partida.descargado || processingPartida === partida.id processingPartida === partida.id
? 'text-gray-400 cursor-not-allowed' ? 'text-gray-400 cursor-not-allowed'
: partida.descargado
? 'text-purple-400 hover:text-purple-600'
: 'text-purple-600 hover:text-purple-900' : 'text-purple-600 hover:text-purple-900'
}`} }`}
title={ title={
processingPartida === partida.id ? 'Procesando partida...' : processingPartida === partida.id ? 'Procesando partida...' :
partida.descargado ? 'No disponible - Ya descargado' : partida.descargado ? 'Partida ya descargada — haz clic para forzar reprocesamiento' :
'Procesar petición' 'Procesar petición'
} }
> >
@@ -5145,16 +5173,26 @@ useEffect(() => {
<div className="flex items-center justify-end space-x-2"> <div className="flex items-center justify-end space-x-2">
{/* Botón COVE */} {/* Botón COVE */}
<button <button
onClick={() => handleCoveProcess(cove)} onClick={() => {
disabled={cove.cove_descargado || processingCove === cove.id} if (cove.cove_descargado) {
setCoveErrorModal({ open: true, cove, tipo: 'ya_descargado_cove' });
} else if (cove.documentos?.some(d => d.document_type === 20)) {
setCoveErrorModal({ open: true, cove, tipo: 'cove' });
} else {
handleCoveProcess(cove);
}
}}
disabled={processingCove === cove.id}
className={`p-1 rounded transition-colors ${ className={`p-1 rounded transition-colors ${
cove.cove_descargado processingCove === cove.id
? 'text-gray-400 cursor-not-allowed'
: processingCove === cove.id
? 'text-blue-400 cursor-not-allowed' ? 'text-blue-400 cursor-not-allowed'
: cove.cove_descargado
? 'text-gray-400 hover:text-gray-600'
: cove.documentos?.some(d => d.document_type === 20)
? 'text-yellow-500 hover:text-yellow-700 opacity-60'
: 'text-blue-600 hover:text-blue-900' : 'text-blue-600 hover:text-blue-900'
}`} }`}
title={cove.cove_descargado ? 'COVE ya descargado' : processingCove === cove.id ? 'Procesando COVE...' : 'Procesar COVE'} title={processingCove === cove.id ? 'Procesando COVE...' : cove.cove_descargado ? 'COVE ya descargado — haz clic para forzar reprocesamiento' : cove.documentos?.some(d => d.document_type === 20) ? 'COVE con errores — haz clic para más información' : 'Procesar COVE'}
> >
{processingCove === cove.id ? ( {processingCove === cove.id ? (
<svg className="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -5169,16 +5207,26 @@ useEffect(() => {
{/* Botón Acuse de COVE */} {/* Botón Acuse de COVE */}
<button <button
onClick={() => handleAcuseCoveProcess(cove)} onClick={() => {
disabled={cove.acuse_cove_descargado || processingAcuseCove === cove.id} if (cove.acuse_cove_descargado) {
setCoveErrorModal({ open: true, cove, tipo: 'ya_descargado_acuse_cove' });
} else if (cove.documentos?.some(d => d.document_type === 24)) {
setCoveErrorModal({ open: true, cove, tipo: 'acuse' });
} else {
handleAcuseCoveProcess(cove);
}
}}
disabled={processingAcuseCove === cove.id}
className={`p-1 rounded transition-colors ${ className={`p-1 rounded transition-colors ${
cove.acuse_cove_descargado processingAcuseCove === cove.id
? 'text-gray-400 cursor-not-allowed'
: processingAcuseCove === cove.id
? 'text-green-400 cursor-not-allowed' ? 'text-green-400 cursor-not-allowed'
: cove.acuse_cove_descargado
? 'text-gray-400 hover:text-gray-600'
: cove.documentos?.some(d => d.document_type === 24)
? 'text-yellow-500 hover:text-yellow-700 opacity-60'
: 'text-green-600 hover:text-green-900' : 'text-green-600 hover:text-green-900'
}`} }`}
title={cove.acuse_cove_descargado ? 'Acuse de COVE ya descargado' : processingAcuseCove === cove.id ? 'Procesando Acuse de COVE...' : 'Procesar Acuse de COVE'} title={processingAcuseCove === cove.id ? 'Procesando Acuse de COVE...' : cove.acuse_cove_descargado ? 'Acuse de COVE ya descargado — haz clic para forzar reprocesamiento' : cove.documentos?.some(d => d.document_type === 24) ? 'Acuse con errores — haz clic para más información' : 'Procesar Acuse de COVE'}
> >
{processingAcuseCove === cove.id ? ( {processingAcuseCove === cove.id ? (
<svg className="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -5647,24 +5695,33 @@ useEffect(() => {
{/* Botón EDoc */} {/* Botón EDoc */}
<button <button
onClick={() => { onClick={() => {
const tieneError = !edoc.edocument_descargado && edoc.documentos?.some(d => d.document_type === 22); if (edoc.edocument_descargado) {
if (tieneError) { setEdocErrorModal({ open: true, edoc, tipo: 'ya_descargado_edoc' });
} else if (!edoc.edocument_descargado && edoc.documentos?.some(d => d.document_type === 22)) {
setEdocErrorModal({ open: true, edoc, tipo: 'edoc' }); setEdocErrorModal({ open: true, edoc, tipo: 'edoc' });
} else { } else {
handleEdocProcess(edoc); handleEdocProcess(edoc);
} }
}} }}
disabled={edoc.edocument_descargado || processingEdoc === edoc.id} disabled={processingEdoc === edoc.id}
className={`p-1 rounded transition-colors ${ className={`p-1 rounded transition-colors ${
edoc.edocument_descargado processingEdoc === edoc.id
? 'text-gray-400 cursor-not-allowed'
: processingEdoc === edoc.id
? 'text-blue-400 cursor-not-allowed' ? 'text-blue-400 cursor-not-allowed'
: edoc.edocument_descargado
? 'text-gray-400 hover:text-gray-600'
: !edoc.edocument_descargado && edoc.documentos?.some(d => d.document_type === 22) : !edoc.edocument_descargado && edoc.documentos?.some(d => d.document_type === 22)
? 'text-yellow-500 hover:text-yellow-700 opacity-60' ? 'text-yellow-500 hover:text-yellow-700 opacity-60'
: 'text-blue-600 hover:text-blue-900' : 'text-blue-600 hover:text-blue-900'
}`} }`}
title={edoc.edocument_descargado ? 'EDoc ya descargado' : processingEdoc === edoc.id ? 'Procesando EDoc...' : !edoc.edocument_descargado && edoc.documentos?.some(d => d.document_type === 22) ? 'EDoc con errores — haz clic para más información' : 'Procesar EDoc'} title={
processingEdoc === edoc.id
? 'Procesando EDoc...'
: edoc.edocument_descargado
? 'EDoc ya descargado — haz clic para forzar reprocesamiento'
: !edoc.edocument_descargado && edoc.documentos?.some(d => d.document_type === 22)
? 'EDoc con errores — haz clic para más información'
: 'Procesar EDoc'
}
> >
{processingEdoc === edoc.id ? ( {processingEdoc === edoc.id ? (
<svg className="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -5678,37 +5735,69 @@ useEffect(() => {
</button> </button>
{/* Botón Acuse de EDoc */} {/* Botón Acuse de EDoc */}
<button {(() => {
onClick={() => { // Tipo 4 = Pedimento Acuse (acuse PDF válido descargado)
const tieneError = !edoc.acuse_descargado && edoc.documentos?.some(d => d.document_type === 26); const tieneAcusePdf = edoc.documentos?.some(d => d.document_type === 4);
if (tieneError) { // Inconsistencia: marcado como descargado pero sin PDF de acuse
const acuseConInconsistencia = edoc.acuse_descargado && !tieneAcusePdf;
// Correcto: descargado y con PDF disponible (gris pero clickeable para forzar)
const acuseOk = edoc.acuse_descargado && tieneAcusePdf;
// Error previo: no descargado pero hay doc de error tipo 26
const tieneErrorPrevio = !edoc.acuse_descargado && edoc.documentos?.some(d => d.document_type === 26);
const handleClick = () => {
if (acuseConInconsistencia) {
setEdocErrorModal({ open: true, edoc, tipo: 'acuse_inconsistencia' });
} else if (acuseOk) {
setEdocErrorModal({ open: true, edoc, tipo: 'ya_descargado_acuse' });
} else if (tieneErrorPrevio) {
setEdocErrorModal({ open: true, edoc, tipo: 'acuse' }); setEdocErrorModal({ open: true, edoc, tipo: 'acuse' });
} else { } else {
handleAcuseEdocProcess(edoc); handleAcuseEdocProcess(edoc);
} }
}} };
disabled={edoc.acuse_descargado || processingAcuseEdoc === edoc.id}
className={`p-1 rounded transition-colors ${ const btnClass = `p-1 rounded transition-colors ${
edoc.acuse_descargado processingAcuseEdoc === edoc.id
? 'text-gray-400 cursor-not-allowed'
: processingAcuseEdoc === edoc.id
? 'text-green-400 cursor-not-allowed' ? 'text-green-400 cursor-not-allowed'
: !edoc.acuse_descargado && edoc.documentos?.some(d => d.document_type === 26) : acuseOk
? 'text-gray-400 hover:text-gray-600'
: acuseConInconsistencia
? 'text-red-500 hover:text-red-700'
: tieneErrorPrevio
? 'text-yellow-500 hover:text-yellow-700 opacity-60' ? 'text-yellow-500 hover:text-yellow-700 opacity-60'
: 'text-green-600 hover:text-green-900' : 'text-green-600 hover:text-green-900'
}`} }`;
title={edoc.acuse_descargado ? 'Acuse de EDoc ya descargado' : processingAcuseEdoc === edoc.id ? 'Procesando Acuse de EDoc...' : !edoc.acuse_descargado && edoc.documentos?.some(d => d.document_type === 26) ? 'Acuse con errores — haz clic para más información' : 'Procesar Acuse de EDoc'}
> const btnTitle = processingAcuseEdoc === edoc.id
{processingAcuseEdoc === edoc.id ? ( ? 'Procesando Acuse de EDoc...'
<svg className="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24"> : acuseOk
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> ? 'Acuse ya descargado — haz clic para forzar reprocesamiento'
</svg> : acuseConInconsistencia
) : ( ? 'Inconsistencia: acuse marcado como descargado pero el documento no está disponible — haz clic para más información'
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> : tieneErrorPrevio
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> ? 'Acuse con errores — haz clic para más información'
</svg> : 'Procesar Acuse de EDoc';
)}
</button> return (
<button
onClick={handleClick}
disabled={processingAcuseEdoc === edoc.id}
className={btnClass}
title={btnTitle}
>
{processingAcuseEdoc === edoc.id ? (
<svg className="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
) : (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)}
</button>
);
})()}
{/* Botón para cargar documentos al cove */} {/* Botón para cargar documentos al cove */}
<button <button
@@ -7428,89 +7517,346 @@ useEffect(() => {
{/* Modal de advertencia: EDoc/Acuse con documentos de error */} {/* Modal de advertencia: EDoc/Acuse con documentos de error */}
{edocErrorModal.open && edocErrorModal.edoc && ( {edocErrorModal.open && edocErrorModal.edoc && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 animate-fade-in">
<div className="w-full max-w-lg p-6 bg-white rounded-lg shadow-xl animate-modal-slide-up">
{edocErrorModal.tipo === 'acuse_inconsistencia' ? (
/* Caso inconsistencia: acuse_descargado=True pero PDF no disponible */
<>
<div className="flex items-center mb-4 space-x-3">
<div className="flex items-center justify-center w-10 h-10 bg-red-100 rounded-full shrink-0">
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">Error en acuse</h3>
</div>
<p className="mb-1 text-sm text-gray-700">
No fue posible visualizar el acuse del EDocument{' '}
<span className="font-medium">{edocErrorModal.edoc.numero_edocument}</span>.
El estado indica <span className="font-medium">Descargado</span> pero el documento no se encuentra disponible.
</p>
<p className="mb-4 text-sm text-gray-500">
Revise la pestaña <span className="font-semibold text-red-600">Errores VU</span> para consultar el detalle del problema.
Al aceptar, el estado se restablecerá para que puedas reintentar el procesamiento.
</p>
<div className="flex justify-end space-x-2">
<button
onClick={() => setEdocErrorModal({ open: false, edoc: null, tipo: null })}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
>
Cancelar
</button>
<button
onClick={() => {
const { edoc } = edocErrorModal;
setEdocErrorModal({ open: false, edoc: null, tipo: null });
handleResetAcuse(edoc);
}}
className="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700"
>
Aceptar y reprocesar
</button>
</div>
</>
) : edocErrorModal.tipo === 'ya_descargado_edoc' || edocErrorModal.tipo === 'ya_descargado_acuse' ? (
/* Caso: ya descargado correctamente, usuario quiere forzar reintento */
<>
<div className="flex items-center mb-4 space-x-3">
<div className="flex items-center justify-center w-10 h-10 bg-blue-100 rounded-full shrink-0">
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">
{edocErrorModal.tipo === 'ya_descargado_acuse' ? 'Acuse ya descargado' : 'EDoc ya descargado'}
</h3>
</div>
<p className="mb-1 text-sm text-gray-700">
Este documento (<span className="font-medium">{edocErrorModal.edoc.numero_edocument}</span>) ya está descargado.
</p>
<p className="mb-4 text-sm text-gray-500">
Si desea reintentar la descarga, tenga en cuenta que es <span className="font-semibold">bajo su responsabilidad</span>.
</p>
<div className="flex justify-end space-x-2">
<button
onClick={() => setEdocErrorModal({ open: false, edoc: null, tipo: null })}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
>
Cancelar
</button>
<button
onClick={() => {
const { edoc, tipo } = edocErrorModal;
setEdocErrorModal({ open: false, edoc: null, tipo: null });
if (tipo === 'ya_descargado_acuse') {
handleAcuseEdocProcess(edoc);
} else {
handleEdocProcess(edoc);
}
}}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700"
>
Reintentar descarga
</button>
</div>
</>
) : (
/* Caso normal: errores previos conocidos (tipo 22/26) */
<>
<div className="flex items-center mb-4 space-x-3">
<div className="flex items-center justify-center w-10 h-10 bg-yellow-100 rounded-full shrink-0">
<svg className="w-5 h-5 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">
{edocErrorModal.tipo === 'acuse' ? 'Acuse de EDoc con errores' : 'EDoc con errores'}
</h3>
</div>
<p className="mb-1 text-sm text-gray-700">
Este EDocument (<span className="font-medium">{edocErrorModal.edoc.numero_edocument}</span>) cuenta con errores en la respuesta recibida.
Revisa el documento de error antes de volver a intentarlo.
</p>
<p className="mb-4 text-sm text-gray-500">
Puedes continuar con el flujo de trabajo, pero se recomienda corregir el error primero.
</p>
{/* Lista de documentos de error */}
{(() => {
const tipoFiltro = edocErrorModal.tipo === 'acuse' ? 26 : 22;
const docsError = edocErrorModal.edoc.documentos?.filter(d => d.document_type === tipoFiltro) || [];
return docsError.length > 0 ? (
<div className="mb-4">
<p className="mb-2 text-xs font-medium tracking-wide text-gray-500 uppercase">Documentos de error</p>
<ul className="space-y-2">
{docsError.map(doc => {
const nombreArchivo = (doc.archivo || '').split('/').pop() || 'Documento';
return (
<li key={doc.id} className="flex items-center justify-between p-2 rounded-md bg-gray-50">
<span className="text-sm text-gray-700 truncate max-w-xs" title={nombreArchivo}>
{nombreArchivo}
</span>
<button
onClick={() => {
setEdocErrorModal({ open: false, edoc: null, tipo: null });
handlePreviewVU(doc);
}}
className="p-1 ml-2 text-blue-600 rounded shrink-0 hover:text-blue-900 hover:bg-blue-50"
title="Ver documento"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</button>
</li>
);
})}
</ul>
</div>
) : null;
})()}
<div className="flex justify-end space-x-2">
<button
onClick={() => setEdocErrorModal({ open: false, edoc: null, tipo: null })}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
>
Cancelar
</button>
<button
onClick={() => {
const { edoc, tipo } = edocErrorModal;
setEdocErrorModal({ open: false, edoc: null, tipo: null });
if (tipo === 'acuse') {
handleAcuseEdocProcess(edoc);
} else {
handleEdocProcess(edoc);
}
}}
className="px-4 py-2 text-sm font-medium text-white bg-yellow-600 rounded-md hover:bg-yellow-700"
>
Continuar de todas formas
</button>
</div>
</>
)}
</div>
</div>
)}
{/* Modal de advertencia: COVE/Acuse con documentos de error */}
{/* Modal de confirmación: Partida ya descargada */}
{partidaModal.open && partidaModal.partida && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 animate-fade-in"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 animate-fade-in">
<div className="w-full max-w-lg p-6 bg-white rounded-lg shadow-xl animate-modal-slide-up"> <div className="w-full max-w-lg p-6 bg-white rounded-lg shadow-xl animate-modal-slide-up">
<div className="flex items-center mb-4 space-x-3"> <div className="flex items-center mb-4 space-x-3">
<div className="flex items-center justify-center w-10 h-10 bg-yellow-100 rounded-full shrink-0"> <div className="flex items-center justify-center w-10 h-10 bg-blue-100 rounded-full shrink-0">
<svg className="w-5 h-5 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg> </svg>
</div> </div>
<h3 className="text-lg font-semibold text-gray-900"> <h3 className="text-lg font-semibold text-gray-900">Partida ya descargada</h3>
{edocErrorModal.tipo === 'acuse' ? 'Acuse de EDoc con errores' : 'EDoc con errores'}
</h3>
</div> </div>
<p className="mb-1 text-sm text-gray-700"> <p className="mb-1 text-sm text-gray-700">
Este EDocument (<span className="font-medium">{edocErrorModal.edoc.numero_edocument}</span>) cuenta con errores en la respuesta recibida. La partida <span className="font-medium">{partidaModal.partida.numero_partida}</span> ya está descargada.
Revisa el documento de error antes de volver a intentarlo.
</p> </p>
<p className="mb-4 text-sm text-gray-500"> <p className="mb-4 text-sm text-gray-500">
Puedes continuar con el flujo de trabajo, pero se recomienda corregir el error primero. Si desea reintentar la descarga, tenga en cuenta que es <span className="font-semibold">bajo su responsabilidad</span>.
</p> </p>
{/* Lista de documentos de error */}
{(() => {
const tipoFiltro = edocErrorModal.tipo === 'acuse' ? 26 : 22;
const docsError = edocErrorModal.edoc.documentos?.filter(d => d.document_type === tipoFiltro) || [];
return docsError.length > 0 ? (
<div className="mb-4">
<p className="mb-2 text-xs font-medium tracking-wide text-gray-500 uppercase">Documentos de error</p>
<ul className="space-y-2">
{docsError.map(doc => {
const nombreArchivo = (doc.archivo || '').split('/').pop() || 'Documento';
return (
<li key={doc.id} className="flex items-center justify-between p-2 rounded-md bg-gray-50">
<span className="text-sm text-gray-700 truncate max-w-xs" title={nombreArchivo}>
{nombreArchivo}
</span>
<button
onClick={() => {
setEdocErrorModal({ open: false, edoc: null, tipo: null });
handlePreviewVU(doc);
}}
className="p-1 ml-2 text-blue-600 rounded shrink-0 hover:text-blue-900 hover:bg-blue-50"
title="Ver documento"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</button>
</li>
);
})}
</ul>
</div>
) : null;
})()}
<div className="flex justify-end space-x-2"> <div className="flex justify-end space-x-2">
<button <button
onClick={() => setEdocErrorModal({ open: false, edoc: null, tipo: null })} onClick={() => setPartidaModal({ open: false, partida: null })}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50" className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
> >
Cancelar Cancelar
</button> </button>
<button <button
onClick={() => { onClick={() => {
const { edoc, tipo } = edocErrorModal; const { partida } = partidaModal;
setEdocErrorModal({ open: false, edoc: null, tipo: null }); setPartidaModal({ open: false, partida: null });
if (tipo === 'acuse') { handlePartidaRequest(partida);
handleAcuseEdocProcess(edoc);
} else {
handleEdocProcess(edoc);
}
}} }}
className="px-4 py-2 text-sm font-medium text-white bg-yellow-600 rounded-md hover:bg-yellow-700" className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700"
> >
Continuar de todas formas Reintentar descarga
</button> </button>
</div> </div>
</div> </div>
</div> </div>
)} )}
{coveErrorModal.open && coveErrorModal.cove && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 animate-fade-in">
<div className="w-full max-w-lg p-6 bg-white rounded-lg shadow-xl animate-modal-slide-up">
{coveErrorModal.tipo === 'ya_descargado_cove' || coveErrorModal.tipo === 'ya_descargado_acuse_cove' ? (
/* Caso: ya descargado correctamente, usuario quiere forzar reintento */
<>
<div className="flex items-center mb-4 space-x-3">
<div className="flex items-center justify-center w-10 h-10 bg-blue-100 rounded-full shrink-0">
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">
{coveErrorModal.tipo === 'ya_descargado_acuse_cove' ? 'Acuse de COVE ya descargado' : 'COVE ya descargado'}
</h3>
</div>
<p className="mb-1 text-sm text-gray-700">
Este documento (<span className="font-medium">{coveErrorModal.cove.numero_cove}</span>) ya está descargado.
</p>
<p className="mb-4 text-sm text-gray-500">
Si desea reintentar la descarga, tenga en cuenta que es <span className="font-semibold">bajo su responsabilidad</span>.
</p>
<div className="flex justify-end space-x-2">
<button
onClick={() => setCoveErrorModal({ open: false, cove: null, tipo: null })}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
>
Cancelar
</button>
<button
onClick={() => {
const { cove, tipo } = coveErrorModal;
setCoveErrorModal({ open: false, cove: null, tipo: null });
if (tipo === 'ya_descargado_acuse_cove') {
handleAcuseCoveProcess(cove);
} else {
handleCoveProcess(cove);
}
}}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700"
>
Reintentar descarga
</button>
</div>
</>
) : (
/* Caso normal: errores previos conocidos (tipo 20/24) */
<>
<div className="flex items-center mb-4 space-x-3">
<div className="flex items-center justify-center w-10 h-10 bg-yellow-100 rounded-full shrink-0">
<svg className="w-5 h-5 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">
{coveErrorModal.tipo === 'acuse' ? 'Acuse de COVE con errores' : 'COVE con errores'}
</h3>
</div>
<p className="mb-1 text-sm text-gray-700">
Este COVE (<span className="font-medium">{coveErrorModal.cove.numero_cove}</span>) cuenta con errores en la respuesta recibida.
Revisa el documento de error antes de volver a intentarlo.
</p>
<p className="mb-4 text-sm text-gray-500">
Puedes continuar con el flujo de trabajo, pero se recomienda corregir el error primero.
</p>
{/* Lista de documentos de error */}
{(() => {
const tipoFiltro = coveErrorModal.tipo === 'acuse' ? 24 : 20;
const docsError = coveErrorModal.cove.documentos?.filter(d => d.document_type === tipoFiltro) || [];
return docsError.length > 0 ? (
<div className="mb-4">
<p className="mb-2 text-xs font-medium tracking-wide text-gray-500 uppercase">Documentos de error</p>
<ul className="space-y-2">
{docsError.map(doc => {
const nombreArchivo = (doc.archivo || '').split('/').pop() || 'Documento';
return (
<li key={doc.id} className="flex items-center justify-between p-2 rounded-md bg-gray-50">
<span className="text-sm text-gray-700 truncate max-w-xs" title={nombreArchivo}>
{nombreArchivo}
</span>
<button
onClick={() => {
setCoveErrorModal({ open: false, cove: null, tipo: null });
handlePreviewVU(doc);
}}
className="p-1 ml-2 text-blue-600 rounded shrink-0 hover:text-blue-900 hover:bg-blue-50"
title="Ver documento"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</button>
</li>
);
})}
</ul>
</div>
) : null;
})()}
<div className="flex justify-end space-x-2">
<button
onClick={() => setCoveErrorModal({ open: false, cove: null, tipo: null })}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
>
Cancelar
</button>
<button
onClick={() => {
const { cove, tipo } = coveErrorModal;
setCoveErrorModal({ open: false, cove: null, tipo: null });
if (tipo === 'acuse') {
handleAcuseCoveProcess(cove);
} else {
handleCoveProcess(cove);
}
}}
className="px-4 py-2 text-sm font-medium text-white bg-yellow-600 rounded-md hover:bg-yellow-700"
>
Continuar de todas formas
</button>
</div>
</>
)}
</div>
</div>
)}
</div> </div>
); );
} }