Actualizar trazabilidad.html

This commit is contained in:
2026-03-01 22:35:23 +00:00
parent fd4a6cc794
commit ac3d396567

View File

@@ -18,99 +18,108 @@
} }
.selected-card { .selected-card {
background-color: #eff6ff !important; /* blue-50 */ background-color: #eff6ff !important;
border-color: #bfdbfe !important; /* blue-200 */ border-color: #bfdbfe !important;
} }
</style> </style>
</head> </head>
<body class="bg-slate-50 text-slate-800 font-sans h-screen flex flex-col overflow-hidden"> <body class="bg-gray-50 text-gray-800 font-sans antialiased overflow-hidden">
<header class="bg-white border-b border-slate-200 px-6 py-4 flex items-center justify-between shadow-sm z-20 shrink-0"> <div class="flex h-screen overflow-hidden text-left">
<div class="flex items-center gap-4"> <div id="sidebar-container" class="h-full shrink-0"></div>
<button onclick="window.history.back()" class="p-2.5 bg-slate-100 text-slate-500 hover:bg-blue-100 hover:text-blue-600 rounded-xl transition-all shadow-sm active:scale-95">
<i data-lucide="arrow-left" class="w-5 h-5"></i>
</button>
<div>
<h1 class="text-xl font-black text-slate-800 flex items-center gap-2">
<span class="bg-slate-800 p-1.5 rounded-lg text-white"><i data-lucide="history" class="w-4 h-4"></i></span>
Centro de Trazabilidad
</h1>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mt-1">Auditoría completa de expedientes</p>
</div>
</div>
</header>
<div class="flex-1 flex overflow-hidden"> <div class="flex-1 flex flex-col overflow-hidden relative">
<div id="header-container"></div>
<aside class="w-full md:w-1/3 lg:w-1/4 bg-white border-r border-slate-200 flex flex-col z-10 shadow-[4px_0_24px_rgba(0,0,0,0.02)]"> <div class="flex-1 flex overflow-hidden bg-white border-t border-slate-200">
<div class="p-4 border-b border-slate-100 bg-slate-50/50 space-y-3 shrink-0"> <aside class="w-full md:w-1/3 lg:w-1/4 bg-slate-50/50 border-r border-slate-200 flex flex-col z-10">
<div class="relative">
<i data-lucide="search" class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400"></i>
<input type="text" id="searchBox" oninput="renderServices()" placeholder="Buscar cliente, teléfono, ref..." class="w-full pl-9 pr-4 py-2.5 bg-white border border-slate-200 rounded-xl text-sm font-medium outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-50 transition-all shadow-sm">
</div>
<div class="flex gap-2 items-center bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm">
<div class="px-3 text-slate-400 border-r border-slate-100"><i data-lucide="calendar-days" class="w-4 h-4"></i></div>
<input type="month" id="monthBox" onchange="renderServices()" class="w-full bg-transparent text-xs font-black px-2 py-2 outline-none text-slate-600 uppercase tracking-widest cursor-pointer">
<button onclick="document.getElementById('monthBox').value=''; renderServices()" class="p-2 text-slate-400 hover:text-red-500" title="Borrar mes"><i data-lucide="x" class="w-4 h-4"></i></button>
</div>
</div>
<div id="servicesList" class="flex-1 overflow-y-auto no-scrollbar p-3 space-y-2"> <div class="p-4 border-b border-slate-200 bg-white shrink-0 flex items-center justify-between">
<div class="py-10 text-center text-slate-400"> <div>
<i data-lucide="loader-2" class="w-6 h-6 animate-spin mx-auto mb-2"></i> <h2 class="text-lg font-black text-slate-800 flex items-center gap-2">
<p class="text-xs font-bold uppercase tracking-widest">Cargando expedientes...</p> <span class="bg-slate-800 p-1.5 rounded-lg text-white"><i data-lucide="history" class="w-4 h-4"></i></span>
</div> Auditoría
</div> </h2>
</aside> <p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-0.5">Centro de Control</p>
<main class="flex-1 overflow-y-auto no-scrollbar bg-slate-50 relative">
<div id="emptyState" class="absolute inset-0 flex flex-col items-center justify-center text-center p-6">
<div class="w-24 h-24 bg-slate-200 text-slate-400 rounded-full flex items-center justify-center mb-6 shadow-inner">
<i data-lucide="mouse-pointer-click" class="w-10 h-10"></i>
</div>
<h2 class="text-2xl font-black text-slate-700 tracking-tight">Selecciona un expediente</h2>
<p class="text-slate-500 font-medium mt-2 max-w-sm">Busca en el panel izquierdo y haz clic en un servicio para ver todo su historial de movimientos y comunicaciones.</p>
</div>
<div id="traceabilityView" class="hidden p-6 md:p-10 max-w-3xl mx-auto fade-in">
<div class="flex justify-between items-end border-b-2 border-slate-200 pb-4 mb-6">
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Trazabilidad en tiempo real</p>
<h2 class="text-3xl font-black text-slate-800" id="currentSvcRef">#SVC-000</h2>
</div>
<div class="bg-blue-100 text-blue-700 px-3 py-1.5 rounded-lg font-black text-xs uppercase tracking-widest border border-blue-200" id="currentSvcStatus">
ESTADO
</div>
</div>
<div class="bg-white p-5 rounded-3xl shadow-sm border border-slate-200 mb-10 sticky top-0 z-10">
<div class="flex gap-4 items-start">
<div class="w-12 h-12 bg-amber-50 rounded-full flex items-center justify-center text-amber-500 shrink-0 shadow-inner border border-amber-100">
<i data-lucide="pen-tool" class="w-6 h-6"></i>
</div> </div>
<div class="flex-1"> </div>
<textarea id="manualNoteInput" rows="2" placeholder="Escribe un apunte interno, observación o nota para este expediente..." class="w-full bg-slate-50 border border-slate-200 px-4 py-3 rounded-2xl text-sm font-medium outline-none focus:bg-white focus:border-amber-400 focus:ring-4 focus:ring-amber-50 transition-all resize-none"></textarea>
<div class="flex justify-end mt-3"> <div class="p-4 border-b border-slate-200 bg-white space-y-3 shrink-0">
<button onclick="addManualNote(this)" class="bg-amber-500 hover:bg-amber-600 text-white text-xs font-black uppercase tracking-widest px-6 py-2.5 rounded-xl shadow-md transition-all flex items-center gap-2 active:scale-95"> <div class="relative">
Guardar Apunte <i data-lucide="send" class="w-4 h-4"></i> <i data-lucide="search" class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400"></i>
</button> <input type="text" id="searchBox" oninput="renderServices()" placeholder="Buscar cliente, teléfono, ref..." class="w-full pl-9 pr-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl text-sm font-medium outline-none focus:bg-white focus:border-blue-500 focus:ring-2 focus:ring-blue-100 transition-all shadow-sm">
</div>
<div class="flex gap-2 items-center bg-slate-50 border border-slate-200 rounded-xl overflow-hidden shadow-sm focus-within:ring-2 focus-within:ring-blue-100 focus-within:border-blue-500 transition-all">
<div class="px-3 text-slate-400 border-r border-slate-200"><i data-lucide="calendar-days" class="w-4 h-4"></i></div>
<input type="month" id="monthBox" onchange="renderServices()" class="w-full bg-transparent text-xs font-black px-2 py-2.5 outline-none text-slate-600 uppercase tracking-widest cursor-pointer">
<button onclick="document.getElementById('monthBox').value=''; renderServices()" class="p-2 text-slate-400 hover:text-red-500 transition-colors" title="Borrar mes"><i data-lucide="x" class="w-4 h-4"></i></button>
</div>
</div>
<div id="servicesList" class="flex-1 overflow-y-auto no-scrollbar p-3 space-y-2">
<div class="py-10 text-center text-slate-400">
<i data-lucide="loader-2" class="w-6 h-6 animate-spin mx-auto mb-2"></i>
<p class="text-xs font-bold uppercase tracking-widest">Cargando...</p>
</div>
</div>
</aside>
<main class="flex-1 overflow-y-auto no-scrollbar bg-slate-50/50 relative">
<div id="emptyState" class="absolute inset-0 flex flex-col items-center justify-center text-center p-6 fade-in">
<div class="w-24 h-24 bg-white border border-slate-200 text-slate-300 rounded-full flex items-center justify-center mb-6 shadow-sm">
<i data-lucide="mouse-pointer-click" class="w-10 h-10"></i>
</div>
<h2 class="text-2xl font-black text-slate-700 tracking-tight">Selecciona un expediente</h2>
<p class="text-sm text-slate-500 font-medium mt-2 max-w-sm">Busca en el panel izquierdo y haz clic en un servicio para ver todo su historial de movimientos.</p>
</div>
<div id="traceabilityView" class="hidden p-6 md:p-10 max-w-4xl mx-auto fade-in">
<div class="flex flex-col md:flex-row justify-between items-start md:items-end border-b-2 border-slate-200 pb-4 mb-6 gap-4">
<div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Línea de tiempo del expediente</p>
<h2 class="text-3xl font-black text-slate-800 flex items-center gap-3">
<span id="currentSvcRef">#SVC-000</span>
<span class="bg-blue-100 text-blue-700 px-3 py-1 rounded-lg font-black text-[10px] uppercase tracking-widest border border-blue-200" id="currentSvcStatus">ESTADO</span>
</h2>
</div>
<div class="bg-white px-4 py-2 rounded-xl shadow-sm border border-slate-200 flex items-center gap-2">
<i data-lucide="activity" class="w-4 h-4 text-emerald-500"></i>
<span class="text-xs font-bold text-slate-600" id="logCount">0 Registros</span>
</div> </div>
</div> </div>
</div>
</div>
<div class="relative timeline-line ml-2 md:ml-4 pb-20" id="timelineContainer"> <div class="bg-white p-5 rounded-2xl shadow-sm border border-slate-200 mb-8 sticky top-0 z-10">
<div class="flex gap-4 items-start">
<div class="w-10 h-10 bg-amber-50 rounded-full flex items-center justify-center text-amber-500 shrink-0 border border-amber-100">
<i data-lucide="pen-tool" class="w-5 h-5"></i>
</div>
<div class="flex-1">
<textarea id="manualNoteInput" rows="2" placeholder="Escribe un apunte interno, observación o nota para este expediente..." class="w-full bg-slate-50 border border-slate-200 px-4 py-3 rounded-xl text-sm font-medium outline-none focus:bg-white focus:border-amber-400 focus:ring-2 focus:ring-amber-100 transition-all resize-none"></textarea>
<div class="flex justify-end mt-2">
<button onclick="addManualNote(this)" class="bg-slate-800 hover:bg-slate-700 text-white text-xs font-black uppercase tracking-widest px-5 py-2.5 rounded-lg shadow transition-all flex items-center gap-2 active:scale-95">
Guardar Apunte <i data-lucide="send" class="w-3 h-3"></i>
</button>
</div>
</div>
</div>
</div>
<div class="relative timeline-line ml-2 md:ml-4 pb-20" id="timelineContainer">
</div>
</div> </div>
</main>
</div> </div>
</div>
</main>
</div> </div>
<div id="toast" class="fixed bottom-8 right-8 bg-slate-900 text-white px-6 py-3 rounded-2xl shadow-2xl hidden z-[200] font-bold text-sm flex items-center gap-2 transition-all"></div> <div id="toast" class="fixed bottom-8 right-8 bg-slate-900 text-white px-6 py-3 rounded-2xl shadow-2xl hidden z-[200] font-bold text-sm flex items-center gap-2 transition-all"></div>
<script src="js/layout.js"></script>
<script> <script>
const API_URL = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' const API_URL = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
? 'http://localhost:3000' ? 'http://localhost:3000'
@@ -122,13 +131,18 @@
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
if (!localStorage.getItem("token")) window.location.href = "index.html"; if (!localStorage.getItem("token")) window.location.href = "index.html";
lucide.createIcons(); lucide.createIcons();
// Ponemos el mes actual por defecto
const now = new Date();
const currentMonthStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
document.getElementById('monthBox').value = currentMonthStr;
loadAllServices(); loadAllServices();
}); });
// 1. CARGAMOS TODOS LOS SERVICIOS AL ENTRAR // 1. CARGAMOS TODOS LOS SERVICIOS AL ENTRAR
async function loadAllServices() { async function loadAllServices() {
try { try {
// Usamos el endpoint de providers que trae todos los servicios sin filtrar archivados
const res = await fetch(`${API_URL}/providers/scraped`, { const res = await fetch(`${API_URL}/providers/scraped`, {
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
}); });
@@ -137,6 +151,17 @@
if (data.ok && data.services) { if (data.ok && data.services) {
allServices = data.services; allServices = data.services;
renderServices(); // Pintamos la lista renderServices(); // Pintamos la lista
// Si nos pasaron un ID por la URL, lo seleccionamos automáticamente
const urlParams = new URLSearchParams(window.location.search);
const paramId = urlParams.get('id');
if (paramId) {
const sToSelect = allServices.find(s => s.id == paramId);
if(sToSelect) {
const stText = sToSelect.status === 'archived' ? 'Archivado' : 'Activo';
selectService(sToSelect.id, sToSelect.service_ref, stText);
}
}
} else { } else {
document.getElementById('servicesList').innerHTML = `<p class="text-center text-red-500 text-sm font-bold mt-10">Error al cargar datos.</p>`; document.getElementById('servicesList').innerHTML = `<p class="text-center text-red-500 text-sm font-bold mt-10">Error al cargar datos.</p>`;
} }
@@ -163,12 +188,18 @@
matchesMonth = svcDate === month; matchesMonth = svcDate === month;
} }
return matchesSearch && matchesMonth; // Omitir bloqueos de agenda
return matchesSearch && matchesMonth && s.provider !== 'SYSTEM_BLOCK';
}); });
list.innerHTML = ""; list.innerHTML = "";
if (filtered.length === 0) { if (filtered.length === 0) {
list.innerHTML = `<div class="text-center p-6 text-slate-400 text-sm font-bold">No se encontraron expedientes.</div>`; list.innerHTML = `
<div class="text-center p-8 mt-10">
<i data-lucide="inbox" class="w-8 h-8 text-slate-300 mx-auto mb-3"></i>
<p class="text-slate-400 text-sm font-bold">No se encontraron expedientes.</p>
</div>`;
lucide.createIcons();
return; return;
} }
@@ -177,24 +208,21 @@
const name = raw["Nombre Cliente"] || raw["CLIENTE"] || "Cliente Desconocido"; const name = raw["Nombre Cliente"] || raw["CLIENTE"] || "Cliente Desconocido";
const date = new Date(s.created_at).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit', year: '2-digit' }); const date = new Date(s.created_at).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit', year: '2-digit' });
// Colorcitos para saber si está activo o archivado
const statusColor = s.status === 'archived' ? 'bg-slate-200 text-slate-600' : 'bg-emerald-100 text-emerald-700'; const statusColor = s.status === 'archived' ? 'bg-slate-200 text-slate-600' : 'bg-emerald-100 text-emerald-700';
const statusText = s.status === 'archived' ? 'Archivado' : 'Activo'; const statusText = s.status === 'archived' ? 'Archivado' : 'Activo';
// Si es el servicio seleccionado actualmente, le ponemos otro fondo
const isSelected = s.id === currentServiceId ? 'selected-card' : 'bg-white hover:bg-slate-50 border-transparent hover:border-slate-200'; const isSelected = s.id === currentServiceId ? 'selected-card' : 'bg-white hover:bg-slate-50 border-transparent hover:border-slate-200';
list.innerHTML += ` list.innerHTML += `
<div onclick="selectService(${s.id}, '${s.service_ref}', '${statusText}')" <div onclick="selectService(${s.id}, '${s.service_ref}', '${statusText}')"
class="cursor-pointer p-4 rounded-2xl border transition-all shadow-sm ${isSelected}"> class="cursor-pointer p-4 rounded-2xl border transition-all shadow-sm ${isSelected}">
<div class="flex justify-between items-start mb-1"> <div class="flex justify-between items-start mb-1">
<span class="text-xs font-black text-blue-600 tracking-widest uppercase">#${s.service_ref}</span> <span class="text-xs font-black text-slate-800 tracking-widest uppercase">#${s.service_ref}</span>
<span class="text-[9px] font-black uppercase tracking-widest px-2 py-0.5 rounded-md ${statusColor}">${statusText}</span> <span class="text-[9px] font-black uppercase tracking-widest px-2 py-0.5 rounded-md ${statusColor}">${statusText}</span>
</div> </div>
<p class="text-sm font-black text-slate-800 truncate" title="${name}">${name}</p> <p class="text-sm font-black text-blue-600 truncate" title="${name}">${name}</p>
<div class="flex justify-between items-center mt-2"> <div class="flex justify-between items-center mt-2">
<p class="text-[10px] font-bold text-slate-500"><i data-lucide="map-pin" class="w-3 h-3 inline"></i> ${raw["Población"] || "Sin zona"}</p> <p class="text-[10px] font-bold text-slate-500 truncate pr-2"><i data-lucide="map-pin" class="w-3 h-3 inline"></i> ${raw["Población"] || "Sin zona"}</p>
<p class="text-[10px] font-bold text-slate-400">${date}</p> <p class="text-[10px] font-bold text-slate-400 shrink-0">${date}</p>
</div> </div>
</div> </div>
`; `;
@@ -209,15 +237,17 @@
// Actualizamos la lista para pintar de azul el seleccionado // Actualizamos la lista para pintar de azul el seleccionado
renderServices(); renderServices();
// Ocultar Empty State y Mostrar Timeline // Actualizar URL sin recargar para que si refresca se quede donde estaba
const newUrl = new URL(window.location);
newUrl.searchParams.set('id', id);
window.history.pushState({}, '', newUrl);
document.getElementById('emptyState').classList.add('hidden'); document.getElementById('emptyState').classList.add('hidden');
document.getElementById('traceabilityView').classList.remove('hidden'); document.getElementById('traceabilityView').classList.remove('hidden');
// Actualizar Cabecera del Timeline
document.getElementById('currentSvcRef').innerText = `#${ref}`; document.getElementById('currentSvcRef').innerText = `#${ref}`;
document.getElementById('currentSvcStatus').innerText = statusText; document.getElementById('currentSvcStatus').innerText = statusText;
// Poner el timeline en loading
document.getElementById('timelineContainer').innerHTML = ` document.getElementById('timelineContainer').innerHTML = `
<div class="text-center text-slate-400 py-10 flex flex-col items-center relative z-10"> <div class="text-center text-slate-400 py-10 flex flex-col items-center relative z-10">
<i data-lucide="loader-2" class="w-8 h-8 animate-spin mb-3"></i> <i data-lucide="loader-2" class="w-8 h-8 animate-spin mb-3"></i>
@@ -226,10 +256,7 @@
`; `;
lucide.createIcons(); lucide.createIcons();
// Limpiar caja de notas por si había algo escrito
document.getElementById('manualNoteInput').value = ""; document.getElementById('manualNoteInput').value = "";
// Cargar los logs de este servicio
loadLogsForService(id); loadLogsForService(id);
} }
@@ -250,7 +277,7 @@
container.innerHTML = ` container.innerHTML = `
<div class="relative pl-14 pt-2 z-10"> <div class="relative pl-14 pt-2 z-10">
<div class="absolute left-[17px] top-4 w-3 h-3 bg-slate-300 rounded-full ring-4 ring-white"></div> <div class="absolute left-[17px] top-4 w-3 h-3 bg-slate-300 rounded-full ring-4 ring-white"></div>
<div class="bg-slate-100 border border-slate-200 p-6 rounded-2xl text-center"> <div class="bg-white shadow-sm border border-slate-200 p-6 rounded-2xl text-center">
<p class="text-sm font-bold text-slate-500 italic">No hay actividad registrada aún para este expediente.</p> <p class="text-sm font-bold text-slate-500 italic">No hay actividad registrada aún para este expediente.</p>
</div> </div>
</div> </div>
@@ -271,7 +298,7 @@
// Lógica de colores del timeline // Lógica de colores del timeline
let icon = "activity"; let icon = "activity";
let colorClass = "bg-slate-100 text-slate-600 border-slate-200"; let colorClass = "bg-slate-100 text-slate-600 border-slate-200";
let dotClass = "bg-slate-400 ring-slate-100"; let dotClass = "bg-slate-300 ring-slate-100";
let iconColor = "text-slate-500"; let iconColor = "text-slate-500";
const actionLower = log.action.toLowerCase(); const actionLower = log.action.toLowerCase();
@@ -279,7 +306,7 @@
const fullText = actionLower + " " + detailsLower; const fullText = actionLower + " " + detailsLower;
if (fullText.includes('estado') || fullText.includes('actualización')) { icon = "arrow-right-left"; colorClass = "bg-blue-50 text-blue-700 border-blue-200"; dotClass = "bg-blue-500 ring-blue-100"; iconColor = "text-blue-500"; } if (fullText.includes('estado') || fullText.includes('actualización')) { icon = "arrow-right-left"; colorClass = "bg-blue-50 text-blue-700 border-blue-200"; dotClass = "bg-blue-500 ring-blue-100"; iconColor = "text-blue-500"; }
else if (fullText.includes('llamada') || fullText.includes('contacto') || fullText.includes('teléfono')) { icon = "phone-call"; colorClass = "bg-teal-50 text-teal-700 border-teal-200"; dotClass = "bg-teal-500 ring-teal-100"; iconColor = "text-teal-500"; } else if (fullText.includes('llamada') || fullText.includes('contacto') || fullText.includes('teléfono') || fullText.includes('intento')) { icon = "phone-call"; colorClass = "bg-teal-50 text-teal-700 border-teal-200"; dotClass = "bg-teal-500 ring-teal-100"; iconColor = "text-teal-500"; }
else if (fullText.includes('whatsapp') || fullText.includes('sms')) { icon = "message-circle"; colorClass = "bg-emerald-50 text-emerald-700 border-emerald-200"; dotClass = "bg-emerald-500 ring-emerald-100"; iconColor = "text-emerald-500"; } else if (fullText.includes('whatsapp') || fullText.includes('sms')) { icon = "message-circle"; colorClass = "bg-emerald-50 text-emerald-700 border-emerald-200"; dotClass = "bg-emerald-500 ring-emerald-100"; iconColor = "text-emerald-500"; }
else if (fullText.includes('camino') || fullText.includes('ruta')) { icon = "car"; colorClass = "bg-purple-50 text-purple-700 border-purple-200"; dotClass = "bg-purple-500 ring-purple-100"; iconColor = "text-purple-500"; } else if (fullText.includes('camino') || fullText.includes('ruta')) { icon = "car"; colorClass = "bg-purple-50 text-purple-700 border-purple-200"; dotClass = "bg-purple-500 ring-purple-100"; iconColor = "text-purple-500"; }
else if (fullText.includes('nota') || fullText.includes('apunte') || fullText.includes('manual')) { icon = "sticky-note"; colorClass = "bg-amber-50 text-amber-800 border-amber-200"; dotClass = "bg-amber-400 ring-amber-100"; iconColor = "text-amber-500"; } else if (fullText.includes('nota') || fullText.includes('apunte') || fullText.includes('manual')) { icon = "sticky-note"; colorClass = "bg-amber-50 text-amber-800 border-amber-200"; dotClass = "bg-amber-400 ring-amber-100"; iconColor = "text-amber-500"; }
@@ -294,9 +321,9 @@
<div class="absolute left-[15px] top-6 w-4 h-4 rounded-full border-2 border-white ring-4 ${dotClass} z-20 flex items-center justify-center"> <div class="absolute left-[15px] top-6 w-4 h-4 rounded-full border-2 border-white ring-4 ${dotClass} z-20 flex items-center justify-center">
${pingAnimation} ${pingAnimation}
</div> </div>
<div class="bg-white border border-slate-200 p-5 rounded-3xl shadow-sm hover:shadow-md transition-all group"> <div class="bg-white border border-slate-200 p-5 rounded-2xl shadow-sm hover:shadow-md transition-all group">
<div class="flex flex-wrap justify-between items-start gap-4 mb-3"> <div class="flex flex-wrap justify-between items-start gap-4 mb-3">
<span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-[10px] font-black uppercase tracking-widest border ${colorClass}"> <span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-[10px] font-black uppercase tracking-widest border ${colorClass}">
<i data-lucide="${icon}" class="w-3.5 h-3.5 ${iconColor}"></i> ${log.action} <i data-lucide="${icon}" class="w-3.5 h-3.5 ${iconColor}"></i> ${log.action}
</span> </span>
<div class="text-right"> <div class="text-right">
@@ -324,7 +351,6 @@
} }
} }
// 4. GUARDAR APUNTE MANUAL Y RECARGAR
async function addManualNote(btn) { async function addManualNote(btn) {
if (!currentServiceId) return showToast("⚠️ Selecciona un expediente primero."); if (!currentServiceId) return showToast("⚠️ Selecciona un expediente primero.");