Actualizar servicios.html

This commit is contained in:
2026-02-16 22:23:06 +00:00
parent 99bd74148b
commit 938786e11c

View File

@@ -11,6 +11,15 @@
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.no-scrollbar::-webkit-scrollbar { display: none; }
.card-hover:hover { transform: translateY(-2px); transition: all 0.2s; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05); }
/* ANIMACIÓN DE TEMBLOR (SHAKE) PARA TARJETAS BLOQUEADAS */
.shake { animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both; }
@keyframes shake {
10%, 90% { transform: translate3d(-2px, 0, 0); }
20%, 80% { transform: translate3d(4px, 0, 0); }
30%, 50%, 70% { transform: translate3d(-6px, 0, 0); }
40%, 60% { transform: translate3d(6px, 0, 0); }
}
</style>
</head>
<body class="bg-slate-50 text-slate-800 font-sans antialiased text-left">
@@ -240,8 +249,9 @@
</div>
</div>
<div id="toastAppt" class="fixed bottom-8 right-8 bg-emerald-600 text-white px-6 py-4 rounded-2xl shadow-2xl hidden z-[200] font-bold text-sm flex items-center gap-2">
<i data-lucide="check-circle" class="w-5 h-5"></i> <span id="toastMsg">Guardado correctamente</span>
<div id="toastAppt" class="fixed bottom-8 right-8 bg-emerald-600 text-white px-6 py-4 rounded-2xl shadow-2xl hidden z-[200] font-bold text-sm flex items-center gap-3 transition-colors duration-300">
<span id="toastIconContainer"><i data-lucide="check-circle" class="w-5 h-5"></i></span>
<span id="toastMsg">Guardado correctamente</span>
</div>
<script src="js/layout.js"></script>
@@ -303,7 +313,17 @@
const assigned = filteredData.filter(s => s.raw_data.scheduled_date && s.raw_data.scheduled_date !== "" && s.raw_data.status_operativo !== 'sin_asignar');
document.getElementById('unassigned-list').innerHTML = unassigned.length > 0
? unassigned.map(s => cardTemplate(s, 'rose', s.assigned_name ? 'Pausado' : 'Sin Asignar')).join('')
? unassigned.map(s => {
let color = 'rose';
let label = s.assigned_name ? 'Pausado' : 'Sin Asignar';
// COLORES INTELIGENTES PARA LA BOLSA DE TRABAJO
if (!s.assigned_name) {
if (s.automation_status === 'in_progress') { color = 'amber'; label = 'Buscando (WA)'; }
if (s.automation_status === 'failed') { color = 'orange'; label = 'En Bolsa'; }
}
return cardTemplate(s, color, label);
}).join('')
: '<p class="text-center py-10 text-slate-300 text-xs font-bold uppercase border-2 border-dashed border-slate-200/50 rounded-[1.5rem]">Vacío</p>';
document.getElementById('pending-list').innerHTML = pending.length > 0
@@ -337,23 +357,69 @@
if(raw.status_operativo === 'incidencia') iconEstado = 'alert-triangle';
if(raw.status_operativo === 'terminado') iconEstado = 'check-circle';
// DICCIONARIO DE COLORES BLINDADO PARA TAILWIND JIT
const colorClasses = {
'rose': 'bg-rose-100 text-rose-600',
'blue': 'bg-blue-100 text-blue-600',
'emerald': 'bg-emerald-100 text-emerald-600',
'amber': 'bg-amber-100 text-amber-600',
'orange': 'bg-orange-100 text-orange-600',
'red': 'bg-red-100 text-red-600',
'purple': 'bg-purple-100 text-purple-600'
};
const badgeClass = colorClasses[color] || 'bg-slate-100 text-slate-600';
// LÓGICA DE BLOQUEO (No se puede abrir si está buscando o en bolsa, a menos que ya tenga operario y sea pausa)
const isBlocked = !s.assigned_name && (s.automation_status === 'in_progress' || s.automation_status === 'failed');
const clickAction = isBlocked ? `shakeCard(this, '${s.automation_status}')` : `openDetail(${s.id})`;
const cursorStyle = isBlocked ? 'cursor-not-allowed' : 'cursor-pointer';
return `
<div class="bg-white p-5 rounded-3xl border border-slate-100 shadow-sm card-hover text-left flex items-start justify-between gap-4 cursor-pointer" onclick="openDetail(${s.id})">
<div class="space-y-1">
<div class="bg-white p-5 rounded-3xl border border-slate-100 shadow-sm card-hover text-left flex items-start justify-between gap-4 ${cursorStyle}" onclick="${clickAction}">
<div class="space-y-1 w-full min-w-0">
<div class="flex items-center gap-2 mb-2">
<span class="text-[8px] font-black bg-${color}-100 text-${color}-600 px-2 py-0.5 rounded-md uppercase tracking-wider">${label}</span>
<span class="text-[8px] font-black ${badgeClass} px-2 py-0.5 rounded-md uppercase tracking-wider">${label}</span>
<span class="text-[8px] text-slate-400 font-bold uppercase border border-slate-100 px-1.5 py-0.5 rounded-md">#${s.service_ref}</span>
</div>
<h4 class="font-black text-slate-800 uppercase text-xs leading-tight">${name}</h4>
<p class="text-[10px] text-slate-400 font-bold uppercase truncate max-w-[200px]">${addr}</p>
<h4 class="font-black text-slate-800 uppercase text-xs leading-tight truncate">${name}</h4>
<p class="text-[10px] text-slate-400 font-bold uppercase truncate">${addr}</p>
<div class="flex items-center gap-3 mt-3 pt-2 border-t border-slate-50">
<div class="flex items-center gap-1.5"><i data-lucide="user" class="w-3 h-3 text-slate-400"></i><span class="text-[9px] font-black text-slate-600 uppercase">${s.assigned_name || 'Sin asignar'}</span></div>
${raw.scheduled_date ? `<div class="flex items-center gap-1.5 text-blue-600"><i data-lucide="${iconEstado}" class="w-3 h-3"></i><span class="text-[9px] font-black uppercase">${cita}</span></div>` : ''}
${raw.scheduled_date && !isBlocked ? `<div class="flex items-center gap-1.5 text-blue-600"><i data-lucide="${iconEstado}" class="w-3 h-3"></i><span class="text-[9px] font-black uppercase">${cita}</span></div>` : ''}
</div>
</div>
</div>`;
}
// FUNCIÓN QUE HACE TEMBLAR LA TARJETA
function shakeCard(element, status) {
element.classList.add('shake');
setTimeout(() => element.classList.remove('shake'), 400);
if (status === 'in_progress') {
showToast("⏳ El robot está asignando este servicio por WhatsApp...", "warning");
} else {
showToast("📍 Este servicio está en la bolsa esperando a que un operario lo coja.", "warning");
}
}
function showToast(msg, type = 'success') {
const toast = document.getElementById('toastAppt');
const iconContainer = document.getElementById('toastIconContainer');
document.getElementById('toastMsg').innerText = msg;
if (type === 'warning') {
toast.classList.replace('bg-emerald-600', 'bg-amber-500');
iconContainer.innerHTML = '<i data-lucide="alert-circle" class="w-5 h-5"></i>';
} else {
toast.classList.replace('bg-amber-500', 'bg-emerald-600');
iconContainer.innerHTML = '<i data-lucide="check-circle" class="w-5 h-5"></i>';
}
lucide.createIcons();
toast.classList.remove('hidden');
setTimeout(() => { toast.classList.add('hidden'); }, 3500);
}
function openDetail(id) {
const s = localData.find(x => x.id === id);
if (!s) return;
@@ -373,9 +439,7 @@
document.getElementById('detDesc').innerHTML = (raw["Descripción"] || raw["DESCRIPCION"] || "Sin notas.").replace(/\n/g, '<br>');
// LÓGICA DE PANELES LATERALES
if (s.assigned_name && raw.status_operativo !== 'sin_asignar') {
// TIENE OPERARIO: Mostrar panel de fechas
document.getElementById('panelAsignado').classList.remove('hidden');
document.getElementById('panelSinAsignar').classList.add('hidden');
@@ -384,11 +448,9 @@
document.getElementById('timeInput').value = raw.scheduled_time || "";
document.getElementById('detStatusMap').value = raw.status_operativo || 'citado';
} else {
// NO TIENE OPERARIO (O FUE PAUSADO): Mostrar panel de re-asignación
document.getElementById('panelAsignado').classList.add('hidden');
document.getElementById('panelSinAsignar').classList.remove('hidden');
// Pre-cargar el gremio si el robot lo detectó previamente
if(raw.guild_id) {
document.getElementById('reGremio').value = raw.guild_id;
loadOps(raw.guild_id, 'reOperario');
@@ -408,11 +470,7 @@
const time = document.getElementById('timeInput').value;
const statusMap = document.getElementById('detStatusMap').value;
// Si lo mandan a "sin_asignar", borrar operario en BD
let assigned_to = undefined;
if (statusMap === 'sin_asignar') {
assigned_to = null;
} else if(!date && statusMap !== 'incidencia' && statusMap !== 'terminado') {
if (statusMap !== 'sin_asignar' && !date && statusMap !== 'incidencia' && statusMap !== 'terminado') {
if(!confirm("No has asignado Fecha. ¿Deseas continuar?")) return;
}
@@ -422,7 +480,6 @@
btn.disabled = true;
try {
// Si cambiamos a sin_asignar, usamos la ruta de scraped para borrar el assigned_to
if(statusMap === 'sin_asignar') {
await fetch(`${API_URL}/providers/scraped/${id}`, {
method: 'PUT',
@@ -459,8 +516,8 @@
});
const data = await res.json();
if(data.ok) { closeDetailModal(); showToast("Enviado a la rueda de WhatsApp"); refreshPanel(); }
else { alert(data.error || "No hay operarios en esa zona para ese gremio"); btn.innerHTML = 'Mandar a la Cola'; }
} catch (e) { alert("Error"); btn.innerHTML = 'Mandar a la Cola'; }
else { alert(data.error || "No hay operarios en esa zona para ese gremio"); btn.innerHTML = '<div><p class="font-black uppercase text-xs">Mandar a la Cola</p><p class="text-[9px] text-blue-200">Rueda automática (WA)</p></div><i data-lucide="zap" class="w-5 h-5"></i>'; lucide.createIcons(); }
} catch (e) { alert("Error"); }
}
async function assignManualFromPanel() {
@@ -489,13 +546,6 @@
} catch (e) { alert("Error"); }
}
function showToast(msg) {
document.getElementById('toastMsg').innerText = msg;
const toast = document.getElementById('toastAppt');
toast.classList.remove('hidden');
setTimeout(() => { toast.classList.add('hidden'); }, 3000);
}
async function saveNewService(e) {
e.preventDefault();
const action = e.submitter.value;