Permite anular servicios sin tener que asignártelos primero

solicitar.html: botón "Anular Aviso" en la Bolsa de Trabajo. buscar.html: botón de anular en la pestaña "Sin Asignar" y en "Mis Asignados". asignados.html: botón "Anular Servicio" en la ficha de un servicio ya asignado pero sin cita. En los tres casos se cambia el estado a Anulado directamente, sin tocar assigned_to, para no disparar el WhatsApp de "servicio asignado".
This commit is contained in:
2026-09-07 07:26:24 +00:00
parent 8b2fbe4138
commit 806fa00103
3 changed files with 119 additions and 3 deletions

View File

@@ -193,6 +193,10 @@
<button onclick="saveAppointment()" id="btnSaveAppt" disabled class="w-full bg-slate-300 text-white font-black py-4 rounded-2xl shadow-sm transition-all flex items-center justify-center gap-2 uppercase tracking-widest text-xs cursor-not-allowed"> <button onclick="saveAppointment()" id="btnSaveAppt" disabled class="w-full bg-slate-300 text-white font-black py-4 rounded-2xl shadow-sm transition-all flex items-center justify-center gap-2 uppercase tracking-widest text-xs cursor-not-allowed">
<i data-lucide="calendar-check" class="w-5 h-5"></i> Confirmar Cita <i data-lucide="calendar-check" class="w-5 h-5"></i> Confirmar Cita
</button> </button>
<!-- 🆕 Anular un servicio ya asignado pero aún sin cita, sin tener que agendarlo primero -->
<button onclick="anularServicio()" id="btnAnularAsig" class="w-full mt-3 bg-white text-rose-600 font-black py-3 rounded-2xl border border-rose-200 flex items-center justify-center gap-2 uppercase tracking-widest text-xs active:scale-95 transition-transform">
<i data-lucide="ban" class="w-4 h-4"></i> Anular Servicio
</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1029,6 +1033,42 @@ function renderServices(servicesToRender = localServices) {
lucide.createIcons(); lucide.createIcons();
} }
} }
// 🆕 Anula un servicio ya asignado pero todavía sin cita, sin obligar a agendarlo antes.
async function anularServicio() {
const id = document.getElementById('detId').value;
if (!confirm("¿Seguro que quieres anular este servicio? No podrás deshacerlo desde aquí.")) return;
const stAnulado = systemStatuses.find(st => st.name.toLowerCase().includes('anula'));
if (!stAnulado) return alert("El sistema no tiene un estado 'Anulado' configurado. Avisa a la oficina.");
const btn = document.getElementById('btnAnularAsig');
const originalContent = btn.innerHTML;
btn.innerHTML = `<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i> Anulando...`;
btn.disabled = true;
try {
const res = await fetch(`${API_URL}/services/set-appointment/${id}`, {
method: 'PUT',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify({ status_operativo: String(stAnulado.id) })
});
if (res.ok) {
showToast("Servicio anulado");
closeModal('actionModal', 'modalContent');
refreshData();
} else {
alert("Error al anular.");
}
} catch (e) {
alert("Error de conexión");
} finally {
btn.innerHTML = originalContent;
btn.disabled = false;
lucide.createIcons();
}
}
function showToast(msg) { function showToast(msg) {
const t = document.getElementById('toast'); const t = document.getElementById('toast');
document.getElementById('toastMsg').innerText = msg; document.getElementById('toastMsg').innerText = msg;

View File

@@ -444,6 +444,14 @@
</button> </button>
`; `;
// 🆕 Botón Anular: disponible en ambas pestañas, para no tener que asignarse
// el servicio primero solo para poder cancelarlo (y evitar el WhatsApp de "asignado").
const anularBtn = `
<button onclick="anularServicio(${s.id})" class="bg-white hover:bg-rose-50 border border-rose-200 text-rose-600 font-black py-2.5 px-3 rounded-xl flex items-center justify-center transition-colors" title="Anular">
<i data-lucide="ban" class="w-4 h-4"></i>
</button>
`;
if (currentTab === 'mine') { if (currentTab === 'mine') {
if (!isScheduled) { if (!isScheduled) {
actionButtons = ` actionButtons = `
@@ -459,19 +467,20 @@
</button> </button>
` + actionButtons; ` + actionButtons;
} }
actionButtons = actionButtons + anularBtn;
} else if (currentTab === 'unassigned') { } else if (currentTab === 'unassigned') {
// GOAL 2: BOTÓN ASIGNÁRMELO // GOAL 2: BOTÓN ASIGNÁRMELO
actionButtons = ` actionButtons = `
<button onclick="claimService(${s.id})" class="flex-1 bg-emerald-500 hover:bg-emerald-600 text-white font-black py-2.5 rounded-xl flex items-center justify-center gap-1.5 transition-colors text-[10px] uppercase tracking-widest shadow-md"> <button onclick="claimService(${s.id})" class="flex-1 bg-emerald-500 hover:bg-emerald-600 text-white font-black py-2.5 rounded-xl flex items-center justify-center gap-1.5 transition-colors text-[10px] uppercase tracking-widest shadow-md">
<i data-lucide="hand" class="w-4 h-4"></i> ¡Asignármelo! <i data-lucide="hand" class="w-4 h-4"></i> ¡Asignármelo!
</button> </button>
` + actionButtons; ` + actionButtons + anularBtn;
} }
return ` return `
<div class="bg-white p-5 rounded-3xl border border-slate-200 shadow-sm relative overflow-hidden flex flex-col gap-4"> <div class="bg-white p-5 rounded-3xl border border-slate-200 shadow-sm relative overflow-hidden flex flex-col gap-4">
${isUrgent ? '<div class="absolute top-0 right-0 bg-red-500 text-white text-[8px] font-black px-3 py-1 rounded-bl-xl uppercase tracking-widest shadow-sm z-10">Urgente</div>' : ''} ${isUrgent ? '<div class="absolute top-0 right-0 bg-red-500 text-white text-[8px] font-black px-3 py-1 rounded-bl-xl uppercase tracking-widest shadow-sm z-10">Urgente</div>' : ''}
<div class="flex justify-between items-start"> <div class="flex justify-between items-start">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-[9px] font-black ${colorData.bg} ${colorData.text} px-2.5 py-1.5 rounded-lg uppercase tracking-wider flex items-center gap-1.5 border ${colorData.border}"> <span class="text-[9px] font-black ${colorData.bg} ${colorData.text} px-2.5 py-1.5 rounded-lg uppercase tracking-wider flex items-center gap-1.5 border ${colorData.border}">
@@ -481,7 +490,7 @@
<span class="text-[9px] text-slate-400 font-bold uppercase bg-slate-50 border border-slate-100 px-2.5 py-1.5 rounded-lg">#${s.service_ref}</span> <span class="text-[9px] text-slate-400 font-bold uppercase bg-slate-50 border border-slate-100 px-2.5 py-1.5 rounded-lg">#${s.service_ref}</span>
</div> </div>
</div> </div>
<div> <div>
<p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-0.5 truncate">${company}</p> <p class="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-0.5 truncate">${company}</p>
<h3 class="font-black text-slate-800 text-base uppercase leading-tight truncate">${name}</h3> <h3 class="font-black text-slate-800 text-base uppercase leading-tight truncate">${name}</h3>
@@ -553,6 +562,32 @@
} }
} }
// 🆕 Anula un servicio directamente, esté o no asignado, sin pasar por "Asignármelo" primero
// (así no se le manda al cliente el WhatsApp de "servicio asignado" para algo que se va a cancelar).
async function anularServicio(id) {
if (!confirm("¿Seguro que quieres anular este servicio? No podrás deshacerlo desde aquí.")) return;
const stAnulado = systemStatuses.find(st => st.name.toLowerCase().includes('anula'));
if (!stAnulado) return alert("El sistema no tiene un estado 'Anulado' configurado. Avisa a la oficina.");
try {
const res = await fetch(`${API_URL}/providers/scraped/${id}`, {
method: 'PUT',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify({ status_operativo: String(stAnulado.id) })
});
if (res.ok) {
showToast("Servicio anulado");
loadAllData();
} else {
alert("Error al anular.");
}
} catch (e) {
alert("Error de conexión");
}
}
async function quickCopyPortalLink(serviceId, phone, name, addr, btnElement) { async function quickCopyPortalLink(serviceId, phone, name, addr, btnElement) {
if (!phone) return alert("El cliente no tiene teléfono guardado"); if (!phone) return alert("El cliente no tiene teléfono guardado");

View File

@@ -117,6 +117,11 @@
</button> </button>
<p class="text-center text-[10px] font-bold text-slate-400 mt-4">Al quedártelo, el servicio pasará a tu lista de Asignados.</p> <p class="text-center text-[10px] font-bold text-slate-400 mt-4">Al quedártelo, el servicio pasará a tu lista de Asignados.</p>
<!-- 🆕 Anular sin tener que asignártelo primero (evita el WhatsApp de "asignado" al cliente) -->
<button onclick="anularServicio()" id="btnAnularBolsa" class="w-full mt-3 bg-white text-rose-600 font-black py-3.5 rounded-2xl border border-rose-200 active:scale-95 transition-transform flex items-center justify-center gap-2 uppercase tracking-widest text-xs">
<i data-lucide="ban" class="w-4 h-4"></i> Anular Aviso
</button>
</div> </div>
</div> </div>
<div id="toast" class="fixed top-5 left-1/2 -translate-x-1/2 bg-slate-800 text-white px-5 py-3 rounded-full shadow-2xl z-[200] font-bold text-xs uppercase tracking-widest flex items-center gap-2 transition-all duration-300 opacity-0 pointer-events-none transform -translate-y-10"> <div id="toast" class="fixed top-5 left-1/2 -translate-x-1/2 bg-slate-800 text-white px-5 py-3 rounded-full shadow-2xl z-[200] font-bold text-xs uppercase tracking-widest flex items-center gap-2 transition-all duration-300 opacity-0 pointer-events-none transform -translate-y-10">
@@ -440,6 +445,42 @@
lucide.createIcons(); lucide.createIcons();
} }
} }
// 🆕 Anula el aviso directamente desde la bolsa, sin tener que asignártelo primero
// (así el cliente NO recibe el WhatsApp de "servicio asignado" por algo que ni siquiera vas a coger).
async function anularServicio() {
const id = document.getElementById('detId').value;
if (!confirm("¿Seguro que quieres anular este aviso? No podrás deshacerlo desde aquí.")) return;
const stAnulado = systemStatuses.find(st => st.name.toLowerCase().includes('anula'));
if (!stAnulado) return alert("El sistema no tiene un estado 'Anulado' configurado. Avisa a la oficina.");
const btn = document.getElementById('btnAnularBolsa');
const originalContent = btn.innerHTML;
btn.innerHTML = `<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i> Anulando...`;
btn.disabled = true;
try {
const res = await fetch(`${API_URL}/providers/scraped/${id}`, {
method: 'PUT',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify({ status_operativo: String(stAnulado.id) })
});
if (res.ok) {
showToast("Aviso anulado");
closeModal();
loadAvailableServices();
} else {
alert("Error al anular.");
}
} catch (e) {
alert("Error de conexión");
} finally {
btn.innerHTML = originalContent;
btn.disabled = false;
lucide.createIcons();
}
}
// 🔥 FUNCIÓN DE FILTRADO Y ORDENACIÓN 🔥 // 🔥 FUNCIÓN DE FILTRADO Y ORDENACIÓN 🔥
function filterServices() { function filterServices() {
const query = document.getElementById('searchInput').value.toLowerCase().trim(); const query = document.getElementById('searchInput').value.toLowerCase().trim();