PageSourceSearch

https://app.mentevivapsicologia.com/modules/disponibilidad/widget_fixed.js

js mentevivapsicologia.com collected 2026-10-08 21:09:10 UTC 28,858 bytes, 787 lines download raw bytes

1/**
2 * Widget de Reservas de Citas (Versión corregida)
3 *
4 * Este script crea un widget para la reserva de citas que se puede integrar
5 * en cualquier sitio web, incluyendo WordPress.
6 *
7 * Uso:
8 * <script src="https://tu-servidor.com/modules/disponibilidad/widget_fixed.js"
9 *         id="citas-widget"
10 *         data-url="https://tu-servidor.com">
11 * </script>
12 */
13
14(function() {
15    // Configuración
16    const script = document.getElementById('citas-widget');
17    if (!script) {
18        console.error('Widget de Citas: No se encontró el elemento script con id "citas-widget"');
19        return;
20    }
21
22    const baseUrl = script.getAttribute('data-url') || '';
23    if (!baseUrl) {
24        console.error('Widget de Citas: Falta el atributo data-url');
25        return;
26    }
27
28    // Cambiar a false por defecto para ocultar información de depuración
29    const debugMode = script.getAttribute('data-debug') === 'true';
30    
31    // Eliminar todos los console.log para producción
32    if (!debugMode) {
33        console.log = function() {};
34    }
35    
36    // Variables globales
37    let disponibilidadData = {};
38    let selectedDate = null;
39    let selectedSlot = null;
40    let currentYear = new Date().getFullYear();
41    let currentMonth = new Date().getMonth() + 1; // JavaScript meses: 0-11
42
43    // Crear y añadir estilos
44    const styles = `
45        .citas-widget {
46            font-family: 'Arial', sans-serif;
47            max-width: 800px;
48            margin: 0 auto;
49            border-radius: 8px;
50            overflow: hidden;
51            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
52            background-color: #fff;
53        }
54        .citas-widget-header {
55            background-color: #4a6fdc;
56            color: white;
57            padding: 15px 20px;
58        }
59        .citas-widget-header h3 {
60            margin: 0;
61        }
62        .citas-widget-body {
63            padding: 20px;
64        }
65        .citas-widget-step {
66            display: none;
67        }
68        .citas-widget-step.active {
69            display: block;
70        }
71        .citas-widget-step h4 {
72            margin-top: 0;
73            margin-bottom: 15px;
74        }
75        .citas-calendar-header {
76            display: flex;
77            justify-content: space-between;
78            align-items: center;
79            margin-bottom: 15px;
80        }
81        .citas-calendar-title {
82            font-weight: bold;
83            font-size: 16px;
84        }
85        .citas-calendar-nav {
86            display: flex;
87            gap: 10px;
88        }
89        .citas-calendar-nav button {  
90            background-color: #f0f0f0;
91            border: none;
92            border-radius: 4px;       
93            padding: 5px 10px;        
94            cursor: pointer;
95        }
96        .citas-calendar-grid {        
97            display: grid;
98            grid-template-columns: repeat(7, 1fr);
99            gap: 5px;
100        }
101        .citas-calendar-day-header {
102            text-align: center;
103            font-weight: bold;
104            padding: 5px;
105            background-color: #f5f5f5;
106        }
107        .citas-calendar-day {
108            border: 1px solid #e0e0e0;
109            padding: 8px;
110            text-align: center;
111            cursor: pointer;
112            position: relative;
113            min-height: 40px;
114            display: flex;
115            align-items: center;
116            justify-content: center;
117        }
118        .citas-calendar-day.disabled {
119            background-color: #f5f5f5;
120            color: #aaa;
121            cursor: default;
122        }
123        .citas-calendar-day.selectable {
124            background-color: #e6f7ff;
125            color: #0077cc;
126        }
127        .citas-calendar-day.selected {
128            background-color: #4a6fdc;
129            color: #fff;
130            font-weight: bold;
131        }
132        .citas-slots-grid {
133            display: grid;
134            grid-template-columns: repeat(3, 1fr);
135            gap: 10px;
136            margin-top: 20px;
137        }
138        .citas-slot {
139            border: 1px solid #e0e0e0;
140            padding: 10px;
141            text-align: center;
142            cursor: pointer;
143            border-radius: 4px;
144        }
145        .citas-slot:hover {
146            background-color: #f0f0f0;
147        }
148        .citas-slot.selected {
149            background-color: #4a6fdc;
150            color: #fff;
151            border-color: #4a6fdc;
152        }
153        .citas-form {
154            margin-top: 20px;
155        }
156        .citas-form-group {
157            margin-bottom: 15px;
158        }
159        .citas-form-group label {
160            display: block;
161            margin-bottom: 5px;
162            font-weight: bold;
163        }
164        .citas-form-group input,
165        .citas-form-group textarea,
166        .citas-form-group select {
167            width: 100%;
168            padding: 10px;
169            border: 1px solid #ddd;
170            border-radius: 4px;
171            font-size: 14px;
172        }
173        .citas-widget-footer {
174            padding: 15px 20px;
175            border-top: 1px solid #eee;
176            display: flex;
177            justify-content: space-between;
178        }
179        .citas-btn {
180            padding: 10px 15px;
181            border: none;
182            border-radius: 4px;
183            cursor: pointer;
184            font-weight: bold;
185        }
186        .citas-btn-primary {
187            background-color: #4a6fdc;
188            color: #fff;
189        }
190        .citas-btn-secondary {
191            background-color: #f0f0f0;
192            color: #333;
193        }
194        .citas-btn:disabled {
195            opacity: 0.7;
196            cursor: not-allowed;
197        }
198        .citas-confirmation {
199            text-align: center;
200            padding: 20px;
201        }
202        .citas-confirmation-icon {
203            font-size: 48px;
204            color: #4caf50;
205            margin-bottom: 15px;
206        }
207        .citas-error {
208            background-color: #ffebee;
209            color: #d32f2f;
210            padding: 10px;
211            margin: 10px 0;
212            border-radius: 4px;
213            display: none;
214        }
215        .citas-loading {
216            display: flex;
217            justify-content: center;
218            align-items: center;
219            min-height: 100px;
220        }
221        .citas-loading:after {
222            content: '';
223            width: 30px;
224            height: 30px;
225            border: 5px solid #f3f3f3;
226            border-top: 5px solid #4a6fdc;
227            border-radius: 50%;
228            animation: citasSpinner 1s linear infinite;
229        }
230        .citas-debug {
231            margin-top: 15px;
232            padding: 10px;
233            background-color: #f8f9fa;
234            border: 1px solid #ddd;
235            border-radius: 4px;
236            font-family: monospace;
237            font-size: 12px;
238            white-space: pre-wrap;
239            overflow: auto;
240            max-height: 200px;
241            display: none; /* Oculto por defecto */
242        }
243        @keyframes citasSpinner {
244            0% { transform: rotate(0deg); }
245            100% { transform: rotate(360deg); }
246        }
247    `;
248
249    const styleEl = document.createElement('style');
250    styleEl.textContent = styles;
251    document.head.appendChild(styleEl);
252
253    // Crear estructura del widget
254    const widget = document.createElement('div');
255    widget.className = 'citas-widget';
256    widget.innerHTML = `
257        <div class="citas-widget-header">
258            <h3>Reserva tu Cita</h3>
259        </div>
260        <div class="citas-widget-body">
261            <div class="citas-error" id="citas-error"></div>
262            ${debugMode ? '<div class="citas-debug" id="citas-debug"></div>' : ''}
263
264            <!-- Paso 1: Seleccionar fecha -->
265            <div class="citas-widget-step active" id="step-1">
266                <h4>Selecciona una fecha</h4>
267                <div class="citas-widget-calendar">
268                    <div class="citas-calendar-header">
269                        <div class="citas-calendar-title" id="calendar-title">Cargando...</div>
270                        <div class="citas-calendar-nav">
271                            <button id="prev-month">&lt; Anterior</button>
272                            <button id="next-month">Siguiente &gt;</button>
273                        </div>
274                    </div>
275                    <div class="citas-calendar-loading" id="calendar-loading">
276                        <div class="citas-loading"></div>
277                    </div>
278                    <div class="citas-calendar-grid" id="calendar-grid">
279                        <!-- Días de la semana -->
280                        <div class="citas-calendar-day-header">Dom</div>
281                        <div class="citas-calendar-day-header">Lun</div>
282                        <div class="citas-calendar-day-header">Mar</div>
283                        <div class="citas-calendar-day-header">Mié</div>
284                        <div class="citas-calendar-day-header">Jue</div>
285                        <div class="citas-calendar-day-header">Vie</div>
286                        <div class="citas-calendar-day-header">Sáb</div>
287
288                        <!-- Aquí se generarán los días -->
289                    </div>
290                </div>
291            </div>
292
293            <!-- Paso 2: Seleccionar hora -->
294            <div class="citas-widget-step" id="step-2">
295                <h4>Selecciona una hora</h4>
296                <p class="fecha-seleccionada" id="fecha-seleccionada"></p>
297                <div class="citas-slots-grid" id="slots-grid">
298                    <!-- Aquí se generarán los slots disponibles -->
299                </div>
300            </div>
301
302            <!-- Paso 3: Formulario de datos -->
303            <div class="citas-widget-step" id="step-3">
304                <h4>Completa tus datos</h4>
305                <p class="fecha-seleccionada" id="resumen-cita"></p>
306
307                <form class="citas-form" id="citas-form">
308                    <div class="citas-form-group">
309                        <label for="nombre">Nombre *</label>
310                        <input type="text" id="nombre" name="nombre" required>
311                    </div>
312                    <div class="citas-form-group">
313                        <label for="apellidos">Apellidos *</label>
314                        <input type="text" id="apellidos" name="apellidos" required>
315                    </div>
316                    <div class="citas-form-group">
317                        <label for="email">Email *</label>
318                        <input type="email" id="email" name="email" required>
319                    </div>
320                    <div class="citas-form-group">
321                        <label for="telefono">Teléfono *</label>
322                        <input type="tel" id="telefono" name="telefono" required>
323                    </div>
324                    <div class="citas-form-group">
325                        <label for="motivo">Motivo de la cita *</label>
326                        <select id="motivo" name="motivo" required>
327                            <option value="">Selecciona una opción</option>
328                            <option value="Primera consulta">Primera consulta</option>
329                            <option value="Seguimiento">Seguimiento</option>
330                            <option value="Terapia">Terapia</option>
331                            <option value="Evaluación">Evaluación</option>
332                            <option value="Otro">Otro</option>
333                        </select>
334                    </div>
335                    <div class="citas-form-group">
336                        <label for="comentarios">Comentarios adicionales</label>
337                        <textarea id="comentarios" name="comentarios" rows="3"></textarea>
338                    </div>
339                </form>
340            </div>
341
342            <!-- Paso 4: Confirmación -->
343            <div class="citas-widget-step" id="step-4">
344                <div class="citas-confirmation">
345                    <div class="citas-confirmation-icon">✓</div>
346                    <h4>¡Solicitud Enviada!</h4>
347                    <p>Tu solicitud de cita ha sido recibida correctamente.</p>
348                    <p>Recibirás un email de confirmación cuando sea revisada.</p>
349                    <p>Resumen de la cita:</p>
350                    <p id="confirmacion-resumen"></p>
351                </div>
352            </div>
353        </div>
354        <div class="citas-widget-footer">
355            <button class="citas-btn citas-btn-secondary" id="btn-back" style="display: none;">Atrás</button>
356            <button class="citas-btn citas-btn-primary" id="btn-next" disabled>Siguiente</button>
357        </div>
358    `;
359
360    // Insertar widget después del script
361    script.parentNode.insertBefore(widget, script.nextSibling);
362
363    // Obtener elementos
364    const btnNext = document.getElementById('btn-next');
365    const btnBack = document.getElementById('btn-back');
366    const calendarGrid = document.getElementById('calendar-grid');
367    const calendarTitle = document.getElementById('calendar-title');
368    const btnPrevMonth = document.getElementById('prev-month');
369    const btnNextMonth = document.getElementById('next-month');
370    const slotsGrid = document.getElementById('slots-grid');
371    const errorEl = document.getElementById('citas-error');
372    const debugEl = debugMode ? document.getElementById('citas-debug') : null;
373    const calendarLoading = document.getElementById('calendar-loading');
374
375    // Pasos del formulario
376    const steps = [
377        document.getElementById('step-1'),
378        document.getElementById('step-2'),
379        document.getElementById('step-3'),
380        document.getElementById('step-4')
381    ];
382
383    let currentStep = 0;
384    let isLoading = false;
385
386    // Función para mostrar error
387    function showError(message) {
388        errorEl.textContent = message;
389        errorEl.style.display = 'block';
390        setTimeout(() => {
391            errorEl.style.display = 'none';
392        }, 5000);
393    }
394
395    // Función para mostrar información de debug
396    function debug(info) {
397        if (!debugMode || !debugEl) return;
398        
399        if (typeof info === 'object') {
400            debugEl.textContent = JSON.stringify(info, null, 2);
401        } else {
402            debugEl.textContent = info;
403        }
404        debugEl.style.display = 'block';
405    }
406
407    // Función para mostrar/ocultar indicador de carga
408    function setLoading(loading) {
409        isLoading = loading;
410        if (loading) {
411            calendarLoading.style.display = 'block';
412        } else {
413            calendarLoading.style.display = 'none';
414        }
415    }
416
417    // Función para cambiar de paso
418    function goToStep(step) {
419        steps[currentStep].classList.remove('active');
420        steps[step].classList.add('active');
421        currentStep = step;
422
423        // Actualizar botones
424        if (step === 0) {
425            btnBack.style.display = 'none';
426            btnNext.disabled = !selectedDate;
427        } else if (step === 3) {
428            btnBack.style.display = 'none';
429            btnNext.style.display = 'none';
430        } else {
431            btnBack.style.display = 'block';
432            btnNext.style.display = 'block';
433            btnNext.disabled = (step === 1 && !selectedSlot) || (step === 2 && !validateForm());
434        }
435
436        // Actualizar textos según el paso
437        if (step === 1) {
438            document.getElementById('fecha-seleccionada').textContent =
439                `Fecha seleccionada: ${formatDate(selectedDate)}`;
440        } else if (step === 2) {
441            document.getElementById('resumen-cita').innerHTML =
442                `Fecha: ${formatDate(selectedDate)}<br>` +
443                `Hora: ${selectedSlot.inicio} - ${selectedSlot.fin}`;
444        }
445    }
446
447    // Validación de formulario
448    function validateForm() {
449        const form = document.getElementById('citas-form');
450        const requiredFields = form.querySelectorAll('[required]');
451
452        for (const field of requiredFields) {
453            if (!field.value.trim()) {
454                return false;
455            }
456        }
457
458        // Validación básica de email
459        const emailField = document.getElementById('email');
460        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
461        if (emailField.value && !emailRegex.test(emailField.value)) {
462            return false;
463        }
464
465        return true;
466    }
467
468    // Función para formatear fecha
469    function formatDate(dateString) {
470        const date = new Date(dateString);
471        return date.toLocaleDateString('es-ES', {
472            weekday: 'long',
473            year: 'numeric',
474            month: 'long',
475            day: 'numeric'
476        });
477    }
478
479    // Función para generar el calendario
480    function generateCalendar(year, month) {
481        setLoading(true);
482        
483        // Limpiar cualquier contenido existente después de los encabezados de día
484        const dayHeaders = calendarGrid.querySelectorAll('.citas-calendar-day-header');
485        calendarGrid.innerHTML = '';
486        dayHeaders.forEach(header => calendarGrid.appendChild(header));
487
488        // Actualizar título
489        const monthName = new Date(year, month - 1, 1).toLocaleDateString('es-ES', { month: 'long' });      
490        calendarTitle.textContent = `${monthName} ${year}`;
491
492        // Obtener el primer día del mes
493        const firstDay = new Date(year, month - 1, 1);
494        // Obtener el día de la semana (0 = domingo, 1 = lunes, ...)
495        const startingDay = firstDay.getDay();
496        // Obtener el número de días en el mes
497        const daysInMonth = new Date(year, month, 0).getDate();
498
499        // Añadir espacios en blanco para los días anteriores al primer día del mes
500        for (let i = 0; i < startingDay; i++) {
501            const emptyDay = document.createElement('div');
502            emptyDay.className = 'citas-calendar-day disabled';
503            calendarGrid.appendChild(emptyDay);
504        }
505
506        // Añadir los días del mes
507        for (let day = 1; day <= daysInMonth; day++) {
508            const dateString = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`; 
509            const dayElement = document.createElement('div');
510            dayElement.className = 'citas-calendar-day';
511            dayElement.textContent = day;
512            dayElement.dataset.date = dateString;
513
514            // Por defecto, todos los días están pendientes de verificar
515            dayElement.classList.add('disabled');
516            dayElement.title = 'Cargando disponibilidad...';
517
518            if (dateString === selectedDate) {
519                dayElement.classList.add('selected');
520            }
521
522            calendarGrid.appendChild(dayElement);
523        }
524
525        // Cargar disponibilidad si no está cargada
526        fetchDisponibilidad(year, month);
527    }
528
529    // Función para seleccionar una fecha
530    function selectDate(dateString) {
531        selectedDate = dateString;
532        selectedSlot = null;
533
534        // Actualizar UI del calendario
535        document.querySelectorAll('.citas-calendar-day').forEach(day => {
536            day.classList.remove('selected');
537            if (day.dataset.date === dateString) {
538                day.classList.add('selected');
539            }
540        });
541
542        // Actualizar estado del botón
543        btnNext.disabled = false;
544    }
545
546    // Función para seleccionar un slot
547    function selectSlot(slot) {
548        selectedSlot = slot;
549
550        // Actualizar UI de los slots
551        document.querySelectorAll('.citas-slot').forEach(el => {
552            el.classList.remove('selected');
553        });
554        document.querySelector(`.citas-slot[data-inicio="${slot.inicio}"][data-fin="${slot.fin}"]`).classList.add('selected');
555
556        // Actualizar estado del botón
557        btnNext.disabled = false;
558    }
559
560    // Función para generar la lista de slots disponibles
561    function generateSlots(dateString) {
562        slotsGrid.innerHTML = '';
563
564        if (!disponibilidadData[dateString] || !disponibilidadData[dateString].disponible) {
565            const message = document.createElement('p');
566            message.textContent = 'No hay horarios disponibles para esta fecha.';
567            slotsGrid.appendChild(message);
568            return;
569        }
570
571        const slots = disponibilidadData[dateString].slots;
572
573        if (!slots || slots.length === 0) {
574            const message = document.createElement('p');
575            message.textContent = 'No hay horarios disponibles para esta fecha.';
576            slotsGrid.appendChild(message);
577            return;
578        }
579
580        slots.forEach(slot => {
581            const slotElement = document.createElement('div');
582            slotElement.className = 'citas-slot';
583            slotElement.textContent = `${slot.inicio} - ${slot.fin}`;
584            slotElement.dataset.inicio = slot.inicio;
585            slotElement.dataset.fin = slot.fin;
586            slotElement.dataset.duracion = slot.duracion;
587
588            slotElement.addEventListener('click', () => selectSlot(slot));
589
590            slotsGrid.appendChild(slotElement);
591        });
592    }
593
594    // Función para obtener disponibilidad del servidor
595    function fetchDisponibilidad(year, month) {
596        setLoading(true);
597        
598        // Usar la API de disponibilidad corregida
599        const apiUrl = `${baseUrl}/modules/disponibilidad/api_get_disponibilidad_fixed.php?year=${year}&month=${month}`;
600
601        debug(`Obteniendo disponibilidad: ${apiUrl}`);
602
603        fetch(apiUrl)
604            .then(response => {
605                debug(`Respuesta HTTP: ${response.status} ${response.statusText}`);
606                if (!response.ok) {
607                    throw new Error(`Error al cargar disponibilidad: ${response.status} ${response.statusText}`);
608                }
609                return response.json();
610            })
611            .then(data => {
612                debug(`Datos recibidos: ${JSON.stringify(data, null, 2)}`);
613                
614                if (data.success) {
615                    // Actualizar datos de disponibilidad
616                    Object.assign(disponibilidadData, data.data);
617
618                    // Actualizar UI del calendario
619                    updateCalendarUI();
620                } else {
621                    showError(data.message || 'Error al cargar disponibilidad');
622                }
623            })
624            .catch(error => {
625                showError(`Error: ${error.message}`);
626                debug(`Error completo: ${error.stack || error}`);
627                console.error('Error:', error);
628            })
629            .finally(() => {
630                setLoading(false);
631            });
632    }
633
634    // Función para actualizar UI del calendario con los datos de disponibilidad
635    function updateCalendarUI() {
636        document.querySelectorAll('.citas-calendar-day').forEach(day => {
637            const dateString = day.dataset.date;
638            if (!dateString) return;
639
640            // Por defecto, marcar como no disponible
641            day.classList.remove('selectable');
642            day.classList.add('disabled');
643            day.title = 'No disponible';
644            
645            // Si hay datos para este día, actualizar según disponibilidad
646            if (disponibilidadData[dateString]) {
647                const diaData = disponibilidadData[dateString];
648                
649                day.classList.remove('disabled', 'selectable');
650                
651                if (diaData.disponible && diaData.slots && diaData.slots.length > 0) {
652                    day.classList.add('selectable');
653                    day.classList.remove('disabled');
654                    day.title = `${diaData.slots.length} horarios disponibles`;
655                    
656                    // Añadir evento de clic solo a días disponibles
657                    day.addEventListener('click', () => selectDate(dateString));
658                } else {
659                    day.classList.add('disabled');
660                    if (diaData.motivo === 'excepcion') {
661                        day.title = 'No disponible: excepción';
662                    } else if (diaData.motivo === 'fecha_pasada') {
663                        day.title = 'Fecha pasada';
664                    } else if (diaData.motivo === 'sin_horario') {
665                        day.title = 'Sin horarios definidos';
666                    } else {
667                        day.title = 'No disponible';
668                    }
669                }
670            }
671        });
672        
673        // Contar días disponibles para debug
674        const diasDisponibles = Object.values(disponibilidadData).filter(d => d.disponible).length;
675        debug(`Días disponibles: ${diasDisponibles}`);
676        
677        // Si hay fecha seleccionada, mantener la selección
678        if (selectedDate) {
679            document.querySelectorAll('.citas-calendar-day').forEach(day => {
680                if (day.dataset.date === selectedDate) {
681                    day.classList.add('selected');
682                }
683            });
684        }
685    }
686
687    // Función para enviar la solicitud de cita
688    function submitCitaRequest() {
689        // Recoger datos del formulario
690        const formData = new FormData(document.getElementById('citas-form'));
691
692        // Añadir datos de la cita
693        formData.append('fecha', selectedDate);
694        formData.append('hora_inicio', selectedSlot.inicio);
695        formData.append('hora_fin', selectedSlot.fin);
696        formData.append('duracion', selectedSlot.duracion);
697
698        // Obtener la API configurada o usar la corregida por defecto
699        const apiEndpoint = script.getAttribute('data-api') || 'api_fixed.php';
700        const apiUrl = `${baseUrl}/modules/disponibilidad/${apiEndpoint}`;
701        
702        debug(`Enviando solicitud a: ${apiUrl}`);
703
704        // Enviar solicitud
705        fetch(apiUrl, {
706            method: 'POST',
707            body: formData
708        })
709        .then(response => {
710            if (!response.ok) {
711                throw new Error('Error al enviar la solicitud');
712            }
713            return response.json();
714        })
715        .then(data => {
716            if (data.success) {
717                // Actualizar resumen de confirmación
718                document.getElementById('confirmacion-resumen').innerHTML =
719                    `Fecha: ${formatDate(selectedDate)}<br>` +
720                    `Hora: ${selectedSlot.inicio} - ${selectedSlot.fin}<br>` +
721                    `Nombre: ${formData.get('nombre')} ${formData.get('apellidos')}<br>` +
722                    `Motivo: ${formData.get('motivo')}`;
723
724                // Ir al paso de confirmación
725                goToStep(3);
726            } else {
727                showError(data.message || 'Error al enviar la solicitud');
728            }
729        })
730        .catch(error => {
731            showError(error.message);
732            console.error('Error:', error);
733        });
734    }
735
736    // Event Listeners
737
738    // Botón "Siguiente"
739    btnNext.addEventListener('click', () => {
740        if (currentStep === 0) {
741            generateSlots(selectedDate);
742            goToStep(1);
743        } else if (currentStep === 1) {
744            goToStep(2);
745        } else if (currentStep === 2) {
746            submitCitaRequest();
747        }
748    });
749
750    // Botón "Atrás"
751    btnBack.addEventListener('click', () => {
752        goToStep(currentStep - 1);
753    });
754
755    // Botones navegación de mes
756    btnPrevMonth.addEventListener('click', () => {
757        if (currentMonth === 1) {
758            currentMonth = 12;
759            currentYear--;
760        } else {
761            currentMonth--;
762        }
763        generateCalendar(currentYear, currentMonth);
764    });
765
766    btnNextMonth.addEventListener('click', () => {
767        if (currentMonth === 12) {
768            currentMonth = 1;
769            currentYear++;
770        } else {
771            currentMonth++;
772        }
773        generateCalendar(currentYear, currentMonth);
774    });
775
776    // Eventos de cambio en formulario
777    document.getElementById('citas-form').addEventListener('input', () => {
778        btnNext.disabled = !validateForm();
779    });
780
781    // Inicialización
782    if (debugMode) {
783        debug('Widget de citas inicializado en modo debug');
784    }
785    
786    generateCalendar(currentYear, currentMonth);
787})(); 

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.