Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
717 lines
38 KiB
HTML
717 lines
38 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Envío Masivo{% endblock %}
|
|
{% block header %}Envío Masivo a TNS{% endblock %}
|
|
{% block content %}
|
|
<div class="max-w-4xl space-y-6">
|
|
|
|
<!-- ─── Panel de control ─────────────────────────────────────────────────── -->
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
<h3 class="font-semibold text-gray-800">
|
|
<i class="fas fa-paper-plane mr-2 text-blue-500"></i>Envío automático por fecha
|
|
</h3>
|
|
<p class="text-xs text-gray-500 mt-1">
|
|
Selecciona fecha y pasos activos para enviar a TNS.
|
|
</p>
|
|
</div>
|
|
<div class="p-6 space-y-4">
|
|
<!-- Inputs + Botones -->
|
|
<div class="flex items-end gap-3 flex-wrap">
|
|
<div class="flex-1 min-w-36">
|
|
<label class="block text-sm font-medium text-gray-700 mb-1">Fecha de atención</label>
|
|
<input type="date" id="fecha-input" value="{{ today }}"
|
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
</div>
|
|
<button onclick="previewAutomation()" id="btn-preview"
|
|
class="px-5 py-2 bg-gray-600 hover:bg-gray-700 text-white font-medium rounded-lg transition-colors flex items-center gap-2 whitespace-nowrap">
|
|
<i class="fas fa-eye"></i> Vista previa
|
|
</button>
|
|
<button onclick="abrirModalConfirmar()" id="btn-run" disabled
|
|
class="px-5 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors flex items-center gap-2 whitespace-nowrap disabled:opacity-40 disabled:cursor-not-allowed">
|
|
<i class="fas fa-play"></i> Ejecutar
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Paso chips -->
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<span class="text-xs text-gray-500 font-medium mr-1">Pasos:</span>
|
|
<button id="chip-terceros" onclick="togglePaso('terceros')"
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold border-2 transition-all border-blue-500 bg-blue-500 text-white">
|
|
<i class="fas fa-user"></i> Terceros
|
|
</button>
|
|
<button id="chip-rda" onclick="togglePaso('rda')"
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold border-2 transition-all border-purple-500 bg-purple-500 text-white">
|
|
<i class="fas fa-exchange-alt"></i> RDA Servicios
|
|
</button>
|
|
<button id="chip-preserv" onclick="togglePaso('preserv')"
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold border-2 transition-all border-orange-500 bg-orange-500 text-white">
|
|
<i class="fas fa-file-alt"></i> Pre-servicios
|
|
</button>
|
|
<button id="chip-ventas" onclick="togglePaso('ventas')"
|
|
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold border-2 transition-all border-emerald-500 bg-emerald-500 text-white">
|
|
<i class="fas fa-file-invoice-dollar"></i> Ventas
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ─── Vista previa ─────────────────────────────────────────────────────── -->
|
|
<div id="preview-section" class="hidden bg-white rounded-xl shadow-sm border border-gray-200">
|
|
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
|
<h3 class="font-semibold text-gray-800"><i class="fas fa-search mr-2 text-gray-500"></i>Resumen de la fecha</h3>
|
|
<span id="preview-fecha" class="text-sm text-gray-500"></span>
|
|
</div>
|
|
<div class="p-6 space-y-4">
|
|
<!-- Stat cards -->
|
|
<div class="grid grid-cols-5 gap-4">
|
|
<div id="card-terceros" class="bg-blue-50 rounded-lg p-4 text-center transition-opacity">
|
|
<div id="cnt-pacientes" class="text-3xl font-bold text-blue-600">0</div>
|
|
<div class="text-sm text-blue-700 mt-1">Pacientes</div>
|
|
</div>
|
|
<div id="card-rda" class="bg-purple-50 rounded-lg p-4 text-center transition-opacity">
|
|
<div id="cnt-recepciones" class="text-3xl font-bold text-purple-600">0</div>
|
|
<div class="text-sm text-purple-700 mt-1">Recepciones</div>
|
|
</div>
|
|
<div id="card-examenes" class="bg-green-50 rounded-lg p-4 text-center transition-opacity">
|
|
<div id="cnt-examenes" class="text-3xl font-bold text-green-600">0</div>
|
|
<div class="text-sm text-green-700 mt-1">Exámenes</div>
|
|
</div>
|
|
<div id="card-preserv" class="bg-orange-50 rounded-lg p-4 text-center transition-opacity">
|
|
<div id="cnt-preservicios" class="text-3xl font-bold text-orange-600">0</div>
|
|
<div class="text-sm text-orange-700 mt-1">Pre-servicios</div>
|
|
<div class="text-xs text-orange-400">RCXC / SC</div>
|
|
</div>
|
|
<div id="card-ventas" class="bg-emerald-50 rounded-lg p-4 text-center transition-opacity">
|
|
<div id="cnt-ventas" class="text-3xl font-bold text-emerald-600">0</div>
|
|
<div class="text-sm text-emerald-700 mt-1">Ventas</div>
|
|
<div class="text-xs text-emerald-400">Ventas/Crear</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Terceros / RDA -->
|
|
<div id="sec-terceros-rda">
|
|
<p class="text-xs font-medium text-gray-600 mb-2">Pacientes — clic para ver recepciones:</p>
|
|
<div id="preview-table" class="border border-gray-100 rounded-lg overflow-hidden text-sm"></div>
|
|
</div>
|
|
|
|
<!-- Pre-servicios -->
|
|
<div id="preservicios-section" class="hidden">
|
|
<p class="text-xs font-medium text-orange-600 mb-2 mt-2">Pre-servicios RCXC / SC — clic para ver JSON:</p>
|
|
<div id="preview-ps-table" class="border border-orange-100 rounded-lg overflow-hidden text-sm"></div>
|
|
</div>
|
|
|
|
<!-- Ventas -->
|
|
<div id="ventas-section" class="hidden">
|
|
<p class="text-xs font-medium text-emerald-600 mb-2 mt-2">Facturas Venta — clic para ver JSON:</p>
|
|
<div id="preview-vta-table" class="border border-emerald-100 rounded-lg overflow-hidden text-sm"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ─── Resultados ────────────────────────────────────────────────────────── -->
|
|
<div id="results-section" class="hidden space-y-3">
|
|
|
|
<!-- Barra de resumen + filtros -->
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 px-5 py-4">
|
|
<div class="flex items-center justify-between gap-4 flex-wrap">
|
|
<!-- Chips por paso -->
|
|
<div class="flex items-center gap-2 flex-wrap min-w-0">
|
|
<span class="text-xs font-semibold text-gray-500 shrink-0">RESULTADO</span>
|
|
<div id="res-summary" class="flex flex-wrap gap-2"></div>
|
|
</div>
|
|
<!-- Filtros -->
|
|
<div class="flex items-center gap-1 bg-gray-100 rounded-lg p-1 shrink-0">
|
|
<button id="filter-btn-errores" onclick="setResultFilter('errores')"
|
|
class="px-3 py-1 text-xs font-medium rounded-md transition-all">
|
|
<i class="fas fa-times-circle mr-1"></i>Errores <span id="filter-cnt-errores" class="font-bold"></span>
|
|
</button>
|
|
<button id="filter-btn-advertencias" onclick="setResultFilter('advertencias')"
|
|
class="px-3 py-1 text-xs font-medium rounded-md transition-all">
|
|
<i class="fas fa-exclamation-circle mr-1"></i>Ya existe <span id="filter-cnt-advertencias" class="font-bold"></span>
|
|
</button>
|
|
<button id="filter-btn-enviados" onclick="setResultFilter('enviados')"
|
|
class="px-3 py-1 text-xs font-medium rounded-md transition-all">
|
|
<i class="fas fa-check-circle mr-1"></i>Enviados <span id="filter-cnt-enviados" class="font-bold"></span>
|
|
</button>
|
|
<button id="filter-btn-todos" onclick="setResultFilter('todos')"
|
|
class="px-3 py-1 text-xs font-medium rounded-md transition-all">
|
|
<i class="fas fa-list mr-1"></i>Todos <span id="filter-cnt-todos" class="font-bold"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Lista unificada -->
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
|
|
<div id="results-list" class="divide-y divide-gray-100"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ─── Modal de confirmación ────────────────────────────────────────────── -->
|
|
<div id="modal-confirmar" class="fixed inset-0 z-50 hidden">
|
|
<div class="absolute inset-0 bg-black/50" onclick="cerrarModalConfirmar()"></div>
|
|
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-lg bg-white rounded-xl shadow-2xl">
|
|
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
|
<h3 class="font-semibold text-gray-800 flex items-center gap-2">
|
|
<i class="fas fa-shield-alt text-blue-500"></i> Validar envío
|
|
</h3>
|
|
<button onclick="cerrarModalConfirmar()" class="text-gray-400 hover:text-gray-600">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div class="p-6 space-y-4">
|
|
<p class="text-sm text-gray-600">
|
|
Se enviará a <strong>TNS</strong> para la fecha <strong id="modal-fecha" class="text-blue-600"></strong>:
|
|
</p>
|
|
<div class="grid grid-cols-5 gap-3">
|
|
<div id="mc-terceros" class="bg-blue-50 rounded-lg p-3 text-center transition-opacity">
|
|
<div id="modal-cnt-pac" class="text-2xl font-bold text-blue-600">0</div>
|
|
<div class="text-xs text-blue-700">Terceros</div>
|
|
<div class="text-xs text-blue-400">Tercero/Crear</div>
|
|
</div>
|
|
<div id="mc-rda" class="bg-purple-50 rounded-lg p-3 text-center transition-opacity">
|
|
<div id="modal-cnt-rec" class="text-2xl font-bold text-purple-600">0</div>
|
|
<div class="text-xs text-purple-700">RDA</div>
|
|
<div class="text-xs text-purple-400">RdaPaciente/Insertar</div>
|
|
</div>
|
|
<div id="mc-examenes" class="bg-green-50 rounded-lg p-3 text-center transition-opacity">
|
|
<div id="modal-cnt-exa" class="text-2xl font-bold text-green-600">0</div>
|
|
<div class="text-xs text-green-700">Exámenes</div>
|
|
<div class="text-xs text-green-400">detallePedido</div>
|
|
</div>
|
|
<div id="mc-preserv" class="bg-orange-50 rounded-lg p-3 text-center transition-opacity">
|
|
<div id="modal-cnt-ps" class="text-2xl font-bold text-orange-600">0</div>
|
|
<div class="text-xs text-orange-700">Pre-servicios</div>
|
|
<div class="text-xs text-orange-400">RCXC / SC</div>
|
|
</div>
|
|
<div id="mc-ventas" class="bg-emerald-50 rounded-lg p-3 text-center transition-opacity">
|
|
<div id="modal-cnt-vta" class="text-2xl font-bold text-emerald-600">0</div>
|
|
<div class="text-xs text-emerald-700">Ventas</div>
|
|
<div class="text-xs text-emerald-400">Ventas/Crear</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-amber-50 border border-amber-200 rounded-lg px-4 py-3 text-xs text-amber-700 flex items-start gap-2">
|
|
<i class="fas fa-exclamation-triangle mt-0.5 shrink-0"></i>
|
|
<span>Esta acción enviará datos reales a TNS. Los registros existentes serán actualizados. No se puede deshacer.</span>
|
|
</div>
|
|
<div class="max-h-36 overflow-y-auto border border-gray-100 rounded-lg" id="modal-lista-pac"></div>
|
|
</div>
|
|
<div class="px-6 py-4 border-t border-gray-100 flex justify-end gap-3">
|
|
<button onclick="cerrarModalConfirmar()"
|
|
class="px-5 py-2 bg-gray-100 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-200">
|
|
Cancelar
|
|
</button>
|
|
<button onclick="confirmarEnvio()"
|
|
class="px-5 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium flex items-center gap-2">
|
|
<i class="fas fa-paper-plane"></i> Confirmar y enviar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let _previewData = null;
|
|
let _rdaJsonStore = {};
|
|
let _resultAll = [];
|
|
let _activeFilter = 'errores';
|
|
|
|
// ── Estado de pasos ──────────────────────────────────────────────────────────
|
|
const _pasos = new Set(['terceros','rda','preserv','ventas']);
|
|
const _pasoMeta = {
|
|
terceros: { border:'border-blue-500', bg:'bg-blue-500', text:'text-blue-600' },
|
|
rda: { border:'border-purple-500', bg:'bg-purple-500', text:'text-purple-600' },
|
|
preserv: { border:'border-orange-500', bg:'bg-orange-500', text:'text-orange-600' },
|
|
ventas: { border:'border-emerald-500',bg:'bg-emerald-500', text:'text-emerald-600'},
|
|
};
|
|
|
|
function togglePaso(paso) {
|
|
if (_pasos.has(paso)) _pasos.delete(paso);
|
|
else _pasos.add(paso);
|
|
const m = _pasoMeta[paso];
|
|
const btn = document.getElementById('chip-' + paso);
|
|
if (_pasos.has(paso)) {
|
|
btn.className = `inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold border-2 transition-all ${m.border} ${m.bg} text-white`;
|
|
} else {
|
|
btn.className = `inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold border-2 transition-all ${m.border} bg-white ${m.text}`;
|
|
}
|
|
document.getElementById('preview-section').classList.add('hidden');
|
|
document.getElementById('results-section').classList.add('hidden');
|
|
document.getElementById('btn-run').disabled = true;
|
|
_previewData = null;
|
|
}
|
|
|
|
function escHtml(s) {
|
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
// ── Vista previa ─────────────────────────────────────────────────────────────
|
|
async function previewAutomation() {
|
|
const fecha = document.getElementById('fecha-input').value;
|
|
if (!fecha) { showToast('Selecciona una fecha', 'error'); return; }
|
|
if (_pasos.size === 0) { showToast('Activa al menos un paso', 'error'); return; }
|
|
|
|
const btn = document.getElementById('btn-preview');
|
|
showLoading(btn);
|
|
document.getElementById('preview-section').classList.add('hidden');
|
|
document.getElementById('results-section').classList.add('hidden');
|
|
document.getElementById('btn-run').disabled = true;
|
|
_previewData = null;
|
|
_rdaJsonStore = {};
|
|
|
|
const form = new FormData();
|
|
form.append('fecha', fecha);
|
|
|
|
try {
|
|
const resp = await fetch('/automation/preview', { method: 'POST', body: form, credentials: 'include' });
|
|
const data = await resp.json();
|
|
hideLoading(btn, '<i class="fas fa-eye"></i> Vista previa');
|
|
|
|
if (!data.success) { showToast(data.message || 'Error al consultar', 'error'); return; }
|
|
|
|
_previewData = data;
|
|
|
|
// Stat cards
|
|
document.getElementById('cnt-pacientes').textContent = data.pacientes;
|
|
document.getElementById('cnt-recepciones').textContent = data.recepciones;
|
|
document.getElementById('cnt-examenes').textContent = data.examenes;
|
|
document.getElementById('cnt-preservicios').textContent= data.preservicios || 0;
|
|
document.getElementById('cnt-ventas').textContent = data.ventas || 0;
|
|
document.getElementById('preview-fecha').textContent = data.fecha;
|
|
|
|
document.getElementById('card-terceros').style.opacity = _pasos.has('terceros') ? '1' : '0.3';
|
|
document.getElementById('card-rda').style.opacity = _pasos.has('rda') ? '1' : '0.3';
|
|
document.getElementById('card-examenes').style.opacity = (_pasos.has('rda')||_pasos.has('preserv')) ? '1' : '0.3';
|
|
document.getElementById('card-preserv').style.opacity = _pasos.has('preserv') ? '1' : '0.3';
|
|
document.getElementById('card-ventas').style.opacity = _pasos.has('ventas') ? '1' : '0.3';
|
|
|
|
// Habilitar run solo si hay datos para al menos un paso activo
|
|
const hasDatos = (
|
|
(_pasos.has('terceros') && data.pacientes > 0) ||
|
|
(_pasos.has('rda') && data.recepciones > 0) ||
|
|
(_pasos.has('preserv') && data.preservicios > 0) ||
|
|
(_pasos.has('ventas') && data.ventas > 0)
|
|
);
|
|
document.getElementById('btn-run').disabled = !hasDatos;
|
|
|
|
// Sección Terceros/RDA
|
|
const secTR = document.getElementById('sec-terceros-rda');
|
|
if (_pasos.has('terceros') || _pasos.has('rda')) {
|
|
secTR.classList.remove('hidden');
|
|
const table = document.getElementById('preview-table');
|
|
if (!data.pacientes_preview || data.pacientes_preview.length === 0) {
|
|
table.innerHTML = '<div class="p-4 text-gray-400 text-center">No hay pacientes para esta fecha</div>';
|
|
} else {
|
|
table.innerHTML = data.pacientes_preview.map((p, i) => {
|
|
const rdaRows = (p.rda || []).map((r, j) => {
|
|
const key = `rda-json-${i}-${j}`;
|
|
_rdaJsonStore[key] = r.json;
|
|
return `
|
|
<div>
|
|
<div class="flex items-center gap-3 px-6 py-1.5 bg-blue-50 border-t border-blue-100 text-xs cursor-pointer hover:bg-blue-100 select-none"
|
|
onclick="toggleJsonBlock('${key}','rdachev-${i}-${j}')">
|
|
<i class="fas fa-chevron-right text-blue-300 text-xs transition-transform" id="rdachev-${i}-${j}"></i>
|
|
<span class="text-blue-400 w-4"><i class="fas fa-flask"></i></span>
|
|
<span class="font-mono text-blue-700 w-28">${escHtml(r.factura)}</span>
|
|
<span class="text-blue-600 w-24">${escHtml(r.fecha)}</span>
|
|
<span class="font-mono bg-blue-100 text-blue-800 px-1.5 py-0.5 rounded w-14 text-center">${escHtml(r.contrato||'—')}</span>
|
|
<span class="text-blue-600 w-20">${r.examenes} exam.</span>
|
|
<span class="text-blue-600 flex-1 truncate">${escHtml(r.diag||'')}</span>
|
|
<span class="text-blue-700 font-medium">$${Number(r.valor||0).toLocaleString('es-CO')}</span>
|
|
</div>
|
|
<div id="${key}" class="hidden bg-gray-900 text-green-300 text-xs font-mono p-4 overflow-x-auto whitespace-pre border-b border-blue-100"></div>
|
|
</div>`;
|
|
}).join('');
|
|
return `
|
|
<div class="divide-y divide-gray-100">
|
|
<div class="flex items-center gap-3 px-4 py-2.5 hover:bg-gray-50 cursor-pointer select-none"
|
|
onclick="toggleSubRow('rda-${i}','chev-${i}')">
|
|
<i class="fas fa-chevron-right text-gray-300 text-xs transition-transform" id="chev-${i}"></i>
|
|
<span class="text-gray-400 text-xs w-8">${escHtml(p.tipo)}</span>
|
|
<span class="text-gray-500 w-32 font-mono text-xs">${escHtml(p.doc)}</span>
|
|
<span class="text-gray-700 flex-1">${escHtml(p.nombre)}</span>
|
|
<span class="text-xs ${p.rda&&p.rda.length>0?'text-blue-600':'text-gray-300'}">${(p.rda||[]).length} RDA</span>
|
|
</div>
|
|
<div id="rda-${i}" class="hidden">${rdaRows||'<div class="px-6 py-2 text-xs text-gray-400 bg-gray-50">Sin recepciones</div>'}</div>
|
|
</div>`;
|
|
}).join('');
|
|
}
|
|
} else {
|
|
secTR.classList.add('hidden');
|
|
}
|
|
|
|
// Pre-servicios
|
|
const psSection = document.getElementById('preservicios-section');
|
|
if (_pasos.has('preserv') && data.preservicios_preview && data.preservicios_preview.length > 0) {
|
|
document.getElementById('preview-ps-table').innerHTML = data.preservicios_preview.map((ps, i) => {
|
|
const key = `ps-json-${i}`;
|
|
_rdaJsonStore[key] = ps.json;
|
|
return `
|
|
<div>
|
|
<div class="flex items-center gap-3 px-4 py-2.5 hover:bg-orange-50 cursor-pointer select-none border-b border-orange-100"
|
|
onclick="toggleJsonBlock('${key}','pschev-${i}')">
|
|
<i class="fas fa-chevron-right text-orange-300 text-xs transition-transform" id="pschev-${i}"></i>
|
|
<span class="font-mono text-orange-700 font-medium w-32">${escHtml(ps.factura)}</span>
|
|
<span class="text-gray-500 w-24 text-xs">${escHtml(ps.fecha)}</span>
|
|
<span class="text-gray-600 flex-1 text-xs">Pac. ${escHtml(String(ps.paciente))}</span>
|
|
<span class="text-xs text-orange-500">${ps.examenes} examen(es)</span>
|
|
</div>
|
|
<div id="${key}" class="hidden bg-gray-900 text-green-300 text-xs font-mono p-4 overflow-x-auto whitespace-pre border-b border-orange-100"></div>
|
|
</div>`;
|
|
}).join('');
|
|
psSection.classList.remove('hidden');
|
|
} else {
|
|
psSection.classList.add('hidden');
|
|
}
|
|
|
|
// Ventas - errores de generación
|
|
if (data.ventas_gen_errors && data.ventas_gen_errors.length > 0) {
|
|
data.ventas_gen_errors.forEach(e => {
|
|
showToast(`Error generando venta ${e.factura}: ${e.error}`, 'error');
|
|
console.error('ventas_gen_error', e);
|
|
});
|
|
}
|
|
|
|
// Ventas
|
|
const vtaSection = document.getElementById('ventas-section');
|
|
if (_pasos.has('ventas') && data.ventas_preview && data.ventas_preview.length > 0) {
|
|
document.getElementById('preview-vta-table').innerHTML = data.ventas_preview.map((vta, i) => {
|
|
const key = `vta-json-${i}`;
|
|
_rdaJsonStore[key] = vta.json;
|
|
return `
|
|
<div>
|
|
<div class="flex items-center gap-3 px-4 py-2.5 hover:bg-emerald-50 cursor-pointer select-none border-b border-emerald-100"
|
|
onclick="toggleJsonBlock('${key}','vtachev-${i}')">
|
|
<i class="fas fa-chevron-right text-emerald-300 text-xs transition-transform" id="vtachev-${i}"></i>
|
|
<span class="font-mono text-emerald-700 font-medium w-32">${escHtml(vta.factura)}</span>
|
|
<span class="text-gray-500 w-24 text-xs">${escHtml(vta.fecha)}</span>
|
|
<span class="font-mono bg-emerald-100 text-emerald-800 px-1.5 py-0.5 rounded text-xs w-14 text-center">${escHtml(vta.contrato||'—')}</span>
|
|
<span class="text-gray-600 flex-1 text-xs">Pac. ${escHtml(String(vta.paciente))}</span>
|
|
<span class="text-xs text-emerald-500">${vta.examenes} examen(es)</span>
|
|
</div>
|
|
<div id="${key}" class="hidden bg-gray-900 text-green-300 text-xs font-mono p-4 overflow-x-auto whitespace-pre border-b border-emerald-100"></div>
|
|
</div>`;
|
|
}).join('');
|
|
vtaSection.classList.remove('hidden');
|
|
} else {
|
|
vtaSection.classList.add('hidden');
|
|
}
|
|
|
|
document.getElementById('preview-section').classList.remove('hidden');
|
|
|
|
if (!hasDatos) {
|
|
showToast('Sin datos para los pasos seleccionados en esa fecha', 'warning');
|
|
} else {
|
|
showToast(`${data.pacientes} pac · ${data.recepciones} RDA · ${data.preservicios||0} PS · ${data.ventas||0} VTA`, 'success');
|
|
}
|
|
} catch(e) {
|
|
hideLoading(btn, '<i class="fas fa-eye"></i> Vista previa');
|
|
showToast('Error de conexión', 'error');
|
|
}
|
|
}
|
|
|
|
// ── Modal ────────────────────────────────────────────────────────────────────
|
|
function abrirModalConfirmar() {
|
|
if (!_previewData) return;
|
|
const d = _previewData;
|
|
document.getElementById('modal-fecha').textContent = d.fecha;
|
|
document.getElementById('mc-terceros').style.opacity = _pasos.has('terceros') ? '1' : '0.3';
|
|
document.getElementById('mc-rda').style.opacity = _pasos.has('rda') ? '1' : '0.3';
|
|
document.getElementById('mc-examenes').style.opacity = (_pasos.has('rda')||_pasos.has('preserv')) ? '1' : '0.3';
|
|
document.getElementById('mc-preserv').style.opacity = _pasos.has('preserv') ? '1' : '0.3';
|
|
document.getElementById('mc-ventas').style.opacity = _pasos.has('ventas') ? '1' : '0.3';
|
|
document.getElementById('modal-cnt-pac').textContent = _pasos.has('terceros') ? d.pacientes : '—';
|
|
document.getElementById('modal-cnt-rec').textContent = _pasos.has('rda') ? d.recepciones : '—';
|
|
document.getElementById('modal-cnt-exa').textContent = (_pasos.has('rda')||_pasos.has('preserv')) ? d.examenes : '—';
|
|
document.getElementById('modal-cnt-ps').textContent = _pasos.has('preserv') ? (d.preservicios||0) : '—';
|
|
document.getElementById('modal-cnt-vta').textContent = _pasos.has('ventas') ? (d.ventas||0) : '—';
|
|
document.getElementById('modal-lista-pac').innerHTML = (d.pacientes_preview || []).map(p => `
|
|
<div class="flex items-center gap-3 px-3 py-1.5 border-b border-gray-50 text-xs last:border-0">
|
|
<span class="text-gray-400 w-6">${escHtml(p.tipo)}</span>
|
|
<span class="font-mono text-gray-500 w-28">${escHtml(p.doc)}</span>
|
|
<span class="text-gray-700 flex-1 truncate">${escHtml(p.nombre)}</span>
|
|
<span class="text-blue-500">${(p.rda||[]).length} RDA</span>
|
|
</div>`).join('');
|
|
document.getElementById('modal-confirmar').classList.remove('hidden');
|
|
}
|
|
|
|
function cerrarModalConfirmar() {
|
|
document.getElementById('modal-confirmar').classList.add('hidden');
|
|
}
|
|
|
|
// ── Ejecutar envío ───────────────────────────────────────────────────────────
|
|
async function confirmarEnvio() {
|
|
cerrarModalConfirmar();
|
|
const fecha = document.getElementById('fecha-input').value;
|
|
const btn = document.getElementById('btn-run');
|
|
showLoading(btn);
|
|
document.getElementById('btn-preview').disabled = true;
|
|
document.getElementById('results-section').classList.add('hidden');
|
|
|
|
const form = new FormData();
|
|
form.append('fecha', fecha);
|
|
form.append('pasos', [..._pasos].join(','));
|
|
|
|
try {
|
|
const resp = await fetch('/automation/run', { method: 'POST', body: form, credentials: 'include' });
|
|
const data = await resp.json();
|
|
hideLoading(btn, '<i class="fas fa-play"></i> Ejecutar');
|
|
document.getElementById('btn-preview').disabled = false;
|
|
|
|
if (!data.success) {
|
|
showToast(data.message || 'Error', 'error');
|
|
return;
|
|
}
|
|
renderResults(data.resultado);
|
|
} catch(e) {
|
|
hideLoading(btn, '<i class="fas fa-play"></i> Ejecutar');
|
|
document.getElementById('btn-preview').disabled = false;
|
|
showToast('Error de conexión', 'error');
|
|
}
|
|
}
|
|
|
|
// ── Definiciones de pasos ────────────────────────────────────────────────────
|
|
const _STEP_DEFS = [
|
|
{ pasoKey:'terceros', resultKey:'paso1_terceros', label:'Terceros', num:1, color:'blue', tipo:'tercero', icon:'fa-user' },
|
|
{ pasoKey:'rda', resultKey:'paso2_rda', label:'RDA', num:2, color:'purple', tipo:'rda', icon:'fa-exchange-alt' },
|
|
{ pasoKey:'preserv', resultKey:'paso3_preserv', label:'Pre-servicios', num:3, color:'orange', tipo:'preserv', icon:'fa-file-alt' },
|
|
{ pasoKey:'ventas', resultKey:'paso4_ventas', label:'Ventas', num:4, color:'emerald',tipo:'venta', icon:'fa-file-invoice-dollar'},
|
|
];
|
|
|
|
const _BADGE = {
|
|
blue: 'bg-blue-100 text-blue-700',
|
|
purple: 'bg-purple-100 text-purple-700',
|
|
orange: 'bg-orange-100 text-orange-700',
|
|
emerald:'bg-emerald-100 text-emerald-700',
|
|
};
|
|
const _CHIP = {
|
|
blue: 'bg-blue-100 text-blue-800',
|
|
purple: 'bg-purple-100 text-purple-800',
|
|
orange: 'bg-orange-100 text-orange-800',
|
|
emerald:'bg-emerald-100 text-emerald-800',
|
|
};
|
|
|
|
// ── Renderizar resultados ────────────────────────────────────────────────────
|
|
function renderResults(r) {
|
|
const activePasos = r.pasos || _STEP_DEFS.map(s => s.pasoKey);
|
|
const activeSteps = _STEP_DEFS.filter(s => activePasos.includes(s.pasoKey));
|
|
|
|
// Summary chips
|
|
document.getElementById('res-summary').innerHTML = activeSteps.map(s => {
|
|
const d = r[s.resultKey];
|
|
const hasErr = d.errores > 0;
|
|
return `<span class="${_CHIP[s.color]} rounded-full px-3 py-1 text-xs font-semibold inline-flex items-center gap-1.5">
|
|
<i class="fas ${s.icon} text-xs opacity-70"></i> ${s.label}
|
|
<span class="text-green-700">✓${d.enviados}</span>
|
|
${hasErr ? `<span class="text-red-600 font-bold">✗${d.errores}</span>` : ''}
|
|
</span>`;
|
|
}).join('');
|
|
|
|
// Clasificar todos los items
|
|
_resultAll = [];
|
|
activeSteps.forEach(s => {
|
|
(r[s.resultKey].detalle || []).forEach((item, i) => {
|
|
const st = item.status || (item.ok ? 'success' : 'error');
|
|
_resultAll.push({ item, step: s, idx: `${s.pasoKey}-${i}`, status: st });
|
|
});
|
|
});
|
|
|
|
const errCount = _resultAll.filter(e => e.status === 'error').length;
|
|
const warnCount = _resultAll.filter(e => e.status === 'warning').length;
|
|
const okCount = _resultAll.filter(e => e.status === 'success').length;
|
|
|
|
document.getElementById('filter-cnt-errores').textContent = `(${errCount})`;
|
|
document.getElementById('filter-cnt-advertencias').textContent = `(${warnCount})`;
|
|
document.getElementById('filter-cnt-enviados').textContent = `(${okCount})`;
|
|
document.getElementById('filter-cnt-todos').textContent = `(${_resultAll.length})`;
|
|
|
|
const defaultFilter = errCount > 0 ? 'errores' : warnCount > 0 ? 'advertencias' : 'todos';
|
|
setResultFilter(defaultFilter);
|
|
|
|
document.getElementById('results-section').classList.remove('hidden');
|
|
|
|
showToast(
|
|
errCount === 0 ? `Envío completado · ${okCount} OK · ${warnCount} ya existían` : `${errCount} fallo(s) · ${okCount} OK · ${warnCount} ya existían`,
|
|
errCount === 0 ? 'success' : 'warning'
|
|
);
|
|
}
|
|
|
|
// ── Filtro de resultados ─────────────────────────────────────────────────────
|
|
function setResultFilter(filter) {
|
|
_activeFilter = filter;
|
|
|
|
// Actualizar estilos de botones
|
|
const configs = {
|
|
errores: { active: 'bg-white shadow text-red-600', inactive: 'text-gray-500 hover:text-gray-700' },
|
|
advertencias: { active: 'bg-white shadow text-yellow-600', inactive: 'text-gray-500 hover:text-gray-700' },
|
|
enviados: { active: 'bg-white shadow text-green-600', inactive: 'text-gray-500 hover:text-gray-700' },
|
|
todos: { active: 'bg-white shadow text-gray-700', inactive: 'text-gray-500 hover:text-gray-700' },
|
|
};
|
|
['errores','advertencias','enviados','todos'].forEach(f => {
|
|
const btn = document.getElementById(`filter-btn-${f}`);
|
|
const base = 'px-3 py-1 text-xs font-medium rounded-md transition-all';
|
|
btn.className = `${base} ${f === filter ? configs[f].active : configs[f].inactive}`;
|
|
});
|
|
|
|
let items;
|
|
if (filter === 'errores') items = _resultAll.filter(e => e.status === 'error');
|
|
else if (filter === 'advertencias') items = _resultAll.filter(e => e.status === 'warning');
|
|
else if (filter === 'enviados') items = _resultAll.filter(e => e.status === 'success');
|
|
else items = _resultAll;
|
|
|
|
const listEl = document.getElementById('results-list');
|
|
|
|
if (items.length === 0) {
|
|
const msg = filter === 'errores' ? 'Sin errores — todo enviado correctamente' :
|
|
filter === 'advertencias' ? 'Sin advertencias' :
|
|
filter === 'enviados' ? 'No hay registros enviados' : 'Sin resultados';
|
|
listEl.innerHTML = `<div class="p-8 text-center text-gray-400 text-sm"><i class="fas ${filter==='errores'?'fa-check-circle text-green-400':'fa-inbox'} text-2xl mb-2 block"></i>${msg}</div>`;
|
|
return;
|
|
}
|
|
|
|
listEl.innerHTML = items.map(({ item, step: s, idx, status }) => {
|
|
let label1 = '', label2 = '', dtype = s.tipo, did = '';
|
|
|
|
if (s.tipo === 'tercero') {
|
|
label1 = item.doc || '—';
|
|
label2 = item.nombre || '—';
|
|
did = escHtml(String(item.codigo || ''));
|
|
} else {
|
|
label1 = item.factura || '—';
|
|
label2 = `Pac. ${item.paciente||'—'} · ${item.examenes||0} examen(es)`;
|
|
did = escHtml(String(item.idrecepcion || 0));
|
|
}
|
|
|
|
const msg = (item.msg || '').trim();
|
|
|
|
if (status === 'success') {
|
|
return `
|
|
<div class="px-5 py-3 flex items-center gap-3 hover:bg-gray-50 transition-colors">
|
|
<span class="text-green-500 shrink-0 text-base"><i class="fas fa-check-circle"></i></span>
|
|
<span class="${_BADGE[s.color]} text-xs font-semibold px-2 py-0.5 rounded shrink-0">
|
|
<i class="fas ${s.icon} mr-1 opacity-70 text-xs"></i>${s.label}
|
|
</span>
|
|
<span class="font-mono text-gray-800 text-xs w-28 shrink-0 truncate" title="${escHtml(label1)}">${escHtml(label1)}</span>
|
|
<span class="text-gray-600 text-xs flex-1 truncate">${escHtml(label2)}</span>
|
|
<span class="text-green-600 text-xs max-w-[220px] truncate shrink-0" title="${escHtml(msg)}">${escHtml(msg.slice(0,60))}</span>
|
|
</div>`;
|
|
} else if (status === 'warning') {
|
|
return `
|
|
<div class="px-5 py-3 flex items-center gap-3 bg-yellow-50 hover:bg-yellow-100 transition-colors border-l-4 border-yellow-400">
|
|
<span class="text-yellow-500 shrink-0 text-base"><i class="fas fa-exclamation-circle"></i></span>
|
|
<span class="${_BADGE[s.color]} text-xs font-semibold px-2 py-0.5 rounded shrink-0">
|
|
<i class="fas ${s.icon} mr-1 opacity-70 text-xs"></i>${s.label}
|
|
</span>
|
|
<span class="font-mono text-gray-800 text-xs w-28 shrink-0 truncate" title="${escHtml(label1)}">${escHtml(label1)}</span>
|
|
<span class="text-gray-600 text-xs flex-1 truncate">${escHtml(label2)}</span>
|
|
<span class="text-yellow-700 text-xs max-w-[260px] truncate shrink-0" title="${escHtml(msg)}">${escHtml(msg.slice(0,80))}</span>
|
|
</div>`;
|
|
} else {
|
|
return `
|
|
<div data-resend-row class="px-5 py-3 flex items-start gap-3 bg-red-50 hover:bg-red-100 transition-colors border-l-4 border-red-400">
|
|
<span class="text-red-400 shrink-0 text-base mt-0.5 fail-status-icon"><i class="fas fa-times-circle"></i></span>
|
|
<span class="${_BADGE[s.color]} text-xs font-semibold px-2 py-0.5 rounded shrink-0 mt-0.5">
|
|
<i class="fas ${s.icon} mr-1 opacity-70 text-xs"></i>${s.label}
|
|
</span>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<span class="font-mono text-gray-800 text-xs font-semibold">${escHtml(label1)}</span>
|
|
<span class="text-gray-600 text-xs truncate">${escHtml(label2)}</span>
|
|
</div>
|
|
${msg ? `<div class="mt-1 text-xs text-red-600 break-words fail-msg" title="${escHtml(msg)}">${escHtml(msg.slice(0,120))}</div>` : ''}
|
|
</div>
|
|
<button id="rbtn-${idx}"
|
|
data-tipo="${dtype}" data-id="${did}"
|
|
onclick="reenviarItem(this)"
|
|
class="shrink-0 flex items-center gap-1.5 px-2.5 py-1.5 bg-white border border-gray-200 hover:bg-blue-50 hover:border-blue-300 text-xs text-gray-600 hover:text-blue-700 rounded-lg transition-colors font-medium mt-0.5">
|
|
<i class="fas fa-redo-alt text-xs"></i> Reenviar
|
|
</button>
|
|
</div>`;
|
|
}
|
|
}).join('');
|
|
}
|
|
|
|
// ── Reenviar item individual ─────────────────────────────────────────────────
|
|
async function reenviarItem(btn) {
|
|
const tipo = btn.dataset.tipo;
|
|
const id = btn.dataset.id;
|
|
const row = btn.closest('[data-resend-row]');
|
|
const origHtml = btn.innerHTML;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin text-xs"></i>';
|
|
btn.disabled = true;
|
|
|
|
const form = new FormData();
|
|
let endpoint = '';
|
|
|
|
if (tipo === 'tercero') {
|
|
endpoint = '/automation/reenviar-tercero';
|
|
form.append('codigo', String(id));
|
|
} else if (tipo === 'rda') {
|
|
endpoint = '/automation/reenviar-rda';
|
|
form.append('idrecepcion', String(id));
|
|
form.append('tipo', 'rda');
|
|
} else if (tipo === 'preserv') {
|
|
endpoint = '/automation/reenviar-rda';
|
|
form.append('idrecepcion', String(id));
|
|
form.append('tipo', 'preserv');
|
|
} else {
|
|
endpoint = '/automation/reenviar-venta';
|
|
form.append('idrecepcion', String(id));
|
|
}
|
|
|
|
try {
|
|
const resp = await fetch(endpoint, { method: 'POST', body: form, credentials: 'include' });
|
|
const data = await resp.json();
|
|
|
|
if (data.success) {
|
|
// Marcar la fila como resuelta visualmente
|
|
row.classList.remove('bg-red-50','bg-red-100','border-l-4','border-red-400');
|
|
row.classList.add('bg-green-50','border-l-4','border-green-400');
|
|
row.removeAttribute('data-resend-row');
|
|
const icon = row.querySelector('.fail-status-icon');
|
|
if (icon) icon.innerHTML = '<i class="fas fa-check-circle text-green-500"></i>';
|
|
const msgEl = row.querySelector('.fail-msg');
|
|
if (msgEl) { msgEl.textContent = (data.message||'OK enviado').slice(0,120); msgEl.className = 'mt-1 text-xs text-green-700'; }
|
|
btn.innerHTML = '<i class="fas fa-check text-green-500 text-xs"></i> OK';
|
|
btn.className = 'shrink-0 flex items-center gap-1.5 px-2.5 py-1.5 bg-green-50 border border-green-200 text-xs text-green-700 rounded-lg font-medium cursor-default mt-0.5';
|
|
showToast('Reenviado correctamente', 'success');
|
|
} else {
|
|
btn.innerHTML = origHtml;
|
|
btn.disabled = false;
|
|
const msgEl = row.querySelector('.fail-msg');
|
|
if (msgEl) msgEl.textContent = (data.message||'Error').slice(0,120);
|
|
else {
|
|
const div = document.createElement('div');
|
|
div.className = 'mt-1 text-xs text-red-600 break-words fail-msg';
|
|
div.textContent = (data.message||'Error').slice(0,120);
|
|
row.querySelector('.flex-1').appendChild(div);
|
|
}
|
|
showToast(data.message || 'Error al reenviar', 'error');
|
|
}
|
|
} catch(e) {
|
|
btn.innerHTML = origHtml;
|
|
btn.disabled = false;
|
|
showToast('Error de conexión', 'error');
|
|
}
|
|
}
|
|
|
|
// ── Helpers UI ───────────────────────────────────────────────────────────────
|
|
function toggleSubRow(id, chevId) {
|
|
const div = document.getElementById(id);
|
|
const chev = document.getElementById(chevId);
|
|
const hidden = div.classList.contains('hidden');
|
|
div.classList.toggle('hidden', !hidden);
|
|
if (chev) chev.style.transform = hidden ? 'rotate(90deg)' : '';
|
|
}
|
|
|
|
function toggleJsonBlock(id, chevId) {
|
|
const div = document.getElementById(id);
|
|
const chev = document.getElementById(chevId);
|
|
const hidden = div.classList.contains('hidden');
|
|
if (hidden && _rdaJsonStore[id]) div.textContent = JSON.stringify(_rdaJsonStore[id], null, 2);
|
|
div.classList.toggle('hidden', !hidden);
|
|
if (chev) chev.style.transform = hidden ? 'rotate(90deg)' : '';
|
|
}
|
|
</script>
|
|
{% endblock %}
|