Actualizar servicios.html
This commit is contained in:
106
servicios.html
106
servicios.html
@@ -11,6 +11,15 @@
|
|||||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
.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); }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-slate-50 text-slate-800 font-sans antialiased text-left">
|
<body class="bg-slate-50 text-slate-800 font-sans antialiased text-left">
|
||||||
@@ -240,8 +249,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<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">
|
||||||
<i data-lucide="check-circle" class="w-5 h-5"></i> <span id="toastMsg">Guardado correctamente</span>
|
<span id="toastIconContainer"><i data-lucide="check-circle" class="w-5 h-5"></i></span>
|
||||||
|
<span id="toastMsg">Guardado correctamente</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/layout.js"></script>
|
<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');
|
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
|
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>';
|
: '<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
|
document.getElementById('pending-list').innerHTML = pending.length > 0
|
||||||
@@ -337,23 +357,69 @@
|
|||||||
if(raw.status_operativo === 'incidencia') iconEstado = 'alert-triangle';
|
if(raw.status_operativo === 'incidencia') iconEstado = 'alert-triangle';
|
||||||
if(raw.status_operativo === 'terminado') iconEstado = 'check-circle';
|
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 `
|
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="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">
|
<div class="space-y-1 w-full min-w-0">
|
||||||
<div class="flex items-center gap-2 mb-2">
|
<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>
|
<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>
|
</div>
|
||||||
<h4 class="font-black text-slate-800 uppercase text-xs leading-tight">${name}</h4>
|
<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 max-w-[200px]">${addr}</p>
|
<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-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>
|
<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>
|
</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) {
|
function openDetail(id) {
|
||||||
const s = localData.find(x => x.id === id);
|
const s = localData.find(x => x.id === id);
|
||||||
if (!s) return;
|
if (!s) return;
|
||||||
@@ -373,9 +439,7 @@
|
|||||||
|
|
||||||
document.getElementById('detDesc').innerHTML = (raw["Descripción"] || raw["DESCRIPCION"] || "Sin notas.").replace(/\n/g, '<br>');
|
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') {
|
if (s.assigned_name && raw.status_operativo !== 'sin_asignar') {
|
||||||
// TIENE OPERARIO: Mostrar panel de fechas
|
|
||||||
document.getElementById('panelAsignado').classList.remove('hidden');
|
document.getElementById('panelAsignado').classList.remove('hidden');
|
||||||
document.getElementById('panelSinAsignar').classList.add('hidden');
|
document.getElementById('panelSinAsignar').classList.add('hidden');
|
||||||
|
|
||||||
@@ -384,11 +448,9 @@
|
|||||||
document.getElementById('timeInput').value = raw.scheduled_time || "";
|
document.getElementById('timeInput').value = raw.scheduled_time || "";
|
||||||
document.getElementById('detStatusMap').value = raw.status_operativo || 'citado';
|
document.getElementById('detStatusMap').value = raw.status_operativo || 'citado';
|
||||||
} else {
|
} else {
|
||||||
// NO TIENE OPERARIO (O FUE PAUSADO): Mostrar panel de re-asignación
|
|
||||||
document.getElementById('panelAsignado').classList.add('hidden');
|
document.getElementById('panelAsignado').classList.add('hidden');
|
||||||
document.getElementById('panelSinAsignar').classList.remove('hidden');
|
document.getElementById('panelSinAsignar').classList.remove('hidden');
|
||||||
|
|
||||||
// Pre-cargar el gremio si el robot lo detectó previamente
|
|
||||||
if(raw.guild_id) {
|
if(raw.guild_id) {
|
||||||
document.getElementById('reGremio').value = raw.guild_id;
|
document.getElementById('reGremio').value = raw.guild_id;
|
||||||
loadOps(raw.guild_id, 'reOperario');
|
loadOps(raw.guild_id, 'reOperario');
|
||||||
@@ -408,11 +470,7 @@
|
|||||||
const time = document.getElementById('timeInput').value;
|
const time = document.getElementById('timeInput').value;
|
||||||
const statusMap = document.getElementById('detStatusMap').value;
|
const statusMap = document.getElementById('detStatusMap').value;
|
||||||
|
|
||||||
// Si lo mandan a "sin_asignar", borrar operario en BD
|
if (statusMap !== 'sin_asignar' && !date && statusMap !== 'incidencia' && statusMap !== 'terminado') {
|
||||||
let assigned_to = undefined;
|
|
||||||
if (statusMap === 'sin_asignar') {
|
|
||||||
assigned_to = null;
|
|
||||||
} else if(!date && statusMap !== 'incidencia' && statusMap !== 'terminado') {
|
|
||||||
if(!confirm("No has asignado Fecha. ¿Deseas continuar?")) return;
|
if(!confirm("No has asignado Fecha. ¿Deseas continuar?")) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,7 +480,6 @@
|
|||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Si cambiamos a sin_asignar, usamos la ruta de scraped para borrar el assigned_to
|
|
||||||
if(statusMap === 'sin_asignar') {
|
if(statusMap === 'sin_asignar') {
|
||||||
await fetch(`${API_URL}/providers/scraped/${id}`, {
|
await fetch(`${API_URL}/providers/scraped/${id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@@ -459,8 +516,8 @@
|
|||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if(data.ok) { closeDetailModal(); showToast("Enviado a la rueda de WhatsApp"); refreshPanel(); }
|
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'; }
|
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"); btn.innerHTML = 'Mandar a la Cola'; }
|
} catch (e) { alert("Error"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function assignManualFromPanel() {
|
async function assignManualFromPanel() {
|
||||||
@@ -489,13 +546,6 @@
|
|||||||
} catch (e) { alert("Error"); }
|
} 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) {
|
async function saveNewService(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const action = e.submitter.value;
|
const action = e.submitter.value;
|
||||||
|
|||||||
Reference in New Issue
Block a user