+
@@ -271,7 +308,155 @@
}
// ==========================================
- // LÓGICA DEL MODAL DE ACCIONES
+ // LÓGICA DEL MOTOR DE AGENDAMIENTO INTELIGENTE
+ // ==========================================
+ function toISODate(dateObj) {
+ const y = dateObj.getFullYear();
+ const m = String(dateObj.getMonth() + 1).padStart(2, '0');
+ const d = String(dateObj.getDate()).padStart(2, '0');
+ return `${y}-${m}-${d}`;
+ }
+
+ function timeToMins(t) {
+ let [h, m] = t.split(':').map(Number);
+ 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 buildDayCarousel() {
+ const container = document.getElementById('dayCarousel');
+ container.innerHTML = "";
+ let today = new Date();
+
+ for (let i = 0; i < 14; i++) { // 14 días a futuro
+ let d = new Date(today);
+ d.setDate(today.getDate() + i);
+
+ if (d.getDay() === 0) continue; // Saltar domingos
+
+ const isoDate = toISODate(d);
+ const dayName = d.toLocaleDateString('es-ES', { weekday: 'short' }).replace('.', '').substring(0,3);
+ const dayNum = d.getDate();
+
+ // Seleccionar hoy por defecto
+ if (!pickerSelectedDate) pickerSelectedDate = isoDate;
+
+ const isSelected = isoDate === pickerSelectedDate;
+
+ container.innerHTML += `
+
+ `;
+ }
+ }
+
+ function selectPickerDate(isoDate) {
+ pickerSelectedDate = isoDate;
+ pickerSelectedTime = ""; // Resetear hora al cambiar de día
+ buildDayCarousel(); // Repintar para actualizar estilos
+ renderTimeSlots();
+ checkSaveButton();
+ }
+
+ function selectPickerTime(timeStr) {
+ pickerSelectedTime = timeStr;
+ renderTimeSlots(); // Repintar para estilos
+ checkSaveButton();
+ }
+
+ function checkSaveButton() {
+ const btn = document.getElementById('btnSaveAppt');
+ if (pickerSelectedDate && pickerSelectedTime) {
+ btn.disabled = false;
+ btn.classList.remove('bg-slate-300', 'cursor-not-allowed');
+ btn.classList.add('bg-primary-dynamic', 'active:scale-95', 'shadow-lg');
+ } else {
+ btn.disabled = true;
+ btn.classList.add('bg-slate-300', 'cursor-not-allowed');
+ btn.classList.remove('bg-primary-dynamic', 'active:scale-95', 'shadow-lg');
+ }
+ }
+
+ function getOccupiedRanges(dateStr) {
+ let ranges = [];
+ globalAllServices.forEach(s => {
+ const raw = s.raw_data || {};
+ if (raw.scheduled_date === dateStr && raw.scheduled_time) {
+ const startMin = timeToMins(raw.scheduled_time);
+ const dur = parseInt(raw.duration_minutes || 60);
+ ranges.push({ start: startMin, end: startMin + dur });
+ }
+ });
+ return ranges;
+ }
+
+ function isOverlapping(startMins, endMins, 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;
+ }
+ return false;
+ }
+
+ function renderTimeSlots() {
+ const grid = document.getElementById('timeGrid');
+ const msg = document.getElementById('noSlotsMsg');
+ const duration = parseInt(document.getElementById('durationInput').value) || 60;
+ const occupied = getOccupiedRanges(pickerSelectedDate);
+
+ grid.innerHTML = "";
+ let slotsGenerated = 0;
+
+ // Función interna para recorrer tramos
+ const genSlotsForPeriod = (startStr, endStr) => {
+ let startMins = timeToMins(startStr);
+ let endMins = timeToMins(endStr);
+
+ // Saltos de 15 minutos
+ for (let m = startMins; m + duration <= endMins; m += 15) {
+ if (!isOverlapping(m, m + duration, occupied)) {
+ const timeStr = minsToTime(m);
+ const isSelected = timeStr === pickerSelectedTime;
+ grid.innerHTML += `
+
+ `;
+ slotsGenerated++;
+ }
+ }
+ };
+
+ // Generar Mañana y Tarde
+ genSlotsForPeriod(bizHours.m_start, bizHours.m_end);
+ genSlotsForPeriod(bizHours.a_start, bizHours.a_end);
+
+ if (slotsGenerated === 0) {
+ msg.classList.remove('hidden');
+ grid.classList.add('hidden');
+ pickerSelectedTime = ""; // Reset forzado
+ } else {
+ msg.classList.add('hidden');
+ grid.classList.remove('hidden');
+ }
+
+ // Si la hora que estaba seleccionada ya no sale (porque ampliaron la duración), la borramos
+ if (pickerSelectedTime && !grid.innerHTML.includes(`'${pickerSelectedTime}'`)) {
+ pickerSelectedTime = "";
+ }
+
+ checkSaveButton();
+ }
+
+ // ==========================================
+ // APERTURA DE MODAL Y GUARDADO
// ==========================================
function openActionModal(id) {
const s = localServices.find(x => x.id === id);
@@ -283,24 +468,21 @@
document.getElementById('detName').innerText = raw["Nombre Cliente"] || "Asegurado";
document.getElementById('detAddress').innerText = `${raw["Dirección"] || ""}, ${raw["Población"] || ""}`;
- // Extraer teléfono limpio
const rawPhone = raw["Teléfono"] || raw["TELEFONOS"] || raw["TELEFONO"] || "";
const matchPhone = rawPhone.toString().match(/[6789]\d{8}/);
document.getElementById('detPhoneRaw').value = matchPhone ? matchPhone[0] : "";
- // Limpiar inputs de fecha y poner duración por defecto
- document.getElementById('dateInput').value = "";
- document.getElementById('timeInput').value = "";
+ // INICIALIZAR EL AGENDADOR
document.getElementById('durationInput').value = "60";
+ pickerSelectedDate = ""; // Reset
+ pickerSelectedTime = ""; // Reset
+ buildDayCarousel(); // Esto selecciona 'hoy' automáticamente y pinta los huecos
+ checkSaveButton();
- // Mostrar modal con animación
const modal = document.getElementById('actionModal');
const content = document.getElementById('modalContent');
modal.classList.remove('hidden');
-
- // Reflow hack para que aplique la transición
- void modal.offsetWidth;
-
+ void modal.offsetWidth; // Reflow
modal.classList.remove('opacity-0');
content.classList.remove('translate-y-full');
}
@@ -312,9 +494,7 @@
modal.classList.add('opacity-0');
content.classList.add('translate-y-full');
- setTimeout(() => {
- modal.classList.add('hidden');
- }, 300);
+ setTimeout(() => { modal.classList.add('hidden'); }, 300);
}
function callClient() {
@@ -331,33 +511,33 @@
async function saveAppointment() {
const id = document.getElementById('detId').value;
- const date = document.getElementById('dateInput').value;
- const time = document.getElementById('timeInput').value;
const duration = document.getElementById('durationInput').value;
- if (!date || !time) return alert("Selecciona fecha y hora para agendar.");
+ if (!pickerSelectedDate || !pickerSelectedTime) return alert("Selecciona un hueco disponible.");
const btn = document.getElementById('btnSaveAppt');
const originalContent = btn.innerHTML;
- btn.innerHTML = ` Guardando...`;
+ btn.innerHTML = ` Agendando...`;
btn.disabled = true;
- // Buscamos el ID del estado "Citado" en el sistema
const citadoStatus = systemStatuses.find(st => st.name.toLowerCase().includes('citado'));
const statusToSave = citadoStatus ? String(citadoStatus.id) : null;
try {
- // Al enviar la fecha a esta ruta del server, el server dispara el WhatsApp Automático al cliente
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({ date: date, time: time, duration_minutes: duration, status_operativo: statusToSave })
+ body: JSON.stringify({
+ date: pickerSelectedDate,
+ time: pickerSelectedTime,
+ duration_minutes: duration,
+ status_operativo: statusToSave
+ })
});
if (res.ok) {
- showToast("Cita guardada correctamente");
+ showToast("Cita Agendada en Calendario");
closeModal();
- // Como ahora tiene fecha, desaparecerá de esta lista y se irá al calendario
refreshData();
} else {
alert("Error al guardar la cita.");