Actualizar asignados.html

This commit is contained in:
2026-02-24 21:13:25 +00:00
parent 83f18de695
commit 755abec6c7

View File

@@ -40,7 +40,7 @@
</head>
<body class="text-slate-800 font-sans antialiased h-screen flex flex-col overflow-hidden relative">
<header class="bg-white px-5 pt-8 pb-4 shadow-sm z-20 shrink-0 border-b border-slate-100 flex justify-between items-center">
<header class="bg-white px-5 pt-8 pb-4 shadow-sm z-20 shrink-0 border-b border-slate-100 flex justify-between items-center relative">
<div>
<p class="text-[10px] font-black text-primary-dynamic uppercase tracking-widest mb-0.5">Por Agendar</p>
<h1 class="text-2xl font-black tracking-tight text-slate-800 leading-none">Sin Cita</h1>
@@ -53,10 +53,29 @@
<main class="flex-1 overflow-y-auto no-scrollbar p-5 relative z-10" id="mainArea">
<div id="loader" class="text-center py-10 opacity-50">
<i data-lucide="loader-2" class="w-8 h-8 animate-spin mx-auto text-primary-dynamic mb-2"></i>
<p class="text-xs font-bold uppercase tracking-widest">Buscando pendientes...</p>
<p class="text-xs font-bold uppercase tracking-widest mt-2 text-slate-400">Sincronizando...</p>
</div>
<div id="servicesList" class="space-y-4 pb-24 hidden fade-in">
<div id="contentWrapper" class="hidden pb-24 fade-in">
<div id="requestsSection" class="mb-6 hidden">
<h2 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-3 flex items-center gap-2">
<span class="relative flex h-2.5 w-2.5">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-blue-500"></span>
</span>
Citas Solicitadas
</h2>
<div id="requestsList" class="space-y-4">
</div>
</div>
<div id="noDateSection">
<h2 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-3">Pendientes de Fecha</h2>
<div id="servicesList" class="space-y-4">
</div>
</div>
</div>
</main>
@@ -85,7 +104,7 @@
<span id="detRef" class="text-[10px] font-black text-slate-400 uppercase tracking-widest"></span>
<h3 class="font-black text-xl text-slate-800 uppercase leading-none" id="detName"></h3>
</div>
<button onclick="closeModal()" class="w-8 h-8 bg-slate-100 rounded-full flex items-center justify-center text-slate-500 hover:text-red-500"><i data-lucide="x" class="w-4 h-4"></i></button>
<button onclick="closeModal('actionModal', 'modalContent')" class="w-8 h-8 bg-slate-100 rounded-full flex items-center justify-center text-slate-500 hover:text-red-500"><i data-lucide="x" class="w-4 h-4"></i></button>
</div>
<div class="overflow-y-auto no-scrollbar flex-1 pb-4">
@@ -111,7 +130,6 @@
<hr class="border-slate-100 mb-6">
<div class="bg-white border border-slate-200 rounded-[1.5rem] p-5 shadow-sm space-y-5">
<div>
<div class="flex justify-between items-center mb-2">
<p class="text-[10px] font-black text-primary-dynamic uppercase tracking-widest flex items-center gap-1.5"><i data-lucide="clock" class="w-4 h-4"></i> Duración Estimada</p>
@@ -133,21 +151,17 @@
<div>
<p class="text-[10px] font-black text-primary-dynamic uppercase tracking-widest mb-2 flex items-center gap-1.5"><i data-lucide="calendar" class="w-4 h-4"></i> Seleccionar Día</p>
<div id="dayCarousel" class="flex overflow-x-auto gap-2 pb-2 no-scrollbar">
</div>
<div id="dayCarousel" class="flex overflow-x-auto gap-2 pb-2 no-scrollbar"></div>
</div>
<div>
<p class="text-[10px] font-black text-primary-dynamic uppercase tracking-widest mb-2 flex items-center gap-1.5"><i data-lucide="watch" class="w-4 h-4"></i> Huecos Disponibles</p>
<div id="timeGrid" class="grid grid-cols-4 gap-2">
</div>
<div id="timeGrid" class="grid grid-cols-4 gap-2"></div>
<p id="noSlotsMsg" class="hidden text-xs text-rose-500 font-bold bg-rose-50 p-3 rounded-xl border border-rose-100 text-center mt-2">
No hay huecos libres para esta duración.
</p>
</div>
</div>
</div>
<div class="pt-4 shrink-0 bg-white border-t border-slate-100">
@@ -155,7 +169,53 @@
<i data-lucide="calendar-check" class="w-5 h-5"></i> Confirmar Cita
</button>
</div>
</div>
</div>
<div id="approveModal" class="fixed inset-0 bg-slate-900/60 z-[110] hidden flex-col justify-end transition-opacity duration-300 opacity-0">
<div id="approveModalContent" class="bg-white rounded-t-[2rem] p-6 pb-12 transform translate-y-full transition-transform duration-300 shadow-2xl">
<div class="flex justify-between items-center mb-6">
<div>
<span id="appRef" class="text-[10px] font-black text-slate-400 uppercase tracking-widest"></span>
<h3 class="font-black text-xl text-slate-800 uppercase leading-none" id="appName"></h3>
</div>
<button onclick="closeModal('approveModal', 'approveModalContent')" class="w-8 h-8 bg-slate-100 rounded-full flex items-center justify-center text-slate-500 hover:text-red-500"><i data-lucide="x" class="w-4 h-4"></i></button>
</div>
<div class="bg-blue-50 border border-blue-100 p-5 rounded-2xl mb-6 shadow-inner text-center">
<p class="text-[10px] font-black text-blue-600 uppercase tracking-widest mb-1">Fecha Solicitada por el Cliente</p>
<h4 class="text-2xl font-black text-slate-800" id="appDate">--/--/----</h4>
<p class="text-sm font-bold text-slate-600 mt-1" id="appTime">--:--</p>
</div>
<input type="hidden" id="appId">
<div class="mb-8">
<p class="text-[10px] font-black text-slate-500 uppercase tracking-widest mb-2 ml-1">¿Cuánto tiempo vas a tardar?</p>
<div class="relative">
<select id="appDurationInput" class="w-full bg-slate-50 border border-slate-200 p-4 rounded-xl text-sm font-black text-slate-700 outline-none focus:ring-2 focus:ring-blue-500 appearance-none pr-10">
<option value="15">15 Minutos (Muy rápido)</option>
<option value="30">30 Minutos</option>
<option value="45">45 Minutos</option>
<option value="60" selected>1 Hora (Estándar)</option>
<option value="90">1 Hora y Media</option>
<option value="120">2 Horas</option>
<option value="180">3 Horas</option>
<option value="240">4 Horas (Media jornada)</option>
</select>
<i data-lucide="chevron-down" class="w-5 h-5 absolute right-4 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none"></i>
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<button onclick="rejectRequest()" id="btnReject" class="bg-white border-2 border-rose-200 text-rose-600 font-black py-4 rounded-xl hover:bg-rose-50 flex items-center justify-center gap-2 transition-all text-xs uppercase tracking-widest">
<i data-lucide="x" class="w-4 h-4"></i> Rechazar
</button>
<button onclick="approveRequest()" id="btnApprove" class="bg-blue-600 text-white font-black py-4 rounded-xl shadow-lg shadow-blue-500/30 hover:bg-blue-700 flex items-center justify-center gap-2 transition-all text-xs uppercase tracking-widest">
<i data-lucide="check" class="w-4 h-4"></i> Aceptar
</button>
</div>
<p class="text-center text-[9px] font-bold text-slate-400 mt-4">Al aceptar, se enviará un WhatsApp al cliente confirmando la cita automáticamente.</p>
</div>
</div>
@@ -170,7 +230,8 @@
: 'https://integrarepara-api.integrarepara.es';
let localServices = []; // Solo los "Sin Cita"
let globalAllServices = []; // TODOS los servicios para calcular solapamientos
let globalAllServices = []; // TODOS para solapamientos
let pendingRequests = []; // Solicitudes del portal
let systemStatuses = [];
let bizHours = { m_start: '09:00', m_end: '14:00', a_start: '16:00', a_end: '19:00' };
@@ -188,11 +249,8 @@
const data = await res.json();
if(data.ok && data.config && data.config.portal_settings) {
// Extraemos horario comercial
const ps = data.config.portal_settings;
if(ps.m_start) bizHours = { m_start: ps.m_start, m_end: ps.m_end, a_start: ps.a_start, a_end: ps.a_end };
// Extraemos colores
if(ps.app_settings) {
theme = ps.app_settings;
localStorage.setItem('app_theme', JSON.stringify(theme));
@@ -207,7 +265,7 @@
}
document.addEventListener("DOMContentLoaded", async () => {
if (!localStorage.getItem("token") || localStorage.getItem("role") !== 'operario') {
if (!localStorage.getItem("token") || (localStorage.getItem("role") !== 'operario' && localStorage.getItem("role") !== 'operario_cerrado')) {
window.location.href = "index.html"; return;
}
await applyTheme();
@@ -225,23 +283,32 @@
}
async function refreshData() {
document.getElementById('servicesList').classList.add('hidden');
document.getElementById('contentWrapper').classList.add('hidden');
document.getElementById('loader').classList.remove('hidden');
try {
const res = await fetch(`${API_URL}/services/active`, {
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
});
const data = await res.json();
const headers = { "Authorization": `Bearer ${localStorage.getItem("token")}` };
if (data.ok) {
globalAllServices = data.services; // Guardamos TODOS para chequear la agenda real
// 1. Cargar servicios activos del operario
const resActivos = await fetch(`${API_URL}/services/active`, { headers });
const dataActivos = await resActivos.json();
// 2. Cargar peticiones de cita pendientes de este operario
const resReqs = await fetch(`${API_URL}/agenda/requests`, { headers });
const dataReqs = await resReqs.json();
if (dataActivos.ok && dataReqs.ok) {
globalAllServices = dataActivos.services;
pendingRequests = dataReqs.requests;
// Filtramos los SIN CITA
localServices = data.services.filter(s => {
// Filtrar la lista normal de "Sin Cita"
localServices = globalAllServices.filter(s => {
if (s.provider === 'SYSTEM_BLOCK') return false;
const raw = s.raw_data || {};
// Si ya tiene fecha o si tiene una solicitud pendiente, no lo mostramos en el listado base
if (raw.scheduled_date && raw.scheduled_date.trim() !== "") return false;
if (raw.appointment_status === 'pending') return false;
if (raw.status_operativo) {
const st = systemStatuses.find(x => String(x.id) === String(raw.status_operativo));
if (st && st.is_final) return false;
@@ -249,22 +316,151 @@
return true;
});
renderRequests();
renderServices();
}
} catch (e) {
alert("Error de conexión");
} finally {
document.getElementById('loader').classList.add('hidden');
document.getElementById('servicesList').classList.remove('hidden');
document.getElementById('contentWrapper').classList.remove('hidden');
}
}
// ==========================================
// RENDERIZADO DE SOLICITUDES PENDIENTES
// ==========================================
function renderRequests() {
const reqSection = document.getElementById('requestsSection');
const reqList = document.getElementById('requestsList');
reqList.innerHTML = '';
if (pendingRequests.length === 0) {
reqSection.classList.add('hidden');
return;
}
reqSection.classList.remove('hidden');
pendingRequests.forEach(req => {
const raw = req.raw_data || {};
const name = raw["Nombre Cliente"] || raw["CLIENTE"] || "Cliente";
const rDate = formatDate(raw.requested_date);
const rTime = addOneHour(raw.requested_time); // Calculamos el tramo para mostrarlo
reqList.innerHTML += `
<div class="bg-blue-50 border border-blue-200 p-4 rounded-3xl flex justify-between items-center shadow-sm relative overflow-hidden">
<div class="absolute right-0 top-0 w-16 h-16 bg-blue-100 rounded-bl-full opacity-50 z-0"></div>
<div class="relative z-10 flex-1">
<p class="text-[9px] font-black text-blue-500 uppercase tracking-widest mb-0.5">Cita Solicitada</p>
<h3 class="font-black text-slate-800 text-sm leading-tight truncate">${name}</h3>
<p class="text-[10px] font-bold text-slate-600 mt-1">${rDate} | ${raw.requested_time} - ${rTime}</p>
</div>
<button onclick="openApproveModal(${req.id})" class="bg-blue-600 text-white w-10 h-10 rounded-full flex items-center justify-center shadow-lg active:scale-95 transition-transform shrink-0 ml-3 relative z-10">
<i data-lucide="chevron-right" class="w-5 h-5"></i>
</button>
</div>
`;
});
lucide.createIcons();
}
function openApproveModal(id) {
const req = pendingRequests.find(r => r.id === id);
if(!req) return;
const raw = req.raw_data || {};
document.getElementById('appId').value = id;
document.getElementById('appRef').innerText = `Exp. #${req.service_ref || "S/R"}`;
document.getElementById('appName').innerText = raw["Nombre Cliente"] || "Cliente";
document.getElementById('appDate').innerText = formatDate(raw.requested_date);
document.getElementById('appTime').innerText = `Llegada aprox: ${raw.requested_time} - ${addOneHour(raw.requested_time)}`;
// Reseteamos el selector de duración a 1 hora por defecto
document.getElementById('appDurationInput').value = "60";
const modal = document.getElementById('approveModal');
const content = document.getElementById('approveModalContent');
modal.classList.remove('hidden');
void modal.offsetWidth;
modal.classList.remove('opacity-0');
content.classList.remove('translate-y-full');
}
async function approveRequest() {
const id = document.getElementById('appId').value;
const duration = document.getElementById('appDurationInput').value;
const btn = document.getElementById('btnApprove');
const originalContent = btn.innerHTML;
btn.innerHTML = `<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i>`;
btn.disabled = true;
try {
const res = await fetch(`${API_URL}/agenda/requests/${id}/approve`, {
method: 'POST',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify({ duration: duration })
});
if (res.ok) {
showToast("Cita Aceptada");
closeModal('approveModal', 'approveModalContent');
refreshData();
} else {
alert("Error al confirmar.");
}
} catch (e) {
alert("Error de conexión");
} finally {
btn.innerHTML = originalContent;
btn.disabled = false;
lucide.createIcons();
}
}
async function rejectRequest() {
if(!confirm("¿Seguro que quieres rechazar este horario? El cliente recibirá un mensaje para elegir otro.")) return;
const id = document.getElementById('appId').value;
const btn = document.getElementById('btnReject');
const originalContent = btn.innerHTML;
btn.innerHTML = `<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i>`;
btn.disabled = true;
try {
const res = await fetch(`${API_URL}/agenda/requests/${id}/reject`, {
method: 'POST',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` }
});
if (res.ok) {
showToast("Cita Rechazada");
closeModal('approveModal', 'approveModalContent');
refreshData();
} else {
alert("Error al rechazar.");
}
} catch (e) {
alert("Error de conexión");
} finally {
btn.innerHTML = originalContent;
btn.disabled = false;
lucide.createIcons();
}
}
// ==========================================
// RENDERIZADO DE LISTADO NORMAL (SIN CITA)
// ==========================================
function renderServices() {
const container = document.getElementById('servicesList');
const noDateSec = document.getElementById('noDateSection');
if (localServices.length === 0) {
container.innerHTML = `
<div class="text-center py-12 bg-white rounded-3xl border border-slate-100 shadow-sm mt-4">
<div class="text-center py-12 bg-white rounded-3xl border border-slate-100 shadow-sm mt-2">
<div class="w-16 h-16 bg-emerald-50 text-emerald-500 rounded-full flex items-center justify-center mx-auto mb-4">
<i data-lucide="check-check" class="w-8 h-8"></i>
</div>
@@ -272,9 +468,19 @@
<p class="text-xs text-slate-400 font-medium mt-1">No tienes avisos pendientes de agendar.</p>
</div>`;
lucide.createIcons();
// Si tampoco hay peticiones, ocultamos el título "Pendientes de fecha"
if (pendingRequests.length === 0) {
noDateSec.querySelector('h2').classList.add('hidden');
} else {
noDateSec.classList.add('hidden');
}
return;
}
noDateSec.classList.remove('hidden');
noDateSec.querySelector('h2').classList.remove('hidden');
container.innerHTML = localServices.map(s => {
const raw = s.raw_data || {};
const name = raw["Nombre Cliente"] || raw["CLIENTE"] || "Asegurado";
@@ -327,6 +533,26 @@
let min = (m % 60).toString().padStart(2, '0');
return `${h}:${min}`;
}
function addOneHour(timeStr) {
if(!timeStr) return "";
let [h, m] = timeStr.split(':').map(Number);
let totalMins = h * 60 + m + 60;
let newH = Math.floor(totalMins / 60);
let newM = totalMins % 60;
return `${String(newH).padStart(2,'0')}:${String(newM).padStart(2,'0')}`;
}
function formatDate(dateStr) {
if (!dateStr) return "";
try {
const parts = dateStr.split('-');
if(parts.length !== 3) return dateStr;
const d = new Date(parts[0], parts[1]-1, parts[2]);
const opciones = { weekday: 'long', day: 'numeric', month: 'short' };
return d.toLocaleDateString('es-ES', opciones);
} catch(e) { return dateStr; }
}
function buildDayCarousel() {
const container = document.getElementById('dayCarousel');
@@ -419,8 +645,8 @@
let startMins = timeToMins(startStr);
let endMins = timeToMins(endStr);
// Saltos de 15 minutos
for (let m = startMins; m + duration <= endMins; m += 15) {
// Saltos de 30 minutos (igual que en buscar.html)
for (let m = startMins; m + duration <= endMins; m += 30) {
if (!isOverlapping(m, m + duration, occupied)) {
const timeStr = minsToTime(m);
const isSelected = timeStr === pickerSelectedTime;
@@ -447,7 +673,7 @@
grid.classList.remove('hidden');
}
// Si la hora que estaba seleccionada ya no sale (porque ampliaron la duración), la borramos
// Si la hora que estaba seleccionada ya no sale, la borramos
if (pickerSelectedTime && !grid.innerHTML.includes(`'${pickerSelectedTime}'`)) {
pickerSelectedTime = "";
}
@@ -456,7 +682,7 @@
}
// ==========================================
// APERTURA DE MODAL Y GUARDADO
// APERTURA DE MODAL Y GUARDADO (MANUAL)
// ==========================================
function openActionModal(id) {
const s = localServices.find(x => x.id === id);
@@ -487,9 +713,9 @@
content.classList.remove('translate-y-full');
}
function closeModal() {
const modal = document.getElementById('actionModal');
const content = document.getElementById('modalContent');
function closeModal(modalId, contentId) {
const modal = document.getElementById(modalId);
const content = document.getElementById(contentId);
modal.classList.add('opacity-0');
content.classList.add('translate-y-full');
@@ -537,7 +763,7 @@
if (res.ok) {
showToast("Cita Agendada en Calendario");
closeModal();
closeModal('actionModal', 'modalContent');
refreshData();
} else {
alert("Error al guardar la cita.");