Actualizar trazabilidad.html

This commit is contained in:
2026-03-01 22:28:15 +00:00
parent 062a78df2b
commit 7c2734d4ac

View File

@@ -7,58 +7,80 @@
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script> <script src="https://unpkg.com/lucide@latest"></script>
<style> <style>
.fade-in { animation: fadeIn 0.3s ease-in-out; } .fade-in { animation: fadeIn 0.4s ease-out forwards; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } @keyframes fadeIn {
from { opacity: 0; transform: translateY(15px); }
to { opacity: 1; transform: translateY(0); }
}
.no-scrollbar::-webkit-scrollbar { display: none; } .no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; } .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
/* Línea continua del timeline */
.timeline-line::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 23px; /* Ajuste fino para centrar la línea con los puntos */
width: 2px;
background: #e2e8f0; /* slate-200 */
z-index: 0;
}
</style> </style>
</head> </head>
<body class="bg-slate-50 text-slate-800 font-sans h-screen flex flex-col overflow-hidden"> <body class="bg-slate-50 text-slate-800 font-sans h-screen flex flex-col overflow-hidden">
<header class="bg-white border-b border-slate-200 px-6 py-4 flex items-center justify-between shadow-sm z-10 shrink-0"> <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 items-center gap-4"> <div class="flex items-center gap-4">
<button onclick="window.history.back()" class="p-2 bg-slate-100 text-slate-500 hover:bg-blue-100 hover:text-blue-600 rounded-xl transition-colors"> <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> <i data-lucide="arrow-left" class="w-5 h-5"></i>
</button> </button>
<div> <div>
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Trazabilidad del Expediente</p> <p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Auditoría y Trazabilidad</p>
<h1 class="text-xl font-black text-slate-800 leading-none mt-1">#<span id="svcRef" class="text-blue-600">...</span></h1> <h1 class="text-xl font-black text-slate-800 leading-none mt-1 flex items-center gap-2">
Expediente <span id="svcRef" class="text-blue-600 px-2 py-0.5 bg-blue-50 rounded-lg">...</span>
</h1>
</div> </div>
</div> </div>
<div class="bg-blue-50 text-blue-600 px-3 py-1.5 rounded-lg border border-blue-100 flex items-center gap-2"> <div class="bg-slate-800 text-white px-4 py-2 rounded-xl shadow-md flex items-center gap-2">
<i data-lucide="activity" class="w-4 h-4"></i> <i data-lucide="activity" class="w-4 h-4 text-emerald-400"></i>
<span class="text-xs font-bold" id="logCount">0 Registros</span> <span class="text-xs font-black uppercase tracking-widest" id="logCount">0 Registros</span>
</div> </div>
</header> </header>
<main class="flex-1 overflow-y-auto no-scrollbar p-6 md:p-10 relative"> <main class="flex-1 overflow-y-auto no-scrollbar p-6 md:p-10 relative">
<div class="max-w-2xl mx-auto"> <div class="max-w-3xl mx-auto">
<div class="bg-white p-5 rounded-[1.5rem] shadow-sm border border-slate-200 mb-8 fade-in flex gap-4"> <div class="bg-white p-5 rounded-3xl shadow-sm border border-slate-200 mb-10 sticky top-0 z-10">
<div class="w-10 h-10 bg-slate-100 rounded-full flex items-center justify-center text-slate-400 shrink-0"> <div class="flex gap-4 items-start">
<i data-lucide="message-square-plus" class="w-5 h-5"></i> <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">
</div> <i data-lucide="pen-tool" class="w-6 h-6"></i>
<div class="flex-1"> </div>
<input type="text" id="manualNoteInput" placeholder="Escribe un apunte o comentario para el registro..." 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-blue-500 focus:ring-2 focus:ring-blue-100 transition-all"> <div class="flex-1">
<div class="flex justify-end mt-3"> <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>
<button onclick="addManualNote()" 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-colors flex items-center gap-2 active:scale-95"> <div class="flex justify-end mt-3">
Guardar Apunte <i data-lucide="send" class="w-3 h-3"></i> <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">
</button> Guardar Apunte <i data-lucide="send" class="w-4 h-4"></i>
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="relative border-l-2 border-slate-200 ml-5 md:ml-8 space-y-8 pb-10" id="timelineContainer"> <div class="relative timeline-line ml-2 md:ml-4 pb-20" id="timelineContainer">
<div class="text-center text-slate-400 py-10 flex flex-col items-center"> <div class="text-center text-slate-400 py-20 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-10 h-10 animate-spin mb-4 text-blue-500"></i>
<p class="text-xs font-bold uppercase tracking-widest">Cargando historial...</p> <p class="text-sm font-black uppercase tracking-widest text-slate-500">Recopilando historial...</p>
</div> </div>
</div> </div>
</div> </div>
</main> </main>
<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> <script>
// Configuración de la API (Usa la misma variable que el resto de tu app si tienes layout.js)
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'
: 'https://integrarepara-api.integrarepara.es'; : 'https://integrarepara-api.integrarepara.es';
@@ -67,16 +89,26 @@
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
lucide.createIcons(); lucide.createIcons();
// Extraer el ID de la URL (Ejemplo: trazabilidad.html?id=15)
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
serviceId = urlParams.get('id'); serviceId = urlParams.get('id');
const urlRef = urlParams.get('ref'); // Opcional, si le pasas la referencia en la URL
if (!serviceId) { if (!serviceId) {
document.getElementById('timelineContainer').innerHTML = '<p class="text-red-500">Error: No se ha indicado el expediente.</p>'; document.getElementById('timelineContainer').innerHTML = `
<div class="bg-red-50 border border-red-200 text-red-600 p-8 rounded-3xl text-center relative z-10">
<i data-lucide="alert-triangle" class="w-12 h-12 mx-auto mb-4 text-red-400"></i>
<h2 class="text-lg font-black uppercase tracking-widest mb-2">Error de Identificación</h2>
<p class="text-sm font-medium">No se ha indicado ningún ID de expediente válido en la URL.</p>
<button onclick="window.history.back()" class="mt-6 bg-red-600 text-white px-6 py-2 rounded-xl font-bold hover:bg-red-700">Volver Atrás</button>
</div>
`;
lucide.createIcons();
return; return;
} }
// Aquí podríamos cargar la referencia real del servicio, por ahora ponemos el ID document.getElementById('svcRef').innerText = urlRef ? urlRef : `#${serviceId}`;
document.getElementById('svcRef').innerText = "SVC-" + serviceId;
loadLogs(); loadLogs();
}); });
@@ -85,8 +117,8 @@
const res = await fetch(`${API_URL}/services/${serviceId}/logs`, { const res = await fetch(`${API_URL}/services/${serviceId}/logs`, {
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
}); });
const data = await res.json();
const data = await res.json();
const container = document.getElementById('timelineContainer'); const container = document.getElementById('timelineContainer');
if (!data.ok) throw new Error("Error cargando logs"); if (!data.ok) throw new Error("Error cargando logs");
@@ -95,85 +127,124 @@
if (data.logs.length === 0) { if (data.logs.length === 0) {
container.innerHTML = ` container.innerHTML = `
<div class="absolute -left-1.5 top-0 w-3 h-3 bg-slate-200 rounded-full"></div> <div class="relative pl-14 pt-2 z-10">
<div class="pl-8 pt-1"> <div class="absolute left-[17px] top-4 w-3 h-3 bg-slate-300 rounded-full ring-4 ring-white"></div>
<p class="text-sm font-bold text-slate-400 italic">No hay actividad registrada aún para este expediente.</p> <div class="bg-slate-100 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>
</div>
</div> </div>
`; `;
return; return;
} }
container.innerHTML = ""; container.innerHTML = ""; // Limpiamos
data.logs.forEach((log, index) => { data.logs.forEach((log, index) => {
const dateObj = new Date(log.created_at); const dateObj = new Date(log.created_at);
const fecha = dateObj.toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric' });
// Formateo de fecha molón: "Hoy a las 10:00" o "12 Mar 2025"
const hoy = new Date();
const esHoy = dateObj.getDate() === hoy.getDate() && dateObj.getMonth() === hoy.getMonth() && dateObj.getFullYear() === hoy.getFullYear();
const fecha = esHoy ? "Hoy" : dateObj.toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric' });
const hora = dateObj.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' }); const hora = dateObj.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' });
// Asignación inteligente de iconos y colores según la palabra clave // LÓGICA INTELIGENTE DE COLORES E ICONOS
let icon = "activity"; let icon = "activity";
let colorClass = "bg-slate-100 text-slate-500 border-slate-200"; let colorClass = "bg-slate-100 text-slate-600 border-slate-200";
let dotClass = "bg-slate-300 ring-slate-100"; let dotClass = "bg-slate-400 ring-slate-100";
let iconColor = "text-slate-500";
const actionLower = log.action.toLowerCase(); const actionLower = log.action.toLowerCase();
const detailsLower = (log.details || "").toLowerCase();
const fullText = actionLower + " " + detailsLower;
if (actionLower.includes('estado')) { icon = "arrow-right-left"; colorClass = "bg-blue-50 text-blue-700 border-blue-200"; dotClass = "bg-blue-500 ring-blue-100"; } if (fullText.includes('estado') || fullText.includes('actualización')) {
else if (actionLower.includes('llamada') || actionLower.includes('teléfono')) { icon = "phone-call"; colorClass = "bg-emerald-50 text-emerald-700 border-emerald-200"; dotClass = "bg-emerald-500 ring-emerald-100"; } 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 (actionLower.includes('whatsapp') || actionLower.includes('sms')) { icon = "message-circle"; colorClass = "bg-emerald-50 text-emerald-700 border-emerald-200"; dotClass = "bg-emerald-500 ring-emerald-100"; } }
else if (actionLower.includes('camino') || actionLower.includes('ruta')) { icon = "car"; colorClass = "bg-purple-50 text-purple-700 border-purple-200"; dotClass = "bg-purple-500 ring-purple-100"; } else if (fullText.includes('llamada') || fullText.includes('contacto') || fullText.includes('teléfono')) {
else if (actionLower.includes('nota') || actionLower.includes('apunte')) { icon = "sticky-note"; colorClass = "bg-amber-50 text-amber-700 border-amber-200"; dotClass = "bg-amber-500 ring-amber-100"; } 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 (actionLower.includes('asign')) { icon = "user-check"; colorClass = "bg-indigo-50 text-indigo-700 border-indigo-200"; dotClass = "bg-indigo-500 ring-indigo-100"; } }
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('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('asign') || fullText.includes('bolsa')) {
icon = "user-check"; colorClass = "bg-indigo-50 text-indigo-700 border-indigo-200"; dotClass = "bg-indigo-500 ring-indigo-100"; iconColor = "text-indigo-500";
}
else if (fullText.includes('archivado') || fullText.includes('cierre')) {
icon = "archive"; colorClass = "bg-slate-200 text-slate-700 border-slate-300"; dotClass = "bg-slate-600 ring-slate-200"; iconColor = "text-slate-600";
}
else if (fullText.includes('creado') || fullText.includes('presupuesto')) {
icon = "sparkles"; colorClass = "bg-yellow-50 text-yellow-700 border-yellow-200"; dotClass = "bg-yellow-500 ring-yellow-100"; iconColor = "text-yellow-500";
}
// El punto animado si es el más reciente // El punto más reciente parpadea suavemente
const pingAnimation = index === 0 ? `<div class="absolute inset-0 rounded-full ${dotClass.split(' ')[0]} animate-ping opacity-70"></div>` : ''; const pingAnimation = index === 0 ? `<div class="absolute inset-0 rounded-full ${dotClass.split(' ')[0]} animate-ping opacity-50"></div>` : '';
container.innerHTML += ` container.innerHTML += `
<div class="relative pl-8 sm:pl-10 fade-in" style="animation-delay: ${index * 0.05}s"> <div class="relative pl-14 sm:pl-16 pt-2 pb-4 fade-in z-10" style="animation-delay: ${index * 0.05}s">
<div class="absolute -left-[9px] top-1.5 w-4 h-4 rounded-full border-2 border-white ring-4 ${dotClass} z-10 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-4 rounded-2xl shadow-sm hover:shadow-md transition-shadow"> <div class="bg-white border border-slate-200 p-5 rounded-3xl shadow-sm hover:shadow-md transition-all group">
<div class="flex justify-between items-start mb-2"> <div class="flex flex-wrap justify-between items-start gap-4 mb-3">
<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}"> <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}">
<i data-lucide="${icon}" class="w-3 h-3"></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">
<p class="text-xs font-black text-slate-800">${hora}</p> <p class="text-sm font-black text-slate-800">${hora}</p>
<p class="text-[9px] font-bold text-slate-400 uppercase tracking-widest">${fecha}</p> <p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">${fecha}</p>
</div> </div>
</div> </div>
<p class="text-sm text-slate-700 font-medium leading-relaxed">${log.details}</p>
<div class="mt-3 pt-3 border-t border-slate-100 flex items-center gap-2"> <p class="text-sm text-slate-700 font-medium leading-relaxed bg-slate-50 p-3 rounded-xl border border-slate-100">${log.details}</p>
<div class="w-5 h-5 rounded-full bg-slate-200 text-slate-500 flex items-center justify-center text-[9px] font-black shrink-0">
<div class="mt-3 flex items-center gap-2">
<div class="w-6 h-6 rounded-full bg-slate-800 text-white flex items-center justify-center text-[10px] font-black shrink-0 shadow-sm">
${log.user_name.charAt(0).toUpperCase()} ${log.user_name.charAt(0).toUpperCase()}
</div> </div>
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Realizado por: <span class="text-slate-600">${log.user_name}</span></p> <p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">
Registrado por: <span class="text-slate-700">${log.user_name}</span>
</p>
</div> </div>
</div> </div>
</div> </div>
`; `;
}); });
lucide.createIcons(); lucide.createIcons();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
document.getElementById('timelineContainer').innerHTML = "<p class='text-red-500 pl-8 pt-2 font-bold'>No se pudo conectar con el servidor.</p>"; document.getElementById('timelineContainer').innerHTML = "<p class='text-red-500 pl-14 pt-4 font-bold relative z-10'>No se pudo conectar con el servidor.</p>";
} }
} }
async function addManualNote() { async function addManualNote(btn) {
const input = document.getElementById('manualNoteInput'); const input = document.getElementById('manualNoteInput');
const text = input.value.trim(); const text = input.value.trim();
if (!text) return;
const btn = event.currentTarget; if (!text) {
showToast("⚠️ Escribe un apunte antes de guardar.");
input.focus();
return;
}
const originalHtml = btn.innerHTML; const originalHtml = btn.innerHTML;
btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i>'; btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i> Guardando...';
btn.disabled = true; btn.disabled = true;
try { try {
await fetch(`${API_URL}/services/${serviceId}/log`, { const res = await fetch(`${API_URL}/services/${serviceId}/log`, {
method: 'POST', method: 'POST',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` }, headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify({ body: JSON.stringify({
@@ -182,16 +253,28 @@
}) })
}); });
input.value = ""; if(res.ok) {
loadLogs(); // Recargamos para ver la nota en la línea de tiempo input.value = "";
showToast("✅ Apunte guardado correctamente");
loadLogs();
} else {
throw new Error("Error del servidor");
}
} catch (e) { } catch (e) {
alert("Error al guardar la nota."); showToast("Error al guardar la nota.");
} finally { } finally {
btn.innerHTML = originalHtml; btn.innerHTML = originalHtml;
btn.disabled = false; btn.disabled = false;
lucide.createIcons(); lucide.createIcons();
} }
} }
function showToast(msg) {
const t = document.getElementById('toast');
t.innerHTML = msg;
t.classList.remove('hidden');
setTimeout(() => t.classList.add('hidden'), 3000);
}
</script> </script>
</body> </body>
</html> </html>