fix/T2026-05-054-procesar-acuses #27
@@ -56,3 +56,12 @@ export const downloadAcuseEdocument = async (edocId) => {
|
||||
window.URL.revokeObjectURL(url);
|
||||
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();
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { getWithAuth, postWithAuth, fetchWithAuth } from '../fetchWithAuth';
|
||||
import { useNotification } from '../context/NotificationContext';
|
||||
@@ -34,15 +34,43 @@ const PROCESAMIENTO_URL_MAP = {
|
||||
|
||||
function AuditResultModal({ modal, iniciandoProcesamiento, onClose, onIniciarProcesamiento }) {
|
||||
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;
|
||||
// remesa sin remesas: no tiene nada que procesar, ocultar botón
|
||||
const sinNadaQueHacer = tipo === 'rm' && data?.tiene_remesas === false;
|
||||
|
||||
const esCompletado = tipo === 'pc'
|
||||
? (!data?.hay_pendientes && !data?.hay_errores)
|
||||
: (data?.estado === 'completado' || data?.auditoria_completa == true);
|
||||
|
||||
const sinNadaQueHacer = (tipo === 'rm' && data?.tiene_remesas === false);
|
||||
const mostrarBotonProcesar = !esCompletado && !sinNadaQueHacer;
|
||||
|
||||
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">
|
||||
<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" />
|
||||
@@ -56,144 +84,315 @@ function AuditResultModal({ modal, iniciandoProcesamiento, onClose, onIniciarPro
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
<span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-sm font-semibold ${
|
||||
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>
|
||||
{badge}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cuerpo */}
|
||||
<div className="px-6 py-5 space-y-4">
|
||||
{/* Cuerpo scrolleable */}
|
||||
<div className="px-6 py-5 space-y-4 overflow-y-auto">
|
||||
{iconoEstado}
|
||||
|
||||
{/* Mensaje principal */}
|
||||
<p className="text-center text-gray-700 font-medium">{data?.mensaje}</p>
|
||||
|
||||
{/* Resumen numérico */}
|
||||
{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 — 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>
|
||||
{tipo === 'pc' ? (
|
||||
<>
|
||||
{/* Información del XML */}
|
||||
{data?.informacion_xml && (
|
||||
<div className="rounded-xl border border-gray-100 overflow-hidden text-xs">
|
||||
<div className="px-4 py-2.5 bg-gray-50 border-b border-gray-100">
|
||||
<p className="text-sm font-semibold text-gray-700">Información del pedimento</p>
|
||||
</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 className="divide-y divide-gray-50">
|
||||
{[
|
||||
{ label: 'Contribuyente', value: `${data.informacion_xml.contribuyente_rfc} — ${data.informacion_xml.contribuyente_nombre}` },
|
||||
{ label: 'Tipo operación', value: `${data.informacion_xml.clave_pedimento} · ${data.informacion_xml.tipo_operacion}` },
|
||||
// { label: 'Aduana', value: data.informacion_xml.aduana_clave },
|
||||
// { label: 'Partidas', value: data.informacion_xml.numero_partidas },
|
||||
{ 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>
|
||||
)}
|
||||
|
||||
{/* Advertencia si auditoría incompleta */}
|
||||
{!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"/>
|
||||
{/* Diagnóstico del PC cuando no está descargado */}
|
||||
{!data?.pc_descargado && (
|
||||
<div className="rounded-xl border border-amber-100 overflow-hidden text-xs">
|
||||
<div className="px-4 py-2.5 bg-amber-50 border-b border-amber-100 flex items-center justify-between">
|
||||
<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>
|
||||
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>
|
||||
)}
|
||||
</details>
|
||||
)}
|
||||
|
||||
{/* Detalle de XMLs con error */}
|
||||
{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">
|
||||
{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>
|
||||
{/* Nota de verificación manual cuando hay errores */}
|
||||
{data?.hay_errores && (
|
||||
<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">
|
||||
<svg className="w-4 h-4 flex-shrink-0 mt-0.5 text-red-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>Se detectaron errores en las respuestas de VUCEM. Revisa y corrige los documentos de error antes de reintentar el procesamiento.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Resultado del procesamiento PC */}
|
||||
{data?.pc_resultado_procesamiento && (() => {
|
||||
const r = data.pc_resultado_procesamiento;
|
||||
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>
|
||||
</details>
|
||||
)}
|
||||
|
||||
{/* Todo bien */}
|
||||
{conError.length === 0 && auditOk && (
|
||||
<p className="text-center text-sm text-green-700 font-medium">Todos los XMLs analizados están correctos</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{/* Razones por las que no puede procesar */}
|
||||
{isNoPuede && r.validacion?.razones_no_puede_procesar?.length > 0 && (
|
||||
<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) => (
|
||||
<p key={i} className="text-red-700 font-medium">• {razon}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pendientes */}
|
||||
{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>
|
||||
{/* Task ID si fue encolado */}
|
||||
{isEncolado && r.task_id && (
|
||||
<div className="flex items-center gap-2 px-4 py-2 bg-green-50 border-t border-green-100">
|
||||
<span className="text-gray-500">Task ID:</span>
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
</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>
|
||||
{/* Sin pendientes ni errores */}
|
||||
{data?.pc_descargado && !data?.hay_errores && (
|
||||
<p className="text-center text-sm text-green-700 font-medium">Pedimento completo descargado sin errores</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Mensaje principal */}
|
||||
<p className="text-center text-gray-700 font-medium">{data?.mensaje}</p>
|
||||
|
||||
{/* Resumen numérico */}
|
||||
{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>
|
||||
|
||||
{/* 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 && (
|
||||
<button
|
||||
onClick={onIniciarProcesamiento}
|
||||
@@ -500,6 +699,8 @@ function GlobalAuditModal({ modal, onClose, onConsultar, consultando, onIniciarP
|
||||
function Auditor() {
|
||||
const { showMessage } = useNotification();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingTable, setLoadingTable] = useState(false);
|
||||
const isFirstLoad = useRef(true);
|
||||
const [error, setError] = useState('');
|
||||
const [pedimentos, setPedimentos] = useState([]);
|
||||
const [showInstructions, setShowInstructions] = useState(false);
|
||||
@@ -578,17 +779,28 @@ function Auditor() {
|
||||
const handleIniciarProcesamiento = async () => {
|
||||
if (!auditResultModal || iniciandoProcesamiento) return;
|
||||
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 {
|
||||
setIniciandoProcesamiento(true);
|
||||
const response = await postWithAuth(`${API_URL}/customs/pedimentos/${pedimento_id}/${urlPath}/`);
|
||||
if (!response.ok) {
|
||||
throw new Error(await extractApiError(response));
|
||||
|
||||
if (tipo === 'pc') {
|
||||
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();
|
||||
showMessage(data.status || 'Procesamiento iniciado correctamente', 'success');
|
||||
setAuditResultModal(null);
|
||||
@@ -1602,7 +1814,11 @@ function formatXml(xml) {
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPedimentos = async () => {
|
||||
setLoading(true);
|
||||
if (isFirstLoad.current) {
|
||||
setLoading(true);
|
||||
} else {
|
||||
setLoadingTable(true);
|
||||
}
|
||||
try {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
@@ -1618,14 +1834,14 @@ function formatXml(xml) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoadingTable(false);
|
||||
isFirstLoad.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Aplicar debounce al fetchPedimentos
|
||||
const timeoutId = setTimeout(() => {
|
||||
fetchPedimentos();
|
||||
}, 300);
|
||||
|
||||
// Debounce largo solo cuando hay filtro activo; cambios de página son inmediatos
|
||||
const delay = pedimentoFilter ? 1000 : 0;
|
||||
const timeoutId = setTimeout(fetchPedimentos, delay);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [page, itemsPerPage, pedimentoFilter]);
|
||||
|
||||
@@ -1897,7 +2113,7 @@ function formatXml(xml) {
|
||||
name="pedimento"
|
||||
id="pedimento"
|
||||
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"
|
||||
placeholder="Buscar por pedimento..."
|
||||
/>
|
||||
@@ -1950,7 +2166,16 @@ function formatXml(xml) {
|
||||
</tr>
|
||||
</thead>
|
||||
<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">
|
||||
<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>
|
||||
@@ -1968,16 +2193,6 @@ function formatXml(xml) {
|
||||
<path d="M8 5v14l11-7z" />
|
||||
</svg>
|
||||
</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>
|
||||
</td>
|
||||
{/* RM - Remesas */}
|
||||
|
||||
@@ -22,7 +22,7 @@ import { fetchPedimentoCompleto} from '../api/pedimentoCompleto';
|
||||
import { fetchWithAuth, postWithAuth, putWithAuth, postFormDataWithAuth } from '../fetchWithAuth';
|
||||
import { fetchTasks } from '../api/procesos.ts';
|
||||
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 { useParams, Link } from 'react-router-dom';
|
||||
import { useNotification } from '../context/NotificationContext';
|
||||
@@ -338,6 +338,10 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
|
||||
const [processingAcuseEdoc, setProcessingAcuseEdoc] = useState(null);
|
||||
// Modal de advertencia por documentos con errores en EDocs
|
||||
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
|
||||
const [showDocumentsModal, setShowDocumentsModal] = useState(false);
|
||||
@@ -346,8 +350,9 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
|
||||
|
||||
// Función para manejar la visualización de documentos COVE
|
||||
const handleShowCoveDocuments = (cove) => {
|
||||
if (cove.documentos && cove.documentos.length > 0) {
|
||||
setSelectedVUDocuments(cove.documentos);
|
||||
const buenos = (cove.documentos || []).filter(d => d.document_type >= 1 && d.document_type <= 9);
|
||||
if (buenos.length > 0) {
|
||||
setSelectedVUDocuments(buenos);
|
||||
setSelectedVUNumber(cove.numero_cove);
|
||||
setShowDocumentsModal(true);
|
||||
}
|
||||
@@ -364,8 +369,9 @@ const handleDeleteSelectedPedimentoDocuments = async () => {
|
||||
|
||||
// Función para manejar la visualización de Edocuments
|
||||
const handleShowEDocuments = (edocument) => {
|
||||
if (edocument.documentos && edocument.documentos.length > 0) {
|
||||
setSelectedVUDocuments(edocument.documentos);
|
||||
const buenos = (edocument.documentos || []).filter(d => d.document_type >= 1 && d.document_type <= 9);
|
||||
if (buenos.length > 0) {
|
||||
setSelectedVUDocuments(buenos);
|
||||
setSelectedVUNumber(edocument.numero_edocument);
|
||||
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) => {
|
||||
console.log('Request edoc:', edoc);
|
||||
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">
|
||||
{/* Botón Petición (solo activo si está pendiente) */}
|
||||
<button
|
||||
onClick={() => handlePartidaRequest(partida)}
|
||||
disabled={partida.descargado || processingPartida === partida.id}
|
||||
onClick={() => {
|
||||
if (partida.descargado) {
|
||||
setPartidaModal({ open: true, partida });
|
||||
} else {
|
||||
handlePartidaRequest(partida);
|
||||
}
|
||||
}}
|
||||
disabled={processingPartida === partida.id}
|
||||
className={`p-1 rounded transition-colors ${
|
||||
partida.descargado || processingPartida === partida.id
|
||||
processingPartida === partida.id
|
||||
? 'text-gray-400 cursor-not-allowed'
|
||||
: partida.descargado
|
||||
? 'text-purple-400 hover:text-purple-600'
|
||||
: 'text-purple-600 hover:text-purple-900'
|
||||
}`}
|
||||
title={
|
||||
processingPartida === partida.id ? 'Procesando partida...' :
|
||||
partida.descargado ? 'No disponible - Ya descargado' :
|
||||
partida.descargado ? 'Partida ya descargada — haz clic para forzar reprocesamiento' :
|
||||
'Procesar petición'
|
||||
}
|
||||
>
|
||||
@@ -5145,16 +5173,26 @@ useEffect(() => {
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
{/* Botón COVE */}
|
||||
<button
|
||||
onClick={() => handleCoveProcess(cove)}
|
||||
disabled={cove.cove_descargado || processingCove === cove.id}
|
||||
onClick={() => {
|
||||
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 ${
|
||||
cove.cove_descargado
|
||||
? 'text-gray-400 cursor-not-allowed'
|
||||
: processingCove === cove.id
|
||||
processingCove === cove.id
|
||||
? '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'
|
||||
}`}
|
||||
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 ? (
|
||||
<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 */}
|
||||
<button
|
||||
onClick={() => handleAcuseCoveProcess(cove)}
|
||||
disabled={cove.acuse_cove_descargado || processingAcuseCove === cove.id}
|
||||
onClick={() => {
|
||||
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 ${
|
||||
cove.acuse_cove_descargado
|
||||
? 'text-gray-400 cursor-not-allowed'
|
||||
: processingAcuseCove === cove.id
|
||||
processingAcuseCove === cove.id
|
||||
? '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'
|
||||
}`}
|
||||
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 ? (
|
||||
<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 */}
|
||||
<button
|
||||
onClick={() => {
|
||||
const tieneError = !edoc.edocument_descargado && edoc.documentos?.some(d => d.document_type === 22);
|
||||
if (tieneError) {
|
||||
if (edoc.edocument_descargado) {
|
||||
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' });
|
||||
} else {
|
||||
handleEdocProcess(edoc);
|
||||
}
|
||||
}}
|
||||
disabled={edoc.edocument_descargado || processingEdoc === edoc.id}
|
||||
disabled={processingEdoc === edoc.id}
|
||||
className={`p-1 rounded transition-colors ${
|
||||
edoc.edocument_descargado
|
||||
? 'text-gray-400 cursor-not-allowed'
|
||||
: processingEdoc === edoc.id
|
||||
processingEdoc === edoc.id
|
||||
? '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)
|
||||
? 'text-yellow-500 hover:text-yellow-700 opacity-60'
|
||||
: '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 ? (
|
||||
<svg className="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -5678,37 +5735,69 @@ useEffect(() => {
|
||||
</button>
|
||||
|
||||
{/* Botón Acuse de EDoc */}
|
||||
<button
|
||||
onClick={() => {
|
||||
const tieneError = !edoc.acuse_descargado && edoc.documentos?.some(d => d.document_type === 26);
|
||||
if (tieneError) {
|
||||
{(() => {
|
||||
// Tipo 4 = Pedimento Acuse (acuse PDF válido descargado)
|
||||
const tieneAcusePdf = edoc.documentos?.some(d => d.document_type === 4);
|
||||
// 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' });
|
||||
} else {
|
||||
handleAcuseEdocProcess(edoc);
|
||||
}
|
||||
}}
|
||||
disabled={edoc.acuse_descargado || processingAcuseEdoc === edoc.id}
|
||||
className={`p-1 rounded transition-colors ${
|
||||
edoc.acuse_descargado
|
||||
? 'text-gray-400 cursor-not-allowed'
|
||||
: processingAcuseEdoc === edoc.id
|
||||
};
|
||||
|
||||
const btnClass = `p-1 rounded transition-colors ${
|
||||
processingAcuseEdoc === edoc.id
|
||||
? '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-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'}
|
||||
>
|
||||
{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>
|
||||
}`;
|
||||
|
||||
const btnTitle = processingAcuseEdoc === edoc.id
|
||||
? 'Procesando Acuse de EDoc...'
|
||||
: acuseOk
|
||||
? 'Acuse ya descargado — haz clic para forzar reprocesamiento'
|
||||
: acuseConInconsistencia
|
||||
? 'Inconsistencia: acuse marcado como descargado pero el documento no está disponible — haz clic para más información'
|
||||
: tieneErrorPrevio
|
||||
? 'Acuse con errores — haz clic para más información'
|
||||
: 'Procesar Acuse de EDoc';
|
||||
|
||||
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 */}
|
||||
<button
|
||||
@@ -7428,89 +7517,346 @@ useEffect(() => {
|
||||
|
||||
{/* Modal de advertencia: EDoc/Acuse con documentos de error */}
|
||||
{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="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 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" />
|
||||
<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 === 'acuse' ? 'Acuse de EDoc con errores' : 'EDoc con errores'}
|
||||
</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-900">Partida ya descargada</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.
|
||||
La partida <span className="font-medium">{partidaModal.partida.numero_partida}</span> ya está descargada.
|
||||
</p>
|
||||
<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>
|
||||
|
||||
{/* 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 })}
|
||||
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"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const { edoc, tipo } = edocErrorModal;
|
||||
setEdocErrorModal({ open: false, edoc: null, tipo: null });
|
||||
if (tipo === 'acuse') {
|
||||
handleAcuseEdocProcess(edoc);
|
||||
} else {
|
||||
handleEdocProcess(edoc);
|
||||
}
|
||||
const { partida } = partidaModal;
|
||||
setPartidaModal({ open: false, partida: null });
|
||||
handlePartidaRequest(partida);
|
||||
}}
|
||||
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>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user