Actualizar asignados.html

This commit is contained in:
2026-02-24 22:14:16 +00:00
parent 3fc45ae82c
commit 725542dc34

View File

@@ -28,7 +28,7 @@
/* Carrusel de fechas */ /* Carrusel de fechas */
.day-chip { transition: all 0.2s; border: 2px solid transparent; } .day-chip { transition: all 0.2s; border: 2px solid transparent; }
.day-chip.active { background-color: var(--primary) !important; color: white !important; border-color: var(--primary) !important; transform: scale(1.05); box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1); } .day-chip.active { background-color: var(--primary) !important; color: white !important; border-color: var(--primary) !important; transform: scale(1.05); shadow: 0 4px 6px -1px rgba(0,0,0,0.1); }
.day-chip.inactive { background-color: #f8fafc; color: #64748b; border-color: #e2e8f0; } .day-chip.inactive { background-color: #f8fafc; color: #64748b; border-color: #e2e8f0; }
/* Chips de Horas */ /* Chips de Horas */
@@ -182,8 +182,9 @@
<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> <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>
<div class="bg-slate-50 border border-slate-200 p-5 rounded-2xl mb-6 shadow-inner text-center"> <div class="bg-slate-50 border-2 border-primary-dynamic p-5 rounded-2xl mb-6 shadow-inner text-center relative overflow-hidden">
<p class="text-[10px] font-black text-emerald-600 uppercase tracking-widest mb-1">Fecha Solicitada por el Cliente</p> <div class="absolute left-0 top-0 bottom-0 w-2 bg-primary-dynamic"></div>
<p class="text-[10px] font-black text-primary-dynamic uppercase tracking-widest mb-1">Fecha Solicitada por el Cliente</p>
<h4 class="text-2xl font-black text-slate-800" id="appDate">--/--/----</h4> <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> <p class="text-sm font-bold text-slate-600 mt-1" id="appTime">--:--</p>
</div> </div>
@@ -193,7 +194,7 @@
<div class="mb-8"> <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> <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"> <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-emerald-500 appearance-none pr-10"> <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-primary-dynamic appearance-none pr-10">
<option value="15">15 Minutos (Muy rápido)</option> <option value="15">15 Minutos (Muy rápido)</option>
<option value="30">30 Minutos</option> <option value="30">30 Minutos</option>
<option value="45">45 Minutos</option> <option value="45">45 Minutos</option>
@@ -211,7 +212,7 @@
<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"> <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 <i data-lucide="x" class="w-4 h-4"></i> Rechazar
</button> </button>
<button onclick="approveRequest()" id="btnApprove" class="bg-emerald-500 text-white font-black py-4 rounded-xl shadow-lg shadow-emerald-500/30 hover:bg-emerald-600 flex items-center justify-center gap-2 transition-all text-xs uppercase tracking-widest"> <button onclick="approveRequest()" id="btnApprove" class="bg-primary-dynamic text-white font-black py-4 rounded-xl shadow-lg hover:opacity-90 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 <i data-lucide="check" class="w-4 h-4"></i> Aceptar
</button> </button>
</div> </div>
@@ -229,9 +230,9 @@
? 'http://localhost:3000' ? 'http://localhost:3000'
: 'https://integrarepara-api.integrarepara.es'; : 'https://integrarepara-api.integrarepara.es';
let localServices = []; // Solo los "Sin Cita" let localServices = [];
let globalAllServices = []; // TODOS para solapamientos let globalAllServices = [];
let pendingRequests = []; // Solicitudes del portal let pendingRequests = [];
let systemStatuses = []; let systemStatuses = [];
let bizHours = { m_start: '09:00', m_end: '14:00', a_start: '16:00', a_end: '19:00' }; let bizHours = { m_start: '09:00', m_end: '14:00', a_start: '16:00', a_end: '19:00' };
@@ -239,7 +240,6 @@
let pickerSelectedDate = ""; let pickerSelectedDate = "";
let pickerSelectedTime = ""; let pickerSelectedTime = "";
// --- SISTEMA DE TEMA DINÁMICO ---
async function applyTheme() { async function applyTheme() {
try { try {
let theme = JSON.parse(localStorage.getItem('app_theme')); let theme = JSON.parse(localStorage.getItem('app_theme'));
@@ -261,7 +261,7 @@
document.documentElement.style.setProperty('--secondary', theme.secondary); document.documentElement.style.setProperty('--secondary', theme.secondary);
document.documentElement.style.setProperty('--app-bg', theme.bg); document.documentElement.style.setProperty('--app-bg', theme.bg);
} }
} catch (e) { console.warn("Usando tema y horario por defecto"); } } catch (e) { console.warn("Usando tema por defecto"); }
} }
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
@@ -279,7 +279,7 @@
const res = await fetch(`${API_URL}/statuses`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } }); const res = await fetch(`${API_URL}/statuses`, { headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` } });
const data = await res.json(); const data = await res.json();
if (data.ok) systemStatuses = data.statuses; if (data.ok) systemStatuses = data.statuses;
} catch (e) { console.error("Error estados:", e); } } catch (e) { }
} }
async function refreshData() { async function refreshData() {
@@ -289,11 +289,9 @@
try { try {
const headers = { "Authorization": `Bearer ${localStorage.getItem("token")}` }; const headers = { "Authorization": `Bearer ${localStorage.getItem("token")}` };
// 1. Cargar servicios activos del operario
const resActivos = await fetch(`${API_URL}/services/active`, { headers }); const resActivos = await fetch(`${API_URL}/services/active`, { headers });
const dataActivos = await resActivos.json(); const dataActivos = await resActivos.json();
// 2. Cargar peticiones de cita pendientes de este operario
const resReqs = await fetch(`${API_URL}/agenda/requests`, { headers }); const resReqs = await fetch(`${API_URL}/agenda/requests`, { headers });
const dataReqs = await resReqs.json(); const dataReqs = await resReqs.json();
@@ -301,11 +299,9 @@
globalAllServices = dataActivos.services; globalAllServices = dataActivos.services;
pendingRequests = dataReqs.requests; pendingRequests = dataReqs.requests;
// Filtrar la lista normal de "Sin Cita"
localServices = globalAllServices.filter(s => { localServices = globalAllServices.filter(s => {
if (s.provider === 'SYSTEM_BLOCK') return false; if (s.provider === 'SYSTEM_BLOCK') return false;
const raw = s.raw_data || {}; 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.scheduled_date && raw.scheduled_date.trim() !== "") return false;
if (raw.appointment_status === 'pending') return false; if (raw.appointment_status === 'pending') return false;
@@ -348,21 +344,19 @@
const rDate = formatDate(raw.requested_date); const rDate = formatDate(raw.requested_date);
const rTime = addOneHour(raw.requested_time); const rTime = addOneHour(raw.requested_time);
// Extraer descripción y teléfono
const desc = raw["Descripción"] || raw["DESCRIPCION"] || "Sin notas de avería."; const desc = raw["Descripción"] || raw["DESCRIPCION"] || "Sin notas de avería.";
const rawPhone = raw["Teléfono"] || raw["TELEFONOS"] || raw["TELEFONO"] || ""; const rawPhone = raw["Teléfono"] || raw["TELEFONOS"] || raw["TELEFONO"] || "";
const matchPhone = rawPhone.toString().match(/[6789]\d{8}/); const matchPhone = rawPhone.toString().match(/[6789]\d{8}/);
const safePhone = matchPhone ? matchPhone[0] : ""; const safePhone = matchPhone ? matchPhone[0] : "";
// DISEÑO ACTUALIZADO CON ACENTOS VERDES
reqList.innerHTML += ` reqList.innerHTML += `
<div class="bg-white border-2 border-emerald-400 p-5 rounded-[2rem] flex flex-col shadow-md relative overflow-hidden gap-3"> <div class="bg-white border-2 border-primary-dynamic p-5 rounded-[2rem] flex flex-col shadow-md relative overflow-hidden gap-3">
<div class="absolute right-0 top-0 w-24 h-24 bg-emerald-50 rounded-bl-full opacity-60 z-0"></div> <div class="absolute right-0 top-0 w-24 h-24 bg-primary-dynamic rounded-bl-full opacity-10 z-0"></div>
<div class="relative z-10"> <div class="relative z-10">
<p class="text-[9px] font-black text-emerald-600 uppercase tracking-widest mb-0.5">Cita Solicitada</p> <p class="text-[9px] font-black text-primary-dynamic uppercase tracking-widest mb-0.5">Cita Solicitada</p>
<h3 class="font-black text-slate-800 text-base leading-tight truncate pr-8">${name}</h3> <h3 class="font-black text-slate-800 text-base leading-tight truncate pr-8">${name}</h3>
<p class="text-[10px] font-bold text-slate-600 mt-1 flex items-center gap-1"><i data-lucide="calendar" class="w-3 h-3 text-emerald-500"></i> ${rDate} | ${raw.requested_time} - ${rTime}</p> <p class="text-[10px] font-bold text-slate-600 mt-1 flex items-center gap-1"><i data-lucide="calendar" class="w-3 h-3 text-primary-dynamic"></i> ${rDate} | ${raw.requested_time} - ${rTime}</p>
</div> </div>
<div class="relative z-10 bg-slate-50 p-3 rounded-2xl border border-slate-100 mt-1"> <div class="relative z-10 bg-slate-50 p-3 rounded-2xl border border-slate-100 mt-1">
@@ -370,10 +364,10 @@
</div> </div>
<div class="flex gap-2 relative z-10 mt-1"> <div class="flex gap-2 relative z-10 mt-1">
<button onclick="quickCallInline('${safePhone}')" class="flex-1 bg-white border border-slate-200 text-slate-600 hover:bg-slate-50 font-black py-3 rounded-xl flex items-center justify-center gap-1.5 active:scale-95 transition-transform shadow-sm text-[10px] uppercase tracking-widest"> <button onclick="quickCallInline('${safePhone}')" class="flex-1 bg-white border border-primary-dynamic text-primary-dynamic hover:bg-slate-50 font-black py-3 rounded-xl flex items-center justify-center gap-1.5 active:scale-95 transition-transform shadow-sm text-[10px] uppercase tracking-widest">
<i data-lucide="phone" class="w-4 h-4"></i> Llamar <i data-lucide="phone" class="w-4 h-4"></i> Llamar
</button> </button>
<button onclick="openApproveModal(${req.id})" class="flex-1 bg-emerald-500 text-white hover:bg-emerald-600 font-black py-3 rounded-xl flex items-center justify-center gap-1.5 shadow-md active:scale-95 transition-transform text-[10px] uppercase tracking-widest"> <button onclick="openApproveModal(${req.id})" class="flex-1 bg-primary-dynamic text-white hover:opacity-90 font-black py-3 rounded-xl flex items-center justify-center gap-1.5 shadow-md active:scale-95 transition-transform text-[10px] uppercase tracking-widest">
Revisar <i data-lucide="chevron-right" class="w-4 h-4"></i> Revisar <i data-lucide="chevron-right" class="w-4 h-4"></i>
</button> </button>
</div> </div>
@@ -383,10 +377,9 @@
lucide.createIcons(); lucide.createIcons();
} }
// Helper rápido para el botón de llamada
function quickCallInline(phone) { function quickCallInline(phone) {
if (phone) window.location.href = `tel:+34${phone}`; if (phone) window.location.href = `tel:+34${phone}`;
else alert("No hay un teléfono válido guardado para este cliente."); else alert("No hay un teléfono válido guardado.");
} }
function openApproveModal(id) { function openApproveModal(id) {
@@ -400,8 +393,6 @@
document.getElementById('appDate').innerText = formatDate(raw.requested_date); document.getElementById('appDate').innerText = formatDate(raw.requested_date);
document.getElementById('appTime').innerText = `Llegada aprox: ${raw.requested_time} - ${addOneHour(raw.requested_time)}`; 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"; document.getElementById('appDurationInput').value = "60";
const modal = document.getElementById('approveModal'); const modal = document.getElementById('approveModal');
@@ -415,7 +406,6 @@
async function approveRequest() { async function approveRequest() {
const id = document.getElementById('appId').value; const id = document.getElementById('appId').value;
const duration = document.getElementById('appDurationInput').value; const duration = document.getElementById('appDurationInput').value;
const btn = document.getElementById('btnApprove'); const btn = document.getElementById('btnApprove');
const originalContent = btn.innerHTML; const originalContent = btn.innerHTML;
btn.innerHTML = `<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i>`; btn.innerHTML = `<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i>`;
@@ -427,26 +417,17 @@
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` }, headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` },
body: JSON.stringify({ duration: duration }) body: JSON.stringify({ duration: duration })
}); });
if (res.ok) { if (res.ok) {
showToast("Cita Aceptada"); showToast("Cita Aceptada");
closeModal('approveModal', 'approveModalContent'); closeModal('approveModal', 'approveModalContent');
refreshData(); refreshData();
} else { } else { alert("Error al confirmar."); }
alert("Error al confirmar."); } catch (e) { alert("Error de conexión"); }
} finally { btn.innerHTML = originalContent; btn.disabled = false; lucide.createIcons(); }
} catch (e) {
alert("Error de conexión");
} finally {
btn.innerHTML = originalContent;
btn.disabled = false;
lucide.createIcons();
}
} }
async function rejectRequest() { async function rejectRequest() {
if(!confirm("¿Seguro que quieres rechazar este horario? El cliente recibirá un mensaje para elegir otro.")) return; if(!confirm("¿Seguro que quieres rechazar este horario? El cliente recibirá un mensaje para elegir otro.")) return;
const id = document.getElementById('appId').value; const id = document.getElementById('appId').value;
const btn = document.getElementById('btnReject'); const btn = document.getElementById('btnReject');
const originalContent = btn.innerHTML; const originalContent = btn.innerHTML;
@@ -458,26 +439,27 @@
method: 'POST', method: 'POST',
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` } headers: { "Content-Type": "application/json", "Authorization": `Bearer ${localStorage.getItem("token")}` }
}); });
if (res.ok) { if (res.ok) {
showToast("Cita Rechazada"); showToast("Cita Rechazada");
closeModal('approveModal', 'approveModalContent'); closeModal('approveModal', 'approveModalContent');
refreshData(); refreshData();
} else { } else { alert("Error al rechazar."); }
alert("Error al rechazar."); } catch (e) { alert("Error de conexión"); }
} finally { btn.innerHTML = originalContent; btn.disabled = false; lucide.createIcons(); }
} catch (e) {
alert("Error de conexión");
} finally {
btn.innerHTML = originalContent;
btn.disabled = false;
lucide.createIcons();
}
} }
// ========================================== // ==========================================
// RENDERIZADO DE LISTADO NORMAL (SIN CITA) // RENDERIZADO DE LISTADO NORMAL (SIN CITA)
// ========================================== // ==========================================
function calculateDelayDays(createdAtStr) {
if (!createdAtStr) return 0;
const createdDate = new Date(createdAtStr);
const now = new Date();
const diffTime = Math.abs(now - createdDate);
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
return diffDays;
}
function renderServices() { function renderServices() {
const container = document.getElementById('servicesList'); const container = document.getElementById('servicesList');
const noDateSec = document.getElementById('noDateSection'); const noDateSec = document.getElementById('noDateSection');
@@ -492,12 +474,8 @@
<p class="text-xs text-slate-400 font-medium mt-1">No tienes avisos pendientes de agendar.</p> <p class="text-xs text-slate-400 font-medium mt-1">No tienes avisos pendientes de agendar.</p>
</div>`; </div>`;
lucide.createIcons(); lucide.createIcons();
if (pendingRequests.length === 0) noDateSec.querySelector('h2').classList.add('hidden');
if (pendingRequests.length === 0) { else noDateSec.classList.add('hidden');
noDateSec.querySelector('h2').classList.add('hidden');
} else {
noDateSec.classList.add('hidden');
}
return; return;
} }
@@ -512,15 +490,24 @@
const isUrgent = s.is_urgent; const isUrgent = s.is_urgent;
const company = raw["Compañía"] || raw["Procedencia"] || "Particular"; const company = raw["Compañía"] || raw["Procedencia"] || "Particular";
const calledTimes = parseInt(raw.called_times || 0);
const delayDays = calculateDelayDays(s.created_at);
let delayHtml = '';
if (delayDays > 1) {
delayHtml = `<span class="bg-rose-100 text-rose-600 text-[8px] font-black px-2 py-0.5 rounded uppercase tracking-widest ml-2">Retraso: ${delayDays} días</span>`;
}
return ` return `
<div onclick="openActionModal(${s.id})" class="bg-white p-5 rounded-3xl border border-slate-200 shadow-sm active:scale-95 transition-transform flex gap-4 items-center relative overflow-hidden"> <div class="bg-white p-5 rounded-3xl border border-slate-200 shadow-sm relative overflow-hidden mb-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 onclick="openActionModal(${s.id})" class="flex gap-4 items-center active:scale-95 transition-transform cursor-pointer">
<div class="w-12 h-12 bg-primary-dynamic/10 rounded-2xl flex flex-col items-center justify-center text-primary-dynamic shrink-0 border border-primary-dynamic/20"> <div class="w-12 h-12 bg-primary-dynamic/10 rounded-2xl flex flex-col items-center justify-center text-primary-dynamic shrink-0 border border-primary-dynamic/20">
<i data-lucide="clock" class="w-5 h-5"></i> <i data-lucide="clock" class="w-5 h-5"></i>
</div> </div>
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<p class="text-[9px] font-black text-blue-500 uppercase tracking-widest mb-0.5 truncate">${company}</p> <p class="text-[9px] font-black text-blue-500 uppercase tracking-widest mb-0.5 truncate">${company} ${delayHtml}</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>
<p class="text-[10px] font-bold text-slate-500 mt-1 truncate uppercase flex items-center gap-1"> <p class="text-[10px] font-bold text-slate-500 mt-1 truncate uppercase flex items-center gap-1">
<i data-lucide="map-pin" class="w-3 h-3"></i> ${addr}, ${pop} <i data-lucide="map-pin" class="w-3 h-3"></i> ${addr}, ${pop}
@@ -530,12 +517,47 @@
<i data-lucide="chevron-right"></i> <i data-lucide="chevron-right"></i>
</div> </div>
</div> </div>
<div class="mt-4 pt-4 border-t border-slate-100">
<button onclick="markClientNotFound(${s.id}, ${calledTimes})" class="w-full bg-orange-50 border border-orange-100 hover:bg-orange-100 text-orange-600 font-black py-2.5 rounded-xl flex items-center justify-center gap-2 transition-colors text-[10px] uppercase tracking-widest">
<i data-lucide="bell-ring" class="w-4 h-4"></i> No Localizado (${calledTimes})
</button>
</div>
</div>
`; `;
}).join(''); }).join('');
lucide.createIcons(); lucide.createIcons();
} }
async function markClientNotFound(serviceId, currentTimes) {
const btn = event.currentTarget;
const originalHtml = btn.innerHTML;
btn.innerHTML = `<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i> Enviando...`;
btn.disabled = true;
try {
const res = await fetch(`${API_URL}/services/not-found/${serviceId}`, {
method: 'POST',
headers: { "Authorization": `Bearer ${localStorage.getItem("token")}` }
});
const data = await res.json();
if (data.ok) {
showToast(`Aviso enviado. Intentos: ${data.called_times}`);
refreshData(); // Recargamos para actualizar el numerito
} else {
alert("Error al registrar la llamada.");
btn.innerHTML = originalHtml;
btn.disabled = false;
}
} catch (e) {
alert("Error de conexión");
btn.innerHTML = originalHtml;
btn.disabled = false;
}
}
// ========================================== // ==========================================
// LÓGICA DEL MOTOR DE AGENDAMIENTO INTELIGENTE // LÓGICA DEL MOTOR DE AGENDAMIENTO INTELIGENTE
// ========================================== // ==========================================
@@ -546,17 +568,8 @@
return `${y}-${m}-${d}`; return `${y}-${m}-${d}`;
} }
function timeToMins(t) { function timeToMins(t) { let [h, m] = t.split(':').map(Number); return h * 60 + m; }
let [h, m] = t.split(':').map(Number); function minsToTime(m) { let h = Math.floor(m / 60).toString().padStart(2, '0'); let min = (m % 60).toString().padStart(2, '0'); return `${h}:${min}`; }
return h * 60 + m;
}
function minsToTime(m) {
let h = Math.floor(m / 60).toString().padStart(2, '0');
let min = (m % 60).toString().padStart(2, '0');
return `${h}:${min}`;
}
function addOneHour(timeStr) { function addOneHour(timeStr) {
if(!timeStr) return ""; if(!timeStr) return "";
let [h, m] = timeStr.split(':').map(Number); let [h, m] = timeStr.split(':').map(Number);
@@ -565,7 +578,6 @@
let newM = totalMins % 60; let newM = totalMins % 60;
return `${String(newH).padStart(2,'0')}:${String(newM).padStart(2,'0')}`; return `${String(newH).padStart(2,'0')}:${String(newM).padStart(2,'0')}`;
} }
function formatDate(dateStr) { function formatDate(dateStr) {
if (!dateStr) return ""; if (!dateStr) return "";
try { try {
@@ -582,19 +594,16 @@
container.innerHTML = ""; container.innerHTML = "";
let today = new Date(); let today = new Date();
for (let i = 0; i < 14; i++) { // 14 días a futuro for (let i = 0; i < 14; i++) {
let d = new Date(today); let d = new Date(today);
d.setDate(today.getDate() + i); d.setDate(today.getDate() + i);
if (d.getDay() === 0) continue;
if (d.getDay() === 0) continue; // Saltar domingos
const isoDate = toISODate(d); const isoDate = toISODate(d);
const dayName = d.toLocaleDateString('es-ES', { weekday: 'short' }).replace('.', '').substring(0,3); const dayName = d.toLocaleDateString('es-ES', { weekday: 'short' }).replace('.', '').substring(0,3);
const dayNum = d.getDate(); const dayNum = d.getDate();
// Seleccionar hoy por defecto
if (!pickerSelectedDate) pickerSelectedDate = isoDate; if (!pickerSelectedDate) pickerSelectedDate = isoDate;
const isSelected = isoDate === pickerSelectedDate; const isSelected = isoDate === pickerSelectedDate;
container.innerHTML += ` container.innerHTML += `
@@ -608,15 +617,15 @@
function selectPickerDate(isoDate) { function selectPickerDate(isoDate) {
pickerSelectedDate = isoDate; pickerSelectedDate = isoDate;
pickerSelectedTime = ""; // Resetear hora al cambiar de día pickerSelectedTime = "";
buildDayCarousel(); // Repintar para actualizar estilos buildDayCarousel();
renderTimeSlots(); renderTimeSlots();
checkSaveButton(); checkSaveButton();
} }
function selectPickerTime(timeStr) { function selectPickerTime(timeStr) {
pickerSelectedTime = timeStr; pickerSelectedTime = timeStr;
renderTimeSlots(); // Repintar para estilos renderTimeSlots();
checkSaveButton(); checkSaveButton();
} }
@@ -648,7 +657,6 @@
function isOverlapping(startMins, endMins, occupiedRanges) { function isOverlapping(startMins, endMins, occupiedRanges) {
for(let r of occupiedRanges) { for(let r of occupiedRanges) {
// Hay solapamiento si mi inicio es antes de que él acabe, Y mi fin es después de que él empiece
if(startMins < r.end && endMins > r.start) return true; if(startMins < r.end && endMins > r.start) return true;
} }
return false; return false;
@@ -663,12 +671,10 @@
grid.innerHTML = ""; grid.innerHTML = "";
let slotsGenerated = 0; let slotsGenerated = 0;
// Función interna para recorrer tramos
const genSlotsForPeriod = (startStr, endStr) => { const genSlotsForPeriod = (startStr, endStr) => {
let startMins = timeToMins(startStr); let startMins = timeToMins(startStr);
let endMins = timeToMins(endStr); let endMins = timeToMins(endStr);
// Saltos de 30 minutos (igual que en buscar.html)
for (let m = startMins; m + duration <= endMins; m += 30) { for (let m = startMins; m + duration <= endMins; m += 30) {
if (!isOverlapping(m, m + duration, occupied)) { if (!isOverlapping(m, m + duration, occupied)) {
const timeStr = minsToTime(m); const timeStr = minsToTime(m);
@@ -683,20 +689,18 @@
} }
}; };
// Generar Mañana y Tarde
genSlotsForPeriod(bizHours.m_start, bizHours.m_end); genSlotsForPeriod(bizHours.m_start, bizHours.m_end);
genSlotsForPeriod(bizHours.a_start, bizHours.a_end); genSlotsForPeriod(bizHours.a_start, bizHours.a_end);
if (slotsGenerated === 0) { if (slotsGenerated === 0) {
msg.classList.remove('hidden'); msg.classList.remove('hidden');
grid.classList.add('hidden'); grid.classList.add('hidden');
pickerSelectedTime = ""; // Reset forzado pickerSelectedTime = "";
} else { } else {
msg.classList.add('hidden'); msg.classList.add('hidden');
grid.classList.remove('hidden'); grid.classList.remove('hidden');
} }
// Si la hora que estaba seleccionada ya no sale, la borramos
if (pickerSelectedTime && !grid.innerHTML.includes(`'${pickerSelectedTime}'`)) { if (pickerSelectedTime && !grid.innerHTML.includes(`'${pickerSelectedTime}'`)) {
pickerSelectedTime = ""; pickerSelectedTime = "";
} }
@@ -704,9 +708,6 @@
checkSaveButton(); checkSaveButton();
} }
// ==========================================
// APERTURA DE MODAL Y GUARDADO (MANUAL)
// ==========================================
function openActionModal(id) { function openActionModal(id) {
const s = localServices.find(x => x.id === id); const s = localServices.find(x => x.id === id);
if (!s) return; if (!s) return;
@@ -721,17 +722,16 @@
const matchPhone = rawPhone.toString().match(/[6789]\d{8}/); const matchPhone = rawPhone.toString().match(/[6789]\d{8}/);
document.getElementById('detPhoneRaw').value = matchPhone ? matchPhone[0] : ""; document.getElementById('detPhoneRaw').value = matchPhone ? matchPhone[0] : "";
// INICIALIZAR EL AGENDADOR
document.getElementById('durationInput').value = "60"; document.getElementById('durationInput').value = "60";
pickerSelectedDate = ""; // Reset pickerSelectedDate = "";
pickerSelectedTime = ""; // Reset pickerSelectedTime = "";
buildDayCarousel(); // Esto selecciona 'hoy' automáticamente y pinta los huecos buildDayCarousel();
checkSaveButton(); checkSaveButton();
const modal = document.getElementById('actionModal'); const modal = document.getElementById('actionModal');
const content = document.getElementById('modalContent'); const content = document.getElementById('modalContent');
modal.classList.remove('hidden'); modal.classList.remove('hidden');
void modal.offsetWidth; // Reflow void modal.offsetWidth;
modal.classList.remove('opacity-0'); modal.classList.remove('opacity-0');
content.classList.remove('translate-y-full'); content.classList.remove('translate-y-full');
} }
@@ -739,10 +739,8 @@
function closeModal(modalId, contentId) { function closeModal(modalId, contentId) {
const modal = document.getElementById(modalId); const modal = document.getElementById(modalId);
const content = document.getElementById(contentId); const content = document.getElementById(contentId);
modal.classList.add('opacity-0'); modal.classList.add('opacity-0');
content.classList.add('translate-y-full'); content.classList.add('translate-y-full');
setTimeout(() => { modal.classList.add('hidden'); }, 300); setTimeout(() => { modal.classList.add('hidden'); }, 300);
} }