Actualizar trazabilidad.html
This commit is contained in:
@@ -3,28 +3,23 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Trazabilidad - IntegraRepara</title>
|
||||
<title>Centro de Trazabilidad - IntegraRepara</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<style>
|
||||
.fade-in { animation: fadeIn 0.4s ease-out forwards; }
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(15px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.fade-in { animation: fadeIn 0.3s ease-out forwards; }
|
||||
@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; }
|
||||
|
||||
/* 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;
|
||||
content: ''; position: absolute; top: 0; bottom: 0; left: 23px; width: 2px; background: #e2e8f0; z-index: 0;
|
||||
}
|
||||
|
||||
.selected-card {
|
||||
background-color: #eff6ff !important; /* blue-50 */
|
||||
border-color: #bfdbfe !important; /* blue-200 */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -36,85 +31,211 @@
|
||||
<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">Auditoría y Trazabilidad</p>
|
||||
<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 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>
|
||||
<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 text-emerald-400"></i>
|
||||
<span class="text-xs font-black uppercase tracking-widest" 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-3xl mx-auto">
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
|
||||
<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>
|
||||
<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="p-4 border-b border-slate-100 bg-slate-50/50 space-y-3 shrink-0">
|
||||
<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="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 expedientes...</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<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="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-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">
|
||||
<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">
|
||||
Guardar Apunte <i data-lucide="send" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<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 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-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">
|
||||
<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">
|
||||
Guardar Apunte <i data-lucide="send" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative timeline-line ml-2 md:ml-4 pb-20" id="timelineContainer">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative timeline-line ml-2 md:ml-4 pb-20" id="timelineContainer">
|
||||
<div class="text-center text-slate-400 py-20 flex flex-col items-center relative z-10">
|
||||
<i data-lucide="loader-2" class="w-10 h-10 animate-spin mb-4 text-blue-500"></i>
|
||||
<p class="text-sm font-black uppercase tracking-widest text-slate-500">Recopilando historial...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</main>
|
||||
</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>
|
||||
// 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'
|
||||
? 'http://localhost:3000'
|
||||
: 'https://integrarepara-api.integrarepara.es';
|
||||
|
||||
let serviceId = null;
|
||||
let allServices = [];
|
||||
let currentServiceId = null;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
if (!localStorage.getItem("token")) window.location.href = "index.html";
|
||||
lucide.createIcons();
|
||||
loadAllServices();
|
||||
});
|
||||
|
||||
// Extraer el ID de la URL (Ejemplo: trazabilidad.html?id=15)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
serviceId = urlParams.get('id');
|
||||
const urlRef = urlParams.get('ref'); // Opcional, si le pasas la referencia en la URL
|
||||
// 1. CARGAMOS TODOS LOS SERVICIOS AL ENTRAR
|
||||
async function loadAllServices() {
|
||||
try {
|
||||
// Usamos el endpoint de providers que trae todos los servicios sin filtrar archivados
|
||||
const res = await fetch(`${API_URL}/providers/scraped`, {
|
||||
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (!serviceId) {
|
||||
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();
|
||||
if (data.ok && data.services) {
|
||||
allServices = data.services;
|
||||
renderServices(); // Pintamos la lista
|
||||
} else {
|
||||
document.getElementById('servicesList').innerHTML = `<p class="text-center text-red-500 text-sm font-bold mt-10">Error al cargar datos.</p>`;
|
||||
}
|
||||
} catch(e) {
|
||||
document.getElementById('servicesList').innerHTML = `<p class="text-center text-red-500 text-sm font-bold mt-10">Fallo de conexión.</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. PINTAR LA LISTA LATERAL CON FILTROS
|
||||
function renderServices() {
|
||||
const list = document.getElementById('servicesList');
|
||||
const search = document.getElementById('searchBox').value.toLowerCase();
|
||||
const month = document.getElementById('monthBox').value; // Formato YYYY-MM
|
||||
|
||||
const filtered = allServices.filter(s => {
|
||||
const raw = s.raw_data || {};
|
||||
const textSearch = `${s.service_ref} ${raw["Nombre Cliente"] || raw["CLIENTE"]} ${raw["Teléfono"]} ${raw["Dirección"]}`.toLowerCase();
|
||||
|
||||
let matchesSearch = textSearch.includes(search);
|
||||
let matchesMonth = true;
|
||||
|
||||
if (month) {
|
||||
const svcDate = s.created_at ? s.created_at.substring(0, 7) : "";
|
||||
matchesMonth = svcDate === month;
|
||||
}
|
||||
|
||||
return matchesSearch && matchesMonth;
|
||||
});
|
||||
|
||||
list.innerHTML = "";
|
||||
if (filtered.length === 0) {
|
||||
list.innerHTML = `<div class="text-center p-6 text-slate-400 text-sm font-bold">No se encontraron expedientes.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('svcRef').innerText = urlRef ? urlRef : `#${serviceId}`;
|
||||
loadLogs();
|
||||
});
|
||||
filtered.forEach(s => {
|
||||
const raw = s.raw_data || {};
|
||||
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' });
|
||||
|
||||
async function loadLogs() {
|
||||
// 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 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';
|
||||
|
||||
list.innerHTML += `
|
||||
<div onclick="selectService(${s.id}, '${s.service_ref}', '${statusText}')"
|
||||
class="cursor-pointer p-4 rounded-2xl border transition-all shadow-sm ${isSelected}">
|
||||
<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-[9px] font-black uppercase tracking-widest px-2 py-0.5 rounded-md ${statusColor}">${statusText}</span>
|
||||
</div>
|
||||
<p class="text-sm font-black text-slate-800 truncate" title="${name}">${name}</p>
|
||||
<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-400">${date}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// 3. AL HACER CLIC EN UN SERVICIO, CARGAMOS SU TIMELINE
|
||||
function selectService(id, ref, statusText) {
|
||||
currentServiceId = id;
|
||||
|
||||
// Actualizamos la lista para pintar de azul el seleccionado
|
||||
renderServices();
|
||||
|
||||
// Ocultar Empty State y Mostrar Timeline
|
||||
document.getElementById('emptyState').classList.add('hidden');
|
||||
document.getElementById('traceabilityView').classList.remove('hidden');
|
||||
|
||||
// Actualizar Cabecera del Timeline
|
||||
document.getElementById('currentSvcRef').innerText = `#${ref}`;
|
||||
document.getElementById('currentSvcStatus').innerText = statusText;
|
||||
|
||||
// Poner el timeline en loading
|
||||
document.getElementById('timelineContainer').innerHTML = `
|
||||
<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>
|
||||
<p class="text-xs font-bold uppercase tracking-widest">Descargando historial...</p>
|
||||
</div>
|
||||
`;
|
||||
lucide.createIcons();
|
||||
|
||||
// Limpiar caja de notas por si había algo escrito
|
||||
document.getElementById('manualNoteInput').value = "";
|
||||
|
||||
// Cargar los logs de este servicio
|
||||
loadLogsForService(id);
|
||||
}
|
||||
|
||||
async function loadLogsForService(id) {
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/services/${serviceId}/logs`, {
|
||||
const res = await fetch(`${API_URL}/services/${id}/logs`, {
|
||||
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
|
||||
});
|
||||
|
||||
@@ -137,19 +258,17 @@
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = ""; // Limpiamos
|
||||
container.innerHTML = "";
|
||||
|
||||
data.logs.forEach((log, index) => {
|
||||
const dateObj = new Date(log.created_at);
|
||||
|
||||
// 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' });
|
||||
|
||||
// LÓGICA INTELIGENTE DE COLORES E ICONOS
|
||||
// Lógica de colores del timeline
|
||||
let icon = "activity";
|
||||
let colorClass = "bg-slate-100 text-slate-600 border-slate-200";
|
||||
let dotClass = "bg-slate-400 ring-slate-100";
|
||||
@@ -159,55 +278,33 @@
|
||||
const detailsLower = (log.details || "").toLowerCase();
|
||||
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";
|
||||
}
|
||||
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('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";
|
||||
}
|
||||
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('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 más reciente parpadea suavemente
|
||||
const pingAnimation = index === 0 ? `<div class="absolute inset-0 rounded-full ${dotClass.split(' ')[0]} animate-ping opacity-50"></div>` : '';
|
||||
|
||||
container.innerHTML += `
|
||||
<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-[15px] top-6 w-4 h-4 rounded-full border-2 border-white ring-4 ${dotClass} z-20 flex items-center justify-center">
|
||||
${pingAnimation}
|
||||
</div>
|
||||
|
||||
<div class="bg-white border border-slate-200 p-5 rounded-3xl shadow-sm hover:shadow-md transition-all group">
|
||||
<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}">
|
||||
<i data-lucide="${icon}" class="w-3.5 h-3.5 ${iconColor}"></i> ${log.action}
|
||||
</span>
|
||||
|
||||
<div class="text-right">
|
||||
<p class="text-sm font-black text-slate-800">${hora}</p>
|
||||
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">${fecha}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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="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()}
|
||||
@@ -220,16 +317,17 @@
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
lucide.createIcons();
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
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>";
|
||||
}
|
||||
}
|
||||
|
||||
// 4. GUARDAR APUNTE MANUAL Y RECARGAR
|
||||
async function addManualNote(btn) {
|
||||
if (!currentServiceId) return showToast("⚠️ Selecciona un expediente primero.");
|
||||
|
||||
const input = document.getElementById('manualNoteInput');
|
||||
const text = input.value.trim();
|
||||
|
||||
@@ -244,19 +342,16 @@
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/services/${serviceId}/log`, {
|
||||
const res = await fetch(`${API_URL}/services/${currentServiceId}/log`, {
|
||||
method: 'POST',
|
||||
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
|
||||
body: JSON.stringify({
|
||||
action: "Nota Manual",
|
||||
details: text
|
||||
})
|
||||
body: JSON.stringify({ action: "Nota Manual", details: text })
|
||||
});
|
||||
|
||||
if(res.ok) {
|
||||
input.value = "";
|
||||
showToast("✅ Apunte guardado correctamente");
|
||||
loadLogs();
|
||||
loadLogsForService(currentServiceId);
|
||||
} else {
|
||||
throw new Error("Error del servidor");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user