1/***********************************************************/ 2/* */ 3/* Plataforma e-ducativa - Argentina */ 4/* */ 5/* Copyright (c) 2026 de e-ducativa Educación Virtual S.A. */ 6/* */ 7/***********************************************************/ 8/** 9 * EnviarCertificadosMasivamente - Herramienta para envio masivo de certificados 10 * 11 * @module Educativa/Modules/Aula/View/Herramienta/EnviarCertificadosMasivamente 12 * Copyright (c) 2026 Educativa 13 */ 14 15import { DoubleList } from 'Educativa/Modules/Components/DoubleList'; 16import { EducativaCalendar } from 'Educativa/Modules/Base'; 17 18/** 19 * Vista principal de la herramienta 20 */ 21export class EnviarCertificadosMasivamenteView { 22 /** 23 * @typedef {Object} EnviarCertificadosOptions 24 * @property {string} container - ID del contenedor principal 25 * @property {number} id_herramienta - ID de la herramienta 26 * @property {number} id_cursoActual - ID del curso actual 27 * @property {Array} cursos - Lista de cursos disponibles 28 * @property {string} csrf_token - Token CSRF 29 */ 30 31 /** 32 * @param {EnviarCertificadosOptions} args - Opciones de configuracion 33 */ 34 constructor( args ) { 35 this.container = document.getElementById( args.container ); 36 this.idHerramienta = args.id_herramienta; 37 this.idCursoActual = args.id_cursoActual; 38 this.cursos = args.cursos || []; 39 this.csrfToken = args.csrf_token || ''; 40 41 this.formData = {}; 42 this.doubleList = null; 43 this._tooltipDiv = null; 44 45 this._init(); 46 } 47 48 _init() { 49 this._showFormView(); 50 } 51 52 53 // ========================================================================= 54 // VISTA: FORMULARIO 55 // ========================================================================= 56 57 _showFormView() { 58 if ( this._tooltipDiv ) { 59 this._tooltipDiv.remove(); 60 this._tooltipDiv = null; 61 } 62 63 this.container.innerHTML = ''; 64 this.container.classList.remove( 'results_view' ); 65 66 // Titulo 67 const title = document.createElement( 'h2' ); 68 title.textContent = Dict.termcap( 'ENVIAR_CERTIFICADOS_MASIVAMENTE' ); 69 this.container.appendChild( title ); 70 71 // Formulario 72 const form = document.createElement( 'form' ); 73 form.id = 'form_enviar_certificados'; 74 form.className = 'EventForm'; 75 form.method = 'POST'; 76 form.enctype = 'multipart/form-data'; 77 78 // Tip informativo 79 form.appendChild( this._createTipRow() ); 80 81 // Campo: Fuente de usuarios 82 form.appendChild( this._createFuenteUsuariosRow() ); 83 84 // Campo: Cursos 85 form.appendChild( this._createCursosRow() ); 86 87 // Campo: Filtros adicionales (radio buttons mutuamente excluyentes) 88 form.appendChild( this._createFiltrosAdicionalesRow() ); 89 90 // Error area (antes de botones) 91 const errorArea = document.createElement( 'div' ); 92 errorArea.id = 'submit_error'; 93 errorArea.className = 'submit_error double_list_hidden'; 94 errorArea.textContent = Dict.termcap( 'POR_FAVOR_REVISA_EL_FORMULARIO' ); 95 form.appendChild( errorArea ); 96 97 // Botones de accion 98 form.appendChild( this._createActionsRow() ); 99 100 form.addEventListener( 'submit', ( e ) => { 101 e.preventDefault(); 102 this._handleFormSubmit(); 103 } ); 104 105 this.container.appendChild( form ); 106 107 this._initDoubleList(); 108 } 109 110 _createTipRow() { 111 const row = document.createElement( 'div' ); 112 row.className = 'row'; 113 114 const noLabel = document.createElement( 'div' ); 115 noLabel.className = 'no_label'; 116 117 const tip = document.createElement( 'div' ); 118 tip.className = 'aclaracion'; 119 tip.innerHTML = '<i class="fa-regular fa-circle-info info_icon"></i> ' + Dict.termcap( 120 'SELECCIONE_LOS_CURSOS_Y_LA_FUENTE_DE_USUARIOS_PARA_ENVIAR_CERTIFICADOS' 121 ); 122 noLabel.appendChild( tip ); 123 row.appendChild( noLabel ); 124 125 return row; 126 } 127 128 _createFuenteUsuariosRow() {
129 const row = document.createElement( 'div' ); 130 row.className = 'row obligatorio'; 131 row.id = 'line_form_fuente_usuarios'; 132 133 // Label 134 const label = document.createElement( 'label' ); 135 label.textContent = Dict.termcap( 'FUENTE_DE_USUARIOS' ); 136 row.appendChild( label ); 137 138 // Contenedor derecho 139 const rightCont = document.createElement( 'div' ); 140 rightCont.className = 'right_cont'; 141 142 const control = document.createElement( 'div' ); 143 control.className = 'multi_control'; 144 145 // Opcion Archivo: row column que agrupa label del radio + file input 146 const rowArchivo = document.createElement( 'div' ); 147 rowArchivo.className = 'row_multicontrol column'; 148 149 const labelArchivo = document.createElement( 'label' ); 150 labelArchivo.className = 'inline_label'; 151 152 const radioArchivo = document.createElement( 'input' ); 153 radioArchivo.type = 'radio'; 154 radioArchivo.name = 'fuente_usuarios'; 155 radioArchivo.id = 'fuente_archivo'; 156 radioArchivo.value = 'archivo'; 157 radioArchivo.checked = true; 158 labelArchivo.appendChild( radioArchivo ); 159 160 labelArchivo.appendChild( document.createTextNode( Dict.termcap( 'ARCHIVO' ) ) ); 161 162 // Tooltip con icono de ayuda 163 const tooltipBtn = document.createElement( 'span' ); 164 tooltipBtn.className = 'tooltip-button'; 165 labelArchivo.appendChild( tooltipBtn ); 166 167 const tooltipDiv = document.createElement( 'div' ); 168 tooltipDiv.className = 'tooltip'; 169 tooltipDiv.style.display = 'none'; 170 tooltipDiv.innerHTML = Dict.termcap( 171 'ARCHIVO_DE_TEXTO_PLANO_TIPOS_QUE_CONTIENE_UN_USERNAME_POR_LINEA' 172 ).replace( '#{tipos}', '(.csv, .txt)' ); 173 document.body.appendChild( tooltipDiv ); 174 this._tooltipDiv = tooltipDiv; 175 176 tooltipBtn.addEventListener( 'mouseenter', () => { 177 const rect = tooltipBtn.getBoundingClientRect(); 178 // Mostrar invisible para calcular dimensiones reales 179 tooltipDiv.style.visibility = 'hidden'; 180 tooltipDiv.style.display = ''; 181 const height = tooltipDiv.offsetHeight; 182 tooltipDiv.style.left = rect.left + 'px'; 183 tooltipDiv.style.top = ( window.scrollY + rect.top - height - 5 ) + 'px'; 184 tooltipDiv.style.visibility = ''; 185 } ); 186 tooltipBtn.addEventListener( 'mouseleave', () => { 187 tooltipDiv.style.display = 'none'; 188 } ); 189 190 rowArchivo.appendChild( labelArchivo ); 191 192 // File input como subrow dentro del row_multicontrol column 193 const fileContainer = document.createElement( 'div' ); 194 fileContainer.id = 'file_input_container'; 195 fileContainer.className = 'subrow_multicontrol no_stretch'; 196 fileContainer.innerHTML = ` 197 <input type="file" name="archivo_usernames" 198 id="archivo_usernames" accept=".csv,.txt" size="140"> 199 `; 200 rowArchivo.appendChild( fileContainer ); 201 202 control.appendChild( rowArchivo ); 203 204 // Opcion Todos 205 const rowTodos = document.createElement( 'div' ); 206 rowTodos.className = 'row_multicontrol'; 207 208 const labelTodos = document.createElement( 'label' ); 209 labelTodos.className = 'inline_label'; 210 labelTodos.innerHTML = ` 211 <input type="radio" name="fuente_usuarios" id="fuente_todos" value="todos"> 212 ${Dict.termcap( 'TODOS_LOS_USUARIOS_CON_PERMISO_CURSA' )} 213 `; 214 rowTodos.appendChild( labelTodos ); 215 control.appendChild( rowTodos ); 216 217 // Error: sin archivo 218 const error = document.createElement( 'span' ); 219 error.className = 'error_txt double_list_hidden'; 220 error.id = 'error_fuente_usuarios'; 221 error.textContent = Dict.termcap( 'DEBE_SELECCIONAR_UN_ARCHIVO' ); 222 control.appendChild( error ); 223 224 // Error: tipo de archivo invalido 225 const errorTipo = document.createElement( 'span' ); 226 errorTipo.className = 'error_txt double_list_hidden'; 227 errorTipo.id = 'error_archivo_tipo'; 228 errorTipo.textContent = Dict.termcap( 'TIPO_ARCHIVO_NO_VALIDO' ); 229 control.appendChild( errorTipo ); 230 231 rightCont.appendChild( control ); 232 row.appendChild( rightCont ); 233 234 // Eventos 235 setTimeout( () => { 236 const radioArchivo = document.getElementById( 'fuente_archivo' ); 237 const radioTodos = document.getElementById( 'fuente_todos' ); 238 const fileInput = document.getElementById( 'file_input_container' ); 239 240 if ( radioArchivo && radioTodos && fileInput ) { 241 radioArchivo.addEventListener( 'change', () => { 242 fileInput.style.display = ''; 243 } ); 244 radioTodos.addEventListener( 'change', () => { 245 fileInput.style.display = 'none'; 246 } ); 247 } 248 }, 0 ); 249 250 return row; 251 } 252 253 _createCursosRow() {
254 const row = document.createElement( 'div' ); 255 row.className = 'row obligatorio'; 256 row.id = 'line_form_cursos'; 257 258 // Label 259 const label = document.createElement( 'label' ); 260 label.className = 'flex_start'; 261 label.textContent = Dict.termcap( 'AULAS' ); 262 row.appendChild( label ); 263 264 // Contenedor derecho 265 const rightCont = document.createElement( 'div' ); 266 rightCont.className = 'right_cont'; 267 268 const control = document.createElement( 'div' ); 269 control.id = 'cursos_multi_control'; 270 rightCont.appendChild( control ); 271 272 // Error 273 const error = document.createElement( 'span' ); 274 error.className = 'error_txt double_list_hidden'; 275 error.id = 'error_cursos'; 276 error.textContent = Dict.termcap( 'DEBE_SELECCIONAR_AL_MENOS_UN_AULA' ); 277 rightCont.appendChild( error ); 278 279 row.appendChild( rightCont ); 280 281 return row; 282 } 283 284 _createFiltrosAdicionalesRow() { 285 const row = document.createElement( 'div' ); 286 row.className = 'row'; 287 row.id = 'line_form_filtros_adicionales'; 288 289 // Label 290 const label = document.createElement( 'label' ); 291 label.className = 'flex_start'; 292 label.textContent = Dict.termcap( 'FILTROS_ADICIONALES' ); 293 row.appendChild( label ); 294 295 // Contenedor derecho 296 const rightCont = document.createElement( 'div' ); 297 rightCont.className = 'right_cont'; 298 299 const control = document.createElement( 'div' ); 300 control.className = 'multi_control'; 301 302 // Opcion 1: Sin filtro adicional (default) 303 const rowNinguno = document.createElement( 'div' ); 304 rowNinguno.className = 'row_multicontrol'; 305 const opcionNinguno = document.createElement( 'label' ); 306 opcionNinguno.className = 'inline_label'; 307 opcionNinguno.innerHTML = ` 308 <input type="radio" name="filtro_adicional" id="filtro_ninguno" value="ninguno" checked> 309 ${Dict.termcap( 'SIN_FILTRO_ADICIONAL' )} 310 `; 311 rowNinguno.appendChild( opcionNinguno ); 312 control.appendChild( rowNinguno ); 313 314 // Opcion 2: Solo finalizados 315 const rowFinalizados = document.createElement( 'div' ); 316 rowFinalizados.className = 'row_multicontrol'; 317 const opcionFinalizados = document.createElement( 'label' ); 318 opcionFinalizados.className = 'inline_label'; 319 opcionFinalizados.innerHTML = ` 320 <input type="radio" name="filtro_adicional" id="filtro_finalizados" value="finalizados"> 321 ${Dict.termcap( 'SOLO_USUARIOS_QUE_FINALIZARON' )} 322 `; 323 rowFinalizados.appendChild( opcionFinalizados ); 324 control.appendChild( rowFinalizados ); 325 326 // Fechas: wrapper para toggle JS (sin clase visual) 327 const fechasContainer = document.createElement( 'div' ); 328 fechasContainer.id = 'fechas_container'; 329 fechasContainer.className = 'double_list_hidden'; 330 331 const rowFechaDesde = document.createElement( 'div' ); 332 rowFechaDesde.className = 'subrow_multicontrol no_stretch'; 333 rowFechaDesde.innerHTML = ` 334 <label for="fecha_desde">${Dict.termcap( 'FECHA_DESDE' )}:</label> 335 <input type="text" name="fecha_desde" id="fecha_desde" 336 placeholder="dd/mm/aaaa" maxlength="10" autocomplete="off"> 337 <span class="error_txt double_list_hidden" id="error_fecha_desde"> 338 ${Dict.termcap( 'FECHA_INVALIDA' )} 339 </span> 340 `; 341 fechasContainer.appendChild( rowFechaDesde ); 342 343 const rowFechaHasta = document.createElement( 'div' ); 344 rowFechaHasta.className = 'subrow_multicontrol no_stretch'; 345 rowFechaHasta.innerHTML = ` 346 <label for="fecha_hasta">${Dict.termcap( 'FECHA_HASTA' )}:</label> 347 <input type="text" name="fecha_hasta" id="fecha_hasta" 348 placeholder="dd/mm/aaaa" maxlength="10" autocomplete="off"> 349 <span class="error_txt double_list_hidden" id="error_fecha_hasta"> 350 ${Dict.termcap( 'FECHA_INVALIDA' )} 351 </span> 352 `; 353 fechasContainer.appendChild( rowFechaHasta ); 354 355 const errorRango = document.createElement( 'span' ); 356 errorRango.className = 'error_txt double_list_hidden'; 357 errorRango.id = 'error_fecha_rango'; 358 errorRango.textContent = Dict.termcap( 'FECHA_DESDE_MAYOR_HASTA' ); 359 fechasContainer.appendChild( errorRango ); 360 361 control.appendChild( fechasContainer ); 362 363 // Inicializar calendarios cuando el contenedor sea visible 364 this._calendarsInitialized = false;
365 366 // Opcion 3: Avance completo 367 const rowAvance = document.createElement( 'div' ); 368 rowAvance.className = 'row_multicontrol'; 369 const opcionAvance = document.createElement( 'label' ); 370 opcionAvance.className = 'inline_label'; 371 opcionAvance.innerHTML = ` 372 <input type="radio" name="filtro_adicional" id="filtro_avance" value="avance_completo"> 373 ${Dict.termcap( 'SOLO_USUARIOS_CON_AVANCE_COMPLETO' )} 374 `; 375 rowAvance.appendChild( opcionAvance ); 376 control.appendChild( rowAvance ); 377 378 rightCont.appendChild( control ); 379 row.appendChild( rightCont ); 380 381 // Evento para mostrar/ocultar fechas segun seleccion 382 setTimeout( () => { 383 const radios = document.querySelectorAll( 'input[name="filtro_adicional"]' ); 384 const fechasCont = document.getElementById( 'fechas_container' ); 385 386 radios.forEach( radio => { 387 radio.addEventListener( 'change', () => { 388 if ( fechasCont ) { 389 const esFinalizados = radio.value === 'finalizados'; 390 fechasCont.classList.toggle( 'double_list_hidden', !esFinalizados ); 391 392 // Inicializar calendarios la primera vez que se muestran 393 if ( esFinalizados && !this._calendarsInitialized ) { 394 this._calendarsInitialized = true; 395 new EducativaCalendar( { 396 inputField: 'fecha_desde', 397 ifFormat: '%d/%m/%Y', 398 weekNumbers: false, 399 align: 'bR', 400 showsTime: false 401 } ).render(); 402 new EducativaCalendar( { 403 inputField: 'fecha_hasta', 404 ifFormat: '%d/%m/%Y', 405 weekNumbers: false, 406 align: 'bR', 407 showsTime: false 408 } ).render(); 409 } 410 } 411 } ); 412 } ); 413 }, 0 ); 414 415 return row; 416 } 417 418 _createActionsRow() { 419 const row = document.createElement( 'div' ); 420 row.className = 'row actions edu-btn-group'; 421 422 const submitBtn = document.createElement( 'button' ); 423 submitBtn.type = 'submit'; 424 submitBtn.className = 'edu-btn edu-btn-primary'; 425 submitBtn.textContent = Dict.termcap( 'CONTINUAR' ); 426 427 row.appendChild( submitBtn ); 428 429 return row; 430 } 431 432 _initDoubleList() { 433 const container = document.getElementById( 'cursos_multi_control' ); 434 if ( !container ) return; 435 436 const items = this.cursos.map( curso => ( { 437 value: curso.id_curso, 438 title: curso.title, 439 class: curso.activo === 0 ? 'inactivo' : '', 440 position: curso.position || 0 441 } ) ); 442 443 this.doubleList = new DoubleList( { 444 container: 'cursos_multi_control', 445 id: 'cursos', 446 items: items, 447 selected: [], 448 titleLeft: Dict.termcap( 'DISPONIBLES' ), 449 titleRight: Dict.termcap( 'SELECCIONADOS' ), 450 showSearch: true, 451 showInactiveFilter: true, 452 inactiveFilterLabel: Dict.termcap( 'MOSTRAR_AULAS_DESACTIVADAS' ) 453 } ); 454 } 455 456 _handleFormSubmit() { 457 this._clearErrors(); 458 459 if ( !this._validateForm() ) { 460 this._showFormError(); 461 return; 462 } 463 464 this._collectFormData(); 465 this._submitToConfirmacion(); 466 } 467 468 _validateForm() { 469 let valid = true; 470 471 const fuenteArchivo = document.getElementById( 'fuente_archivo' ); 472 const archivoInput = document.getElementById( 'archivo_usernames' ); 473 474 if ( fuenteArchivo && fuenteArchivo.checked ) { 475 const noFile = !archivoInput || 476 !archivoInput.files || 477 archivoInput.files.length === 0; 478 479 if ( noFile ) { 480 this._showFieldError( 'error_fuente_usuarios' ); 481 valid = false;
482 } else { 483 const ext = archivoInput.files[ 0 ].name 484 .split( '.' ).pop().toLowerCase(); 485 if ( ext !== 'csv' && ext !== 'txt' ) { 486 this._showFieldError( 'error_archivo_tipo' ); 487 valid = false; 488 } 489 } 490 } 491 492 if ( !this.doubleList || !this.doubleList.validate() ) { 493 this._showFieldError( 'error_cursos' ); 494 valid = false; 495 } 496 497 // Validar fechas si el filtro de finalizados esta activo 498 const filtroSeleccionado = document.querySelector( 'input[name="filtro_adicional"]:checked' ); 499 if ( filtroSeleccionado && filtroSeleccionado.value === 'finalizados' ) { 500 const fechaDesde = document.getElementById( 'fecha_desde' ); 501 const fechaHasta = document.getElementById( 'fecha_hasta' ); 502 503 const desdeValida = !fechaDesde.value || this._isValidDate( fechaDesde.value ); 504 const hastaValida = !fechaHasta.value || this._isValidDate( fechaHasta.value ); 505 506 if ( fechaDesde.value && !desdeValida ) { 507 this._showFieldError( 'error_fecha_desde' ); 508 valid = false; 509 } 510 if ( fechaHasta.value && !hastaValida ) { 511 this._showFieldError( 'error_fecha_hasta' ); 512 valid = false; 513 } 514 515 // Validar que fecha desde no sea mayor a fecha hasta 516 if ( desdeValida && hastaValida && fechaDesde.value && fechaHasta.value ) { 517 const desde = this._parseDateDmy( fechaDesde.value ); 518 const hasta = this._parseDateDmy( fechaHasta.value ); 519 if ( desde > hasta ) { 520 this._showFieldError( 'error_fecha_rango' ); 521 valid = false; 522 } 523 } 524 } 525 526 return valid; 527 } 528 529 /** 530 * Valida que una fecha en formato dd/mm/aaaa sea real 531 * @param {string} dateStr - Fecha en formato dd/mm/aaaa 532 * @return {boolean} 533 */ 534 _isValidDate( dateStr ) { 535 const match = dateStr.match( /^(\d{2})\/(\d{2})\/(\d{4})$/ ); 536 if ( !match ) return false; 537 538 const day = parseInt( match[1], 10 );
539 const month = parseInt( match[2], 10 ); 540 const year = parseInt( match[3], 10 ); 541 542 if ( month < 1 || month > 12 ) return false; 543 if ( year < 1900 || year > 2100 ) return false; 544 545 // Verificar dia valido para el mes (Date usa mes 0-indexed) 546 const date = new Date( year, month - 1, day ); 547 return date.getFullYear() === year && 548 date.getMonth() === month - 1 && 549 date.getDate() === day; 550 } 551 552 /** 553 * Parsea dd/mm/aaaa a objeto Date para comparacion 554 * @param {string} dateStr - Fecha en formato dd/mm/aaaa 555 * @return {Date} 556 */ 557 _parseDateDmy( dateStr ) { 558 const [ day, month, year ] = dateStr.split( '/' ).map( Number ); 559 return new Date( year, month - 1, day ); 560 } 561 562 _collectFormData() { 563 const fuenteArchivo = document.getElementById( 'fuente_archivo' ); 564 const archivoInput = document.getElementById( 'archivo_usernames' ); 565 const fechaDesde = document.getElementById( 'fecha_desde' ); 566 const fechaHasta = document.getElementById( 'fecha_hasta' ); 567 568 // Obtener filtro adicional seleccionado (radio buttons) 569 const filtroSeleccionado = document.querySelector( 'input[name="filtro_adicional"]:checked' ); 570 const filtroValor = filtroSeleccionado ? filtroSeleccionado.value : 'ninguno'; 571 572 const esArchivo = fuenteArchivo && fuenteArchivo.checked; 573 const esFinalizados = filtroValor === 'finalizados'; 574 const esAvanceCompleto = filtroValor === 'avance_completo'; 575 576 this.formData = { 577 fuente_usuarios: esArchivo ? 'archivo' : 'todos', 578 archivo: esArchivo && archivoInput ? archivoInput.files[0] : null, 579 cursos: this.doubleList.getSelectedItems(), 580 cursosIds: this.doubleList.getSelected(), 581 solo_finalizados: esFinalizados, 582 fecha_desde: esFinalizados && fechaDesde ? fechaDesde.value : '', 583 fecha_hasta: esFinalizados && fechaHasta ? fechaHasta.value : '', 584 avance_completo: esAvanceCompleto 585 }; 586 } 587 588 _showFieldError( elementId ) { 589 const el = document.getElementById( elementId ); 590 if ( el ) el.classList.remove( 'double_list_hidden' ); 591 } 592 593 _showFormError() { 594 const errorArea = document.getElementById( 'submit_error' ); 595 if ( errorArea ) errorArea.classList.remove( 'double_list_hidden' ); 596 } 597 598 _clearErrors() { 599 this.container.querySelectorAll( '.error_txt' ).forEach( el => { 600 el.classList.add( 'double_list_hidden' ); 601 } ); 602 const errorArea = document.getElementById( 'submit_error' ); 603 if ( errorArea ) errorArea.classList.add( 'double_list_hidden' ); 604 } 605 606 // ========================================================================= 607 // ENVIO A CONFIRMACION (server-side) 608 // ========================================================================= 609 610 _submitToConfirmacion() { 611 // Crear formulario temporal fuera del DOM actual 612 const tempForm = document.createElement( 'form' ); 613 tempForm.method = 'post'; 614 tempForm.enctype = 'multipart/form-data'; 615 tempForm.action = 'admin_herramientas.cgi'; 616 tempForm.style.display = 'none'; 617 618 // Campos del formulario 619 const fields = { 620 'wAccion': 'ejecutar', 621 'id_herramienta': this.idHerramienta, 622 'wid_cursoActual': this.idCursoActual, 623 'csrf_token': this.csrfToken, 624 'fuente_usuarios': this.formData.fuente_usuarios, 625 'solo_finalizados': this.formData.solo_finalizados ? '1' : '0', 626 'fecha_desde': this.formData.fecha_desde || '', 627 'fecha_hasta': this.formData.fecha_hasta || '', 628 'avance_completo': this.formData.avance_completo ? '1' : '0' 629 }; 630 631 // Agregar campos ocultos 632 for ( const [name, value] of Object.entries( fields ) ) { 633 const input = document.createElement( 'input' ); 634 input.type = 'hidden'; 635 input.name = name; 636 input.value = value; 637 tempForm.appendChild( input ); 638 } 639 640 // Agregar cursos seleccionados 641 this.formData.cursosIds.forEach( id => { 642 const input = document.createElement( 'input' ); 643 input.type = 'hidden'; 644 input.name = 'cursos'; 645 input.value = id; 646 tempForm.appendChild( input ); 647 } ); 648 649 // Mover el input file original al formulario temporal 650 const originalFileInput = document.querySelector( 'input[name="archivo_usernames"]' ); 651 if ( originalFileInput && originalFileInput.files.length > 0 ) { 652 // Clonar no funciona con files, hay que mover el elemento 653 tempForm.appendChild( originalFileInput ); 654 } 655
656 // Agregar al body y enviar 657 document.body.appendChild( tempForm ); 658 659 tempForm.submit(); 660 } 661 662} 663
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.