Añadir trazabilidad.html
This commit is contained in:
197
trazabilidad.html
Normal file
197
trazabilidad.html
Normal file
@@ -0,0 +1,197 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Trazabilidad - IntegraRepara</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<style>
|
||||
.fade-in { animation: fadeIn 0.3s ease-in-out; }
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
</style>
|
||||
</head>
|
||||
<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">
|
||||
<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">
|
||||
<i data-lucide="arrow-left" class="w-5 h-5"></i>
|
||||
</button>
|
||||
<div>
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Trazabilidad del Expediente</p>
|
||||
<h1 class="text-xl font-black text-slate-800 leading-none mt-1">#<span id="svcRef" class="text-blue-600">...</span></h1>
|
||||
</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">
|
||||
<i data-lucide="activity" class="w-4 h-4"></i>
|
||||
<span class="text-xs font-bold" id="logCount">0 Registros</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex-1 overflow-y-auto no-scrollbar p-6 md:p-10 relative">
|
||||
<div class="max-w-2xl 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="w-10 h-10 bg-slate-100 rounded-full flex items-center justify-center text-slate-400 shrink-0">
|
||||
<i data-lucide="message-square-plus" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<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 justify-end mt-3">
|
||||
<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">
|
||||
Guardar Apunte <i data-lucide="send" class="w-3 h-3"></i>
|
||||
</button>
|
||||
</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="text-center text-slate-400 py-10 flex flex-col items-center">
|
||||
<i data-lucide="loader-2" class="w-8 h-8 animate-spin mb-3"></i>
|
||||
<p class="text-xs font-bold uppercase tracking-widest">Cargando historial...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const API_URL = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
|
||||
? 'http://localhost:3000'
|
||||
: 'https://integrarepara-api.integrarepara.es';
|
||||
|
||||
let serviceId = null;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
lucide.createIcons();
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
serviceId = urlParams.get('id');
|
||||
|
||||
if (!serviceId) {
|
||||
document.getElementById('timelineContainer').innerHTML = "<p class="text-red-500">Error: No se ha indicado el expediente.</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
// Aquí podríamos cargar la referencia real del servicio, por ahora ponemos el ID
|
||||
document.getElementById('svcRef').innerText = "SVC-" + serviceId;
|
||||
loadLogs();
|
||||
});
|
||||
|
||||
async function loadLogs() {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/services/${serviceId}/logs`, {
|
||||
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
const container = document.getElementById('timelineContainer');
|
||||
|
||||
if (!data.ok) throw new Error("Error cargando logs");
|
||||
|
||||
document.getElementById('logCount').innerText = `${data.logs.length} Registros`;
|
||||
|
||||
if (data.logs.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="absolute -left-1.5 top-0 w-3 h-3 bg-slate-200 rounded-full"></div>
|
||||
<div class="pl-8 pt-1">
|
||||
<p class="text-sm font-bold text-slate-400 italic">No hay actividad registrada aún para este expediente.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = "";
|
||||
|
||||
data.logs.forEach((log, index) => {
|
||||
const dateObj = new Date(log.created_at);
|
||||
const fecha = dateObj.toLocaleDateString('es-ES', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
const hora = dateObj.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
// Asignación inteligente de iconos y colores según la palabra clave
|
||||
let icon = "activity";
|
||||
let colorClass = "bg-slate-100 text-slate-500 border-slate-200";
|
||||
let dotClass = "bg-slate-300 ring-slate-100";
|
||||
|
||||
const actionLower = log.action.toLowerCase();
|
||||
|
||||
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"; }
|
||||
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"; }
|
||||
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 (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"; }
|
||||
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"; }
|
||||
|
||||
// El punto animado si es el más reciente
|
||||
const pingAnimation = index === 0 ? `<div class="absolute inset-0 rounded-full ${dotClass.split(' ')[0]} animate-ping opacity-70"></div>` : '';
|
||||
|
||||
container.innerHTML += `
|
||||
<div class="relative pl-8 sm:pl-10 fade-in" 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">
|
||||
${pingAnimation}
|
||||
</div>
|
||||
|
||||
<div class="bg-white border border-slate-200 p-4 rounded-2xl shadow-sm hover:shadow-md transition-shadow">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<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 h-3"></i> ${log.action}
|
||||
</span>
|
||||
<div class="text-right">
|
||||
<p class="text-xs font-black text-slate-800">${hora}</p>
|
||||
<p class="text-[9px] font-bold text-slate-400 uppercase tracking-widest">${fecha}</p>
|
||||
</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">
|
||||
<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">
|
||||
${log.user_name.charAt(0).toUpperCase()}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
lucide.createIcons();
|
||||
|
||||
} catch (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>";
|
||||
}
|
||||
}
|
||||
|
||||
async function addManualNote() {
|
||||
const input = document.getElementById('manualNoteInput');
|
||||
const text = input.value.trim();
|
||||
if (!text) return;
|
||||
|
||||
const btn = event.currentTarget;
|
||||
const originalHtml = btn.innerHTML;
|
||||
btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i>';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
await fetch(`${API_URL}/services/${serviceId}/log`, {
|
||||
method: 'POST',
|
||||
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
|
||||
body: JSON.stringify({
|
||||
action: "Nota Manual",
|
||||
details: text
|
||||
})
|
||||
});
|
||||
|
||||
input.value = "";
|
||||
loadLogs(); // Recargamos para ver la nota en la línea de tiempo
|
||||
} catch (e) {
|
||||
alert("Error al guardar la nota.");
|
||||
} finally {
|
||||
btn.innerHTML = originalHtml;
|
||||
btn.disabled = false;
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user