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">< Anterior</button> 272 <button id="next-month">Siguiente ></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.