Sistema Integral de Gestión Deportiva UACH

Roles · resultados · estadísticas · ranking por facultad · sanciones · protestas · bitácora · reportes oficiales

Sistema elaborado por M.A. Héctor Ramírez
`;downloadBlob(name,html,'application/vnd.ms-excel;charset=utf-8')} function rolesRows(list){return (list||[]).sort((a,b)=>(a.fecha||'').localeCompare(b.fecha||'')).map(p=>[fmt(p.fecha),dep(p.deporte),p.rama||'Sin rama',p.fase||'',p.jornada||'',p.grupo||'',fac(p.a)+' vs '+fac(p.b),p.lugar||'',p.arbitro||'',p.estado||'',p.observaciones||''])} function resultRows(list){return (list||[]).map(r=>{let p=(db.partidos||[]).find(x=>x.id==r.partido)||{};return [r.fecha||'',dep(p.deporte),p.rama||'Sin rama',p.fase||'',p.jornada||'',fac(p.a)+' vs '+fac(p.b),`${r.ma} - ${r.mb}`,r.penal?fac(r.penal):'',r.obs||'']})} function rankRows(arr){return (arr||rankingFacultades()).map((x,i)=>[i+1,x.fac,x.nombre,x.deportes,x.jj,x.jg,x.je,x.jp,x.pf,x.pc,x.dif,x.pts])} function standingsFiltrado(deporte='',rama='',fase='',jornada=''){let rows={};db.facultades.forEach(f=>rows[f.id]={id:f.id,fac:f.siglas,nombre:f.nombre,lugar:0,jj:0,jg:0,jeg:0,jep:0,jp:0,je:0,pf:0,pc:0,dif:0,pts:0});db.resultados.forEach(r=>{let p=(db.partidos||[]).find(x=>x.id==r.partido);if(!p)return;if(deporte&&p.deporte!=deporte)return;if(rama&&(p.rama||'Sin rama')!=rama)return;if(fase&&(p.fase||'Sin fase')!=fase)return;if(jornada&&(p.jornada||'Sin jornada')!=jornada)return;let A=rows[p.a],B=rows[p.b],pt=puntos(r,p),fut=depObj(p.deporte).regla=='futbol';if(!A||!B)return;A.jj++;B.jj++;A.pf+=+r.ma;A.pc+=+r.mb;B.pf+=+r.mb;B.pc+=+r.ma;A.pts+=pt.a;B.pts+=pt.b;if(+r.ma>+r.mb){A.jg++;B.jp++}else if(+r.mb>+r.ma){B.jg++;A.jp++}else if(fut){if(r.penal==p.a){A.jeg++;B.jep++}else if(r.penal==p.b){B.jeg++;A.jep++}else{A.je++;B.je++}}else{A.je++;B.je++}});return Object.values(rows).filter(x=>x.jj>0).map(x=>{x.dif=x.pf-x.pc;return x}).sort((a,b)=>b.pts-a.pts||b.dif-a.dif||b.pf-a.pf||a.fac.localeCompare(b.fac)).map((x,i)=>({...x,lugar:i+1}))} function rankingFiltrado(prefix){let f=getF(prefix);let rows={};db.facultades.forEach(ff=>rows[ff.id]={id:ff.id,fac:ff.siglas,nombre:ff.nombre,jj:0,jg:0,je:0,jp:0,pf:0,pc:0,dif:0,pts:0,deportes:new Set()});resultadosFiltrados(prefix).forEach(r=>{let p=(db.partidos||[]).find(x=>x.id==r.partido);if(!p)return;let A=rows[p.a],B=rows[p.b],pt=puntos(r,p);A.jj++;B.jj++;A.pf+=+r.ma;A.pc+=+r.mb;B.pf+=+r.mb;B.pc+=+r.ma;A.pts+=pt.a;B.pts+=pt.b;A.deportes.add(p.deporte);B.deportes.add(p.deporte);if(+r.ma>+r.mb){A.jg++;B.jp++}else if(+r.mb>+r.ma){B.jg++;A.jp++}else{A.je++;B.je++}});return Object.values(rows).filter(x=>x.jj>0).map(x=>{x.dif=x.pf-x.pc;x.deportes=x.deportes.size;return x}).sort((a,b)=>b.pts-a.pts||b.dif-a.dif||b.pf-a.pf||a.fac.localeCompare(b.fac))} function tablaStandingsFiltrada(prefix){let f=getF(prefix);let st=standingsFiltrado(f.deporte,f.rama,f.fase,f.jornada);if(!st.length)return '

Sin estadísticas con los filtros seleccionados.

';return `${st.map(x=>``).join('')}
LugarFacultadJJJGJE/JEGJEPJPPFPCDIFPuntos
${x.lugar}${x.fac}${x.jj}${x.jg}${x.je+x.jeg}${x.jep}${x.jp}${x.pf}${x.pc}${x.dif}${x.pts}
`} function exportRolesExcel(){xlsTable('roles_filtrados_uach.xls','Roles filtrados UACH',['Fecha','Deporte','Rama','Fase','Jornada','Grupo','Partido','Lugar','Árbitro','Estado','Observaciones'],rolesRows(partidosFiltrados('rol')))} function exportResultadosExcel(){xlsTable('resultados_filtrados_uach.xls','Resultados filtrados UACH',['Fecha','Deporte','Rama','Fase','Jornada','Partido','Marcador','Penales','Observaciones'],resultRows(resultadosFiltrados('res')))} function exportStatsExcel(){let f=getF('est');let st=standingsFiltrado(f.deporte,f.rama,f.fase,f.jornada);xlsTable('estadisticas_filtradas_uach.xls','Estadísticas filtradas UACH',['Lugar','Facultad','JJ','JG','JE/JEG','JEP','JP','PF','PC','DIF','Puntos'],st.map(x=>[x.lugar,x.fac,x.jj,x.jg,x.je+x.jeg,x.jep,x.jp,x.pf,x.pc,x.dif,x.pts]))} function exportRankingExcel(){xlsTable('ranking_facultades_filtrado_uach.xls','Ranking de facultades filtrado UACH',['Lugar','Facultad','Nombre','Deportes','Juegos','JG','JE','JP','PF','PC','DIF','Puntos'],rankRows(rankingFiltrado('rank')))} function exportReporteExcel(){let f=getF('rep');let rr=resultadosFiltrados('rep');xlsTable('reporte_filtrado_uach.xls','Reporte filtrado UACH',['Fecha','Deporte','Rama','Fase','Jornada','Partido','Marcador','Penales','Observaciones'],resultRows(rr))} function roles(){let visibles=partidosFiltrados('rol'); return `${isAdmin()?`

Nuevo rol de juego

`:adminMsg()}

Roles de juego

${panelFiltrosAvanzado('rol')}
Roles visibles
${visibles.length}
Deportes visibles
${new Set(visibles.map(p=>p.deporte)).size}
Ramas visibles
${new Set(visibles.map(p=>p.rama||'Sin rama')).size}
Finalizados
${visibles.filter(p=>p.estado=='Finalizado').length}
${tablaPartidos(visibles,false,true)}
`} function resultados(){let visibles=resultadosFiltrados('res');return `${isAdmin()?`

Capturar resultado

Selecciona el partido y captura el marcador. Las estadísticas y ranking se actualizan automáticamente.

`:adminMsg()}

Resultados

${panelFiltrosAvanzado('res')}
Resultados visibles
${visibles.length}
Deportes
${new Set(visibles.map(r=>((db.partidos||[]).find(p=>p.id==r.partido)||{}).deporte)).size}
Ramas
${new Set(visibles.map(r=>((db.partidos||[]).find(p=>p.id==r.partido)||{}).rama||'Sin rama')).size}
Total marcador
${visibles.reduce((a,r)=>a+(+r.ma)+(+r.mb),0)}
${tablaResultados(visibles,true)}
`} function estadisticas(){let visibles=resultadosFiltrados('est');let totalPF=visibles.reduce((a,r)=>a+(+r.ma)+(+r.mb),0);return `

Estadísticas filtrables

${panelFiltrosAvanzado('est')}
Partidos visibles
${visibles.length}
Puntos/favor visibles
${totalPF}
Promedio visible
${visibles.length?(totalPF/visibles.length).toFixed(1):0}
Facultades visibles
${new Set(visibles.flatMap(r=>{let p=(db.partidos||[]).find(x=>x.id==r.partido)||{};return [p.a,p.b]}).filter(Boolean)).size}

Tabla estadística según filtros

${tablaStandingsFiltrada('est')}

Resultados que alimentan esta estadística

${tablaResultados(visibles,true)}
`} function tabla(){let arr=rankingFiltrado('rank');return `

Ranking general por facultad

${panelFiltrosAvanzado('rank')}

Ranking basado en resultados capturados. Si filtras por deporte, rama, jornada o fase, el ranking se recalcula solo con esa información.

${!arr.length?'

Sin resultados con los filtros seleccionados.

':`${arr.map((x,i)=>``).join('')}
LugarFacultadNombreDeportesJuegosJGJEJPPFPCDIFPuntos
${i+1}${x.fac}${x.nombre}${x.deportes}${x.jj}${x.jg}${x.je}${x.jp}${x.pf}${x.pc}${x.dif}${x.pts}
`}

Tabla estadística del filtro actual

${tablaStandingsFiltrada('rank')}
`} function reportes(){let visibles=resultadosFiltrados('rep');return `

Centro de descargas

Aquí cualquier persona puede filtrar y descargar el reporte exacto para enviarlo por WhatsApp.

${panelFiltrosAvanzado('rep')}

Resumen del filtro actual

Resultados
${visibles.length}
Roles
${partidosFiltrados('rep').length}
Marcador total
${visibles.reduce((a,r)=>a+(+r.ma)+(+r.mb),0)}
Facultades
${new Set(visibles.flatMap(r=>{let p=(db.partidos||[]).find(x=>x.id==r.partido)||{};return [p.a,p.b]}).filter(Boolean)).size}

Tabla estadística

${tablaStandingsFiltrada('rep')}

Resultados filtrados

${tablaResultados(visibles,true)}

Roles filtrados

${tablaPartidos(partidosFiltrados('rep'),false,true)}
`} function downloadRolesPDF(){simplePDF('roles_filtrados_uach.pdf','Roles filtrados UACH',linesRoles(partidosFiltrados('rol')))} function downloadRolesWord(){wordDoc('roles_filtrados_uach.doc','Roles filtrados UACH',linesRoles(partidosFiltrados('rol')))} function downloadStatsPDF(){simplePDF('estadisticas_filtradas_uach.pdf','Estadísticas filtradas UACH',linesStatsFiltradas('est'))} function downloadStatsWord(){wordDoc('estadisticas_filtradas_uach.doc','Estadísticas filtradas UACH',linesStatsFiltradas('est'))} function downloadRankingPDF(){simplePDF('ranking_facultades_filtrado_uach.pdf','Ranking de facultades filtrado UACH',rankingFiltrado('rank').map((x,i)=>`${i+1}. ${x.fac} | Juegos:${x.jj} | JG:${x.jg} JE:${x.je} JP:${x.jp} | PF:${x.pf} PC:${x.pc} DIF:${x.dif} | Puntos:${x.pts}`))} function downloadRankingWord(){wordDoc('ranking_facultades_filtrado_uach.doc','Ranking de facultades filtrado UACH',rankingFiltrado('rank').map((x,i)=>`${i+1}. ${x.fac} | Juegos:${x.jj} | JG:${x.jg} JE:${x.je} JP:${x.jp} | PF:${x.pf} PC:${x.pc} DIF:${x.dif} | Puntos:${x.pts}`))} function linesStatsFiltradas(prefix){let f=getF(prefix);let st=standingsFiltrado(f.deporte,f.rama,f.fase,f.jornada);let res=resultadosFiltrados(prefix);let total=res.reduce((a,r)=>a+(+r.ma)+(+r.mb),0);return [`Filtros: ${f.deporte?dep(f.deporte):'Todos los deportes'} | ${f.rama||'Todas las ramas'} | ${f.jornada||'Todas las jornadas'} | ${f.fase||'Todas las fases'}`,`Partidos: ${res.length}`,`Puntos/favor: ${total}`,'','Tabla estadística:'].concat(st.map(x=>`${x.lugar}. ${x.fac} | JJ:${x.jj} JG:${x.jg} JE/JEG:${x.je+x.jeg} JEP:${x.jep} JP:${x.jp} PF:${x.pf} PC:${x.pc} DIF:${x.dif} PTS:${x.pts}`))} /* === DESCARGAS REALES DESDE SERVIDOR LOCAL === */ function qsFrom(prefix){ const f = getF(prefix); const q = new URLSearchParams(); Object.keys(f).forEach(k=>{ if(f[k]) q.set(k,f[k]); }); return q; } function serverDownload(kind, prefix, fmt){ const q = qsFrom(prefix); q.set('kind', kind); q.set('fmt', fmt || 'xls'); window.location.href = 'api/export.php?' + q.toString(); } function exportRolesExcel(){ serverDownload('roles','rol','xls'); } function downloadRolesPDF(){ serverDownload('roles','rol','pdf'); } function downloadRolesWord(){ serverDownload('roles','rol','doc'); } function exportResultadosExcel(){ serverDownload('resultados','res','xls'); } function exportStatsExcel(){ serverDownload('estadisticas','est','xls'); } function downloadStatsPDF(){ serverDownload('estadisticas','est','pdf'); } function downloadStatsWord(){ serverDownload('estadisticas','est','doc'); } function exportRankingExcel(){ serverDownload('ranking','rank','xls'); } function downloadRankingPDF(){ serverDownload('ranking','rank','pdf'); } function downloadRankingWord(){ serverDownload('ranking','rank','doc'); } function exportReporteExcel(){ serverDownload('reporte','rep','xls'); } function exportReportePDF(){ serverDownload('reporte','rep','pdf'); } function exportReporteWord(){ serverDownload('reporte','rep','doc'); } function toggleCred(id,val){window.credSel=window.credSel||{}; window.credSel[id]=!!val} function toggleAllCred(val){window.credSel=window.credSel||{}; currentCredList().forEach(j=>window.credSel[j.id]=!!val); render()} function clearCredSelection(){window.credSel={}; render()} function currentCredList(){let fd=window.credDep||'', ff=window.credFac||'', fe=window.credEst||'', foto=window.credFoto||'todas', q=(window.credQ||'').toLowerCase();return (db.jugadores||[]).filter(j=>(!fd||j.deporte==fd)&&(!ff||j.facultad==ff)&&(!fe||j.estatus==fe)&&(foto=='todas'||(foto=='con'&&j.foto)||(foto=='sin'&&!j.foto))&&(!q||[j.nombre,j.matricula,j.numero,j.correo,j.telefono,fac(j.facultad),dep(j.deporte),j.estatus].join(' ').toLowerCase().includes(q)))} function credCard(j){let equipo=((db.equipos||[]).find(e=>e.facultad==j.facultad&&e.deporte==j.deporte)||{});let est=j.estatus||'Pendiente';return `
${escHtml(db.config?.torneo||'TORNEO INTERFACULTADES UACH')}Credencial de jugador
${typeof qrImg==='function'?qrImg(j,58):''}
${j.foto?``:`
FOTO
PENDIENTE
`}

${escHtml(j.nombre||'SIN NOMBRE')}

Matrícula/ID: ${escHtml(j.matricula||j.id||'')}

Equipo/Facultad: ${escHtml(fac(j.facultad)||'')}

Deporte: ${escHtml(dep(j.deporte)||'')}

Rama/Categoría: ${escHtml(equipo.rama||j.rama||'')}

Número: ${escHtml(j.numero||'')}

Folio: ${escHtml(j.id||'')}${escHtml(est)}
`} function printCredenciales(soloSeleccionadas){let lista=soloSeleccionadas?(db.jugadores||[]).filter(j=>window.credSel&&window.credSel[j.id]):currentCredList(); if(!lista.length){alert('No hay credenciales para imprimir. Revisa filtros o selecciona jugadores.');return;} let html=`

Credenciales ${escHtml(db.config?.torneo||'Torneo Interfacultades UACH')}

Generado: ${new Date().toLocaleString()} · Total: ${lista.length}

${lista.map(j=>credCard(j)).join('')}
`; printDoc('credenciales_uach',html,'landscape')} function printDoc(title,html,orient='portrait'){let w=window.open('','_blank');w.document.write(`${title} ${html} `);w.document.close()} function printResultadosPDF(){let rows=linesResultados(resultadosFiltrados('res'));let html=`

Resultados filtrados UACH

Generado: ${new Date().toLocaleString()}

${rows.map((r,i)=>``).join('')}
${escHtml(r)}
`;let w=window.open('','_blank');w.document.write(`resultados_filtrados_uach ${html} `);w.document.close()} /* === V5: flujo de aprobación, permisos por entrenador, QR/folio y panel de cotejo === */ function canSeePage(k){ if(!user) return ['dashboard','roles','estadisticas','tabla','reportes','jugadores','credenciales','cotejo','avisos'].includes(k); if(isAdmin()) return true; if(isCoach()) return ['dashboard','roles','estadisticas','tabla','reportes','equipos','jugadores','credenciales','cotejo','avisos'].includes(k); return ['dashboard','roles','estadisticas','tabla','reportes','jugadores','credenciales','cotejo','avisos'].includes(k); } function renderNav(){ nav.innerHTML=pages.filter(p=>canSeePage(p[0])).map(p=>``).join(''); } function visibleJugadores(list){ list=list||db.jugadores||[]; if(isCoach()) return list.filter(j=>!user.facultad || j.facultad==user.facultad); return list; } function visibleEquipos(list){ list=list||db.equipos||[]; if(isCoach()) return list.filter(e=>!user.facultad || e.facultad==user.facultad); return list; } function canManageJugador(j){ if(isAdmin()) return true; if(isCoach()) return j && (!user.facultad || j.facultad==user.facultad); return false; } function canApprove(){ return isAdmin(); } function verifyCode(j){ return ('UACH-'+String(j.id||'').slice(0,6)+'-'+String(j.matricula||'SN').replace(/\W/g,'').slice(-4)).toUpperCase(); } function playerByVerify(q){ q=String(q||'').trim().toUpperCase(); return (db.jugadores||[]).find(j=>String(j.id||'').toUpperCase()==q || verifyCode(j)==q || String(j.matricula||'').toUpperCase()==q); } function jugadores(){ let q=(window.jugSearch||'').toLowerCase(); let base=visibleJugadores(db.jugadores); let lista=base.filter(j=>[j.nombre,j.matricula,j.numero,fac(j.facultad),facN(j.facultad),dep(j.deporte),j.carrera,j.semestre,j.telefono,j.correo,j.estatus,j.observaciones,verifyCode(j)].join(' ').toLowerCase().includes(q)); let facOpts=isCoach()?(db.facultades||[]).filter(f=>!user.facultad||f.id==user.facultad):db.facultades; return `${canEdit()?`

Nuevo jugador

El entrenador puede registrar y corregir jugadores de su equipo. El administrador valida la elegibilidad y aprueba la credencial.

`:''}

Jugadores

${lista.length} visibles
${lista.map(j=>``).join('')}
FotoNombreMatrículaNo.FacultadDeporteContactoEstatusFolio cotejo
${j.foto?``:`
FOTO
PENDIENTE
`}
${escHtml(j.nombre||'')}
${escHtml(j.observaciones||'')}
${escHtml(j.matricula||'')}${escHtml(j.numero||'')}${escHtml(fac(j.facultad))}${escHtml(dep(j.deporte))}${escHtml(j.telefono||'')}
${escHtml(j.correo||'')}
${escHtml(j.estatus||'Pendiente')}${!j.foto?'
Foto pendiente':''}
${verifyCode(j)}${canManageJugador(j)?`${canApprove()?``:''}`:''}
`; } async function addJugador(){ if(isCoach() && user.facultad) jf.value=user.facultad; if(db.jugadores.some(j=>j.matricula&&j.matricula==jm.value&&j.deporte==jd.value))return alert('Esta matrícula ya está registrada en este deporte.'); let foto=''; if(jfoto.files&&jfoto.files[0])foto=await subirArchivo(jfoto.files[0]); db.jugadores.push({id:uid(),nombre:jn.value,matricula:jm.value,numero:jnum.value,facultad:jf.value,deporte:jd.value,carrera:jc.value,semestre:js.value,telefono:jt.value,correo:jco.value,estatus:isCoach()?'Pendiente':(jest.value||'Pendiente'),observaciones:jobs.value||'',foto:foto,validadoPor:'',fechaValidacion:''}); save('Registró jugador'); } function editJugador(id){ let j=(db.jugadores||[]).find(x=>x.id==id); if(!j||!canManageJugador(j)) return alert('No tienes permiso para editar este jugador.'); let facOpts=isCoach()?(db.facultades||[]).filter(f=>!user.facultad||f.id==user.facultad):db.facultades; openM(`

Editar jugador

${j.foto?``:'Sin foto'}

Folio de cotejo: ${verifyCode(j)}

`) } async function saveJugador(id){ let j=(db.jugadores||[]).find(x=>x.id==id); if(!j||!canManageJugador(j)) return alert('No tienes permiso para editar este jugador.'); j.nombre=mjn.value; j.matricula=mjm.value; j.numero=mjnum.value; j.facultad=isCoach()&&user.facultad?user.facultad:mjf.value; j.deporte=mjd.value; j.carrera=mjc.value; j.semestre=mjs.value; j.telefono=mjt.value; j.correo=mjco.value; if(isAdmin()) j.estatus=mjest.value; else j.estatus='Pendiente'; j.observaciones=mjobs.value; if(mjfoto.files&&mjfoto.files[0])j.foto=await subirArchivo(mjfoto.files[0]); closeM(); save('Editó jugador'); } function aprobarJugador(id){ if(!canApprove()) return alert('Solo administrador.'); let j=(db.jugadores||[]).find(x=>x.id==id); if(!j)return; j.estatus='Aprobado'; j.validadoPor=user?.nombre||user?.usuario||'admin'; j.fechaValidacion=new Date().toLocaleString(); save('Aprobó jugador'); } function rechazarJugador(id){ if(!canApprove()) return alert('Solo administrador.'); let j=(db.jugadores||[]).find(x=>x.id==id); if(!j)return; let obs=prompt('Motivo u observación del rechazo:',j.observaciones||''); j.estatus='Rechazado'; j.observaciones=obs||j.observaciones||'Rechazado por comité'; j.validadoPor=user?.nombre||user?.usuario||'admin'; j.fechaValidacion=new Date().toLocaleString(); save('Rechazó jugador'); } function cotejo(){ let q=window.cotejoQ||''; let j=playerByVerify(q); let lista=visibleJugadores(db.jugadores).filter(x=>!q||[x.nombre,x.matricula,fac(x.facultad),dep(x.deporte),verifyCode(x)].join(' ').toLowerCase().includes(q.toLowerCase())).slice(0,40); return `

Panel de cotejo en cancha

Busca por folio de credencial, matrícula o nombre para revisar si el jugador está aprobado. Este panel sirve para evitar cachirules o jugadores inelegibles.

${j?cotejoCard(j):''}

Resultados rápidos

${lista.map(x=>``).join('')}
FotoJugadorFacultadDeporteEstatusFolio
${x.foto?``:`
FOTO
PENDIENTE
`}
${escHtml(x.nombre||'')}
${escHtml(x.matricula||'')}
${escHtml(fac(x.facultad))}${escHtml(dep(x.deporte))}${escHtml(x.estatus||'Pendiente')}${verifyCode(x)}
` } function cotejoCard(j){ let ok=String(j.estatus||'').toLowerCase()=='aprobado'||String(j.estatus||'').toLowerCase()=='activo'; return `
${j.foto?``:`
FOTO
PENDIENTE
`}

${escHtml(j.nombre||'SIN NOMBRE')}

Matrícula: ${escHtml(j.matricula||'')}

Facultad: ${escHtml(fac(j.facultad))}

Deporte: ${escHtml(dep(j.deporte))}

Número: ${escHtml(j.numero||'')}

Folio: ${verifyCode(j)}

Estatus: ${escHtml(j.estatus||'Pendiente')}

${ok?'✅ JUGADOR ELEGIBLE':'⚠️ REVISAR ANTES DE PARTICIPAR'}

${ok?'Validado por comité.':'Pendiente, rechazado o con información incompleta.'}

Observaciones: ${escHtml(j.observaciones||'')}

`; } function pseudoQR(j){ let code=verifyCode(j), seed=0; for(let i=0;i6)||(r>6&&c<2)||(((seed+r*17+c*31)%5)<2)); cells+=``; }} return `
${cells}
`; } function credCard(j){ let equipo=((db.equipos||[]).find(e=>e.facultad==j.facultad&&e.deporte==j.deporte)||{});let est=j.estatus||'Pendiente';return `
${escHtml(db.config?.torneo||'TORNEO INTERFACULTADES UACH')}Credencial de jugador
${pseudoQR(j)}
${j.foto?``:`
FOTO
PENDIENTE
`}

${escHtml(j.nombre||'SIN NOMBRE')}

Matrícula/ID: ${escHtml(j.matricula||j.id||'')}

Equipo/Facultad: ${escHtml(fac(j.facultad)||'')}

Deporte: ${escHtml(dep(j.deporte)||'')}

Rama/Categoría: ${escHtml(equipo.rama||j.rama||'')}

Número: ${escHtml(j.numero||'')}

Folio cotejo: ${escHtml(verifyCode(j))}${escHtml(est)}
` } /* === V6: cotejo funcional, búsqueda manual, verificación por folio/QR real y credenciales escaneables === */ function v6Normalize(v){return String(v||'').trim().toLowerCase();} function verifyCode(j){ if(!j) return ''; if(!j.folioCotejo){ let base=(j.matricula||j.id||'').toString().replace(/[^a-zA-Z0-9]/g,'').toUpperCase(); j.folioCotejo='UACH-'+(base||String(j.id||'0000').slice(-6).toUpperCase()); } return j.folioCotejo; } function verificationUrl(j){ let base=location.origin+location.pathname; return base+'?folio='+encodeURIComponent(verifyCode(j)); } function qrImg(j,sz=84){ let data=encodeURIComponent(verificationUrl(j)); return `QR cotejo`; } function playerByVerify(q){ q=v6Normalize(q); if(!q) return null; return visibleJugadores(db.jugadores).find(j=>{ let code=v6Normalize(verifyCode(j)); let url=v6Normalize(verificationUrl(j)); return code===q || url===q || v6Normalize(j.matricula)===q || v6Normalize(j.id)===q || v6Normalize(j.nombre)===q; }) || visibleJugadores(db.jugadores).find(j=>{ let joined=[verifyCode(j),j.matricula,j.id,j.nombre,fac(j.facultad),dep(j.deporte),j.numero].join(' '); return v6Normalize(joined).includes(q); }) || null; } function buscarCotejo(){ let val=document.getElementById('cotejoInput')?.value||''; window.cotejoQ=val.trim(); render(); } function limpiarCotejo(){ window.cotejoQ=''; let u=new URL(location.href); u.searchParams.delete('folio'); history.replaceState(null,'',u.toString()); render(); } function cotejo(){ let q=window.cotejoQ||new URLSearchParams(location.search).get('folio')||''; window.cotejoQ=q; let j=playerByVerify(q); let lista=visibleJugadores(db.jugadores).filter(x=>!q||[x.nombre,x.matricula,fac(x.facultad),dep(x.deporte),verifyCode(x),x.numero,x.estatus].join(' ').toLowerCase().includes(q.toLowerCase())).slice(0,60); let notFound=q&&!j?`

❌ No encontrado

No se encontró jugador con ese folio, matrícula o nombre. Revisa que el dato esté escrito igual que en la credencial.

`:''; return `

Panel de cotejo en cancha

Escanea el QR de la credencial o escribe folio, matrícula o nombre. El sistema muestra si el jugador está aprobado, pendiente o rechazado.

Si escaneas el QR, se abre esta pantalla con el folio oficial cargado para cotejo.

${j?cotejoCard(j):notFound}

Resultados rápidos para cotejo

${lista.map(x=>``).join('')}
FotoJugadorFacultadDeporteEstatusFolio
${x.foto?``:`
FOTO
PENDIENTE
`}
${escHtml(x.nombre||'')}
${escHtml(x.matricula||'')}
${escHtml(fac(x.facultad))}${escHtml(dep(x.deporte))}${escHtml(x.estatus||'Pendiente')}${!x.foto?'
Sin foto':''}
${verifyCode(x)}
`; } function cotejoCard(j){ let est=String(j.estatus||'Pendiente'); let ok=est.toLowerCase()==='aprobado'||est.toLowerCase()==='activo'; let color=ok?'#047857':(est.toLowerCase()==='rechazado'?'#991b1b':'#b45309'); let titulo=ok?'✅ JUGADOR ELEGIBLE':(est.toLowerCase()==='rechazado'?'⛔ JUGADOR RECHAZADO':'⚠️ JUGADOR PENDIENTE DE VALIDACIÓN'); return `
${j.foto?``:`
FOTO
PENDIENTE
`}
${qrImg(j,110)}

${titulo}

${escHtml(j.nombre||'SIN NOMBRE')}

Matrícula/ID: ${escHtml(j.matricula||'')}

Facultad/equipo: ${escHtml(fac(j.facultad))}

Deporte: ${escHtml(dep(j.deporte))}

Número: ${escHtml(j.numero||'')}

Folio de cotejo: ${escHtml(verifyCode(j))}

Estatus: ${escHtml(est)}

Validado por: ${escHtml(j.validadoPor||'Pendiente')}

Fecha validación: ${escHtml(j.fechaValidacion||'Pendiente')}

Observaciones: ${escHtml(j.observaciones||'')}

Liga QR: ${escHtml(verificationUrl(j))}

Probar QR / abrir cotejo

`; } function credCard(j){ let equipo=((db.equipos||[]).find(e=>e.facultad==j.facultad&&e.deporte==j.deporte)||{});let est=j.estatus||'Pendiente'; return `
${escHtml(db.config?.torneo||'TORNEO INTERFACULTADES UACH')}Credencial oficial de jugador
${qrImg(j,54)}
${j.foto?``:`
FOTO
PENDIENTE
`}

${escHtml(j.nombre||'SIN NOMBRE')}

Matrícula/ID: ${escHtml(j.matricula||j.id||'')}

Equipo/Facultad: ${escHtml(fac(j.facultad)||'')}

Deporte: ${escHtml(dep(j.deporte)||'')}

Rama/Categoría: ${escHtml(equipo.rama||j.rama||'')}

Número: ${escHtml(j.numero||'')}

Folio cotejo: ${escHtml(verifyCode(j))}${escHtml(est)}
`; } function printDoc(title,html,orient='portrait'){ let w=window.open('','_blank'); w.document.write(`${title} ${html} `); w.document.close(); } /* === V7: credenciales 6 por hoja, diseño compacto y QR preparado para dominio real === */ function verificationUrl(j){ // URL real de cotejo: funciona en servidor de pruebas y en dominio real. Si mueves el sistema a otro dominio, se ajusta solo. let configured=(db.config&&db.config.urlCotejo)?String(db.config.urlCotejo).trim():''; let base=configured || (location.origin+location.pathname); let sep=base.includes('?')?'&':'?'; return base+sep+'page=cotejo&folio='+encodeURIComponent(verifyCode(j)); } function credCard(j){ let equipo=((db.equipos||[]).find(e=>e.facultad==j.facultad&&e.deporte==j.deporte)||{}); let est=j.estatus||'Pendiente'; return `
${escHtml(db.config?.torneo||'TORNEO INTERFACULTADES UACH')}Credencial oficial · Comité deportivo
${qrImg(j,58)}
${j.foto?``:`
FOTO
PENDIENTE
`}

${escHtml(j.nombre||'SIN NOMBRE')}

Matrícula/ID: ${escHtml(j.matricula||j.id||'')}

Equipo/Facultad: ${escHtml(fac(j.facultad)||'')}

Deporte: ${escHtml(dep(j.deporte)||'')}

Rama/Categoría: ${escHtml(equipo.rama||j.rama||'')}

Número: ${escHtml(j.numero||'')}

Folio: ${escHtml(verifyCode(j))}${escHtml(est)}
`; } function printDoc(title,html,orient='landscape'){ let w=window.open('','_blank'); w.document.write(`${title} ${html} `); w.document.close(); } function printCredenciales(soloSeleccionadas){ let lista=soloSeleccionadas?(db.jugadores||[]).filter(j=>window.credSel&&window.credSel[j.id]):currentCredList(); if(!lista.length){alert('No hay credenciales para imprimir. Revisa filtros o selecciona jugadores.');return;} let html=`

Credenciales ${escHtml(db.config?.torneo||'Torneo Interfacultades UACH')}

Generado: ${new Date().toLocaleString()} · Total: ${lista.length} · Formato carta horizontal: 6 credenciales por hoja

${lista.map(j=>credCard(j)).join('')}
`; printDoc('credenciales_uach_6_por_hoja',html,'landscape') } /* === V8: permisos entrenador, fútbol con penales A/B, voleibol por sets y QR/cotejo directo === */ function isVoleibolId(id){ return (dep(id)||'').toLowerCase().includes('voleibol'); } function isFutbolId(id){ let d=depObj(id); return d.regla==='futbol' || (dep(id)||'').toLowerCase().includes('fútbol') || (dep(id)||'').toLowerCase().includes('futbol'); } function canSeePage(k){ if(!user) return ['dashboard','roles','estadisticas','reportes','jugadores','credenciales','cotejo','avisos'].includes(k); if(isAdmin()) return true; if(isCoach()) return ['dashboard','roles','estadisticas','reportes','equipos','jugadores','credenciales','cotejo','avisos'].includes(k); return ['dashboard','roles','resultados','estadisticas','reportes','jugadores','credenciales','cotejo','avisos'].includes(k); } function render(){ renderNav(); if(!canSeePage(page)) page='dashboard'; let map={dashboard,roles,resultados,estadisticas,tabla,reportes,protestas,sanciones,equipos,jugadores,credenciales,cotejo,deportes,facultades,usuarios,bitacora,avisos}; app.innerHTML=(map[page]||dashboard)(); applyRoleChrome(); } function applyRoleChrome(){ // Herramientas de respaldo/importación y ranking global: solo administrador. let tools=document.getElementById('adminTools'); if(tools) tools.style.display=isAdmin()?'block':'none'; let btns=[...document.querySelectorAll('aside button')]; btns.forEach(b=>{ if((b.textContent||'').toLowerCase().includes('exportar ranking')) b.style.display=isAdmin()?'block':'none'; }); } function renderNav(){ nav.innerHTML=pages.filter(p=>canSeePage(p[0])).map(p=>``).join(''); } function resultados(){ let visibles=resultadosFiltrados('res'); return `${isAdmin()?`

Capturar resultado

Selecciona el partido. El formulario cambia según el deporte: fútbol con penales y voleibol por sets.

`:''}

Resultados

${panelFiltrosAvanzado('res')}
Resultados visibles
${visibles.length}
Deportes
${new Set(visibles.map(r=>((db.partidos||[]).find(p=>p.id==r.partido)||{}).deporte)).size}
Ramas
${new Set(visibles.map(r=>((db.partidos||[]).find(p=>p.id==r.partido)||{}).rama||'Sin rama')).size}
Total marcador
${visibles.reduce((a,r)=>a+(+r.ma)+(+r.mb),0)}
${tablaResultados(visibles,true)}
`); w.document.close(); }; const _cedulasBase=window.cedulas_arbitro; window.cedulas_arbitro=function(){ if(isCoach()){ let list=(db.partidos||[]).filter(canSeePartido).sort((a,b)=>(b.fecha||'').localeCompare(a.fecha||'')); return `

Cédulas y resultados

El entrenador consulta cédulas/resultados y puede presentar protesta con evidencia. No puede editar captura arbitral.

${list.length?`${list.map(p=>{let r=(db.resultados||[]).find(x=>x.partido==p.id)||{}; return ``}).join('')}
PartidoEstadoResultadoAcción
${escHtml(dep(p.deporte))} · ${escHtml(fac(p.a))} vs ${escHtml(fac(p.b))}
${fmt(p.fecha)} · ${escHtml(p.rama||'')} · ${escHtml(p.jornada||'')}
${typeof cedulaEstadoDigital==='function'?cedulaEstadoDigital(p):escHtml(p.estado||'')}${r.id?`${escHtml(r.ma)} - ${escHtml(r.mb)}`:'Pendiente'}
`:'

No hay partidos visibles para tu usuario.

'}
`; } return _cedulasBase?_cedulasBase():'

No disponible.

'; }; const _protBase=window.protestas; window.protestas=function(){ let html=_protBase?_protBase():''; setTimeout(()=>{if(window.protestaPartido&&document.getElementById('ppar')) document.getElementById('ppar').value=window.protestaPartido;},30); return html; }; window.herramientas=function(){ if(!isAdmin()) return '
Solo administrador.
'; return `

Herramientas del sistema

Sistema en producción: esta opción no borra usuarios, roles, configuración, deportes, facultades ni permisos. Antes de limpiar descarga un respaldo JSON.

Limpia: jugadores, resultados, cédulas, protestas, evidencias, validaciones y estadísticas generadas. Conserva estructura, usuarios, roles, equipos, deportes y facultades.

`; }; window.descargarBackupSistema=function(){let blob=new Blob([JSON.stringify(db,null,2)],{type:'application/json'}); let a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='backup_uach_'+new Date().toISOString().slice(0,10)+'.json'; a.click(); URL.revokeObjectURL(a.href);}; window.limpiarDatosOperativos=function(){ if(!isAdmin()) return; if(!confirm('Se limpiarán datos operativos de temporada/pruebas, conservando usuarios, roles, equipos y configuración. ¿Continuar?')) return; db.jugadores=[]; db.resultados=[]; db.protestas=[]; db.cedulas=[]; db.cedulaEventos=[]; (db.partidos||[]).forEach(p=>{p.estado='Programado'; delete p.cedulaEstado; delete p.cedulaPdf; delete p.cedulaPdfNombre; delete p.cedulaPdfFecha;}); save('Reinició temporada / limpió datos operativos'); }; /* === PATCH DEFINITIVO: EQUIPOS SIN FACULTAD OBLIGATORIA === */ (function(){ function t(v){ return (typeof txt==='function') ? txt(v||'') : String(v||'').replace(/[&<>\"]/g,m=>({'&':'&','<':'<','>':'>','\"':'"'}[m])); } function getTipo(e){ return e.tipoEquipo || e.tipo || 'Invitado'; } function getProc(e){ return e.procedencia || e.institucion || e.facultadNombre || (e.facultad && typeof fac==='function' ? fac(e.facultad) : '') || ''; } window.nombreEquipoVista=function(e){ return e.nombre || getProc(e) || 'Equipo'; }; window.equipos=function(){ let puedeCrear = (typeof isAdmin==='function' && isAdmin()) || (typeof isCoach==='function' && isCoach()); let lista = (typeof visibleEquipos==='function' ? visibleEquipos(db.equipos||[]) : (db.equipos||[])); let q = String(window.eqSearch||'').toLowerCase(); lista = lista.filter(e=>[nombreEquipoVista(e), getTipo(e), getProc(e), (typeof dep==='function'?dep(e.deporte):e.deporte), e.rama, e.entrenador, e.correo, e.telefono, e.estatus].join(' ').toLowerCase().includes(q)); let form = puedeCrear ? `

Agregar equipo

El equipo se registra por nombre. Puede ser UACH, invitado, externo, institución o club; no depende de una facultad.

` : ''; return `${form}

Equipos

${lista.length} visibles
${lista.map(e=>``).join('')}
EquipoTipoInstitución / procedenciaDeporteCategoríaEntrenadorContactoEstatusAcción
${t(nombreEquipoVista(e))}${t(getTipo(e))}${t(getProc(e))}${t(typeof dep==='function'?dep(e.deporte):e.deporte)}${t(e.rama||'')}${t(e.entrenador||'')}${t(e.correo||'')}
${t(e.telefono||'')}
${t(e.estatus||'Activo')}${((typeof isAdmin==='function'&&isAdmin())||(typeof esDueñoEquipo==='function'&&esDueñoEquipo(e)))?`${(typeof isAdmin==='function'&&isAdmin())?``:''}`:''}
`; }; window.guardarEquipoSinFacultad=function(){ if(!((typeof isAdmin==='function'&&isAdmin())||(typeof isCoach==='function'&&isCoach()))) return alert('No tienes permiso para agregar equipos.'); let obj={id:uid(),nombre:(en.value||'').trim(),tipoEquipo:etipo.value,procedencia:(eproc.value||'').trim(),institucion:(eproc.value||'').trim(),facultad:'',deporte:ed.value,rama:er.value,entrenador:ee.value,correo:ec.value,telefono:etel.value,estatus:'Activo',entrenadorId:(user&&user.id)||'',entrenadorUsuario:(user&&user.usuario)||'',creadoPor:(user&&user.usuario)||''}; if(!obj.nombre) return alert('Escribe el nombre del equipo.'); if(!obj.deporte) return alert('Selecciona el deporte.'); db.equipos.push(obj); save('Registró equipo'); }; window.addEquipoProd=window.guardarEquipoSinFacultad; window.addEquipoEntrenador=window.guardarEquipoSinFacultad; window.editarEquipoSinFacultad=function(id){ let e=(db.equipos||[]).find(x=>x.id==id); if(!e||!((typeof isAdmin==='function'&&isAdmin())||(typeof esDueñoEquipo==='function'&&esDueñoEquipo(e)))) return alert('No tienes permiso para editar este equipo.'); let tipo=getTipo(e); openM(`

Editar equipo

${(typeof isAdmin==='function'&&isAdmin())?``:''}
`); }; window.editEquipoProd=window.editarEquipoSinFacultad; window.editEquipo=window.editarEquipoSinFacultad; window.guardarEdicionEquipoSinFacultad=function(id){ let e=(db.equipos||[]).find(x=>x.id==id); if(!e) return; if(!((typeof isAdmin==='function'&&isAdmin())||(typeof esDueñoEquipo==='function'&&esDueñoEquipo(e)))) return; Object.assign(e,{nombre:men.value,tipoEquipo:metipo.value,procedencia:meproc.value,institucion:meproc.value,facultad:'',deporte:med.value,rama:mer.value,entrenador:mee.value,correo:mec.value,telefono:metel.value,estatus:(document.getElementById('mest')?mest.value:(e.estatus||'Activo'))}); closeM(); save('Editó equipo'); }; window.guardarEquipoProd=window.guardarEdicionEquipoSinFacultad; })(); /* === FIN PATCH DEFINITIVO: EQUIPOS SIN FACULTAD OBLIGATORIA === */ /* === PATCH REGISTRO ENTRENADOR FUNCIONAL + APROBACIÓN ADMIN === */ (function(){ function esc(v){ return (typeof txt==='function') ? txt(v||'') : String(v||'').replace(/[&<>\"]/g,m=>({'&':'&','<':'<','>':'>','\"':'"'}[m])); } const loginOriginal = window.login || login; window.login = function(){ const usr = (document.getElementById('u')?.value || '').trim(); const pass = (document.getElementById('p')?.value || '').trim(); let found = (db.usuarios||[]).find(x=>String(x.usuario||'')===usr && String(x.password||'')===pass); if(!found) return alert('Usuario o contraseña incorrectos'); const est = String(found.estatus || 'Activo').toLowerCase(); if(est.includes('pendiente')) return alert('Tu registro está pendiente de aprobación por el administrador.'); if(est.includes('rechazado') || est.includes('bloqueado') || est.includes('inactivo')) return alert('Tu usuario no está activo. Contacta al administrador.'); user = found; localStorage.setItem('uachUser', found.usuario); if(document.getElementById('ses')) ses.innerText=(found.rol||'usuario')+' · '+(found.nombre||found.usuario); if(document.getElementById('u')) u.value=''; if(document.getElementById('p')) p.value=''; render(); }; window.registroEntrenador = function(){ const html = `

Registro de entrenador

Crea tu perfil de entrenador. El administrador deberá aprobarlo antes de que puedas entrar al sistema.

`; if(typeof openM==='function') openM(html); else alert('No se pudo abrir el formulario de registro.'); }; window.guardarRegistroEntrenador = function(){ const nombre=(document.getElementById('rn')?.value||'').trim(); const correo=(document.getElementById('rc')?.value||'').trim(); const telefono=(document.getElementById('rt')?.value||'').trim(); const institucion=(document.getElementById('ri')?.value||'').trim(); const usuario=(document.getElementById('ru')?.value||'').trim(); const p1=document.getElementById('rp1')?.value||''; const p2=document.getElementById('rp2')?.value||''; if(!nombre || !correo || !usuario || !p1) return alert('Completa nombre, correo, usuario y contraseña.'); if(p1!==p2) return alert('Las contraseñas no coinciden.'); if((db.usuarios||[]).some(x=>String(x.usuario||'').toLowerCase()===usuario.toLowerCase())) return alert('Ese usuario ya existe. Elige otro.'); db.usuarios = db.usuarios || []; db.usuarios.push({ id:uid(), nombre, usuario, password:p1, rol:'entrenador', correo, telefono, institucion, facultad:'', estatus:'Pendiente aprobación', fechaRegistro:new Date().toLocaleString() }); closeM(); save('Registro de entrenador pendiente de aprobación'); alert('Registro enviado. Espera la aprobación del administrador.'); }; const usuariosOriginal = window.usuarios || (typeof usuarios==='function' ? usuarios : null); if(usuariosOriginal){ window.usuarios = function(){ if(!(typeof isAdmin==='function' && isAdmin())) return adminMsg(); const pendientes=(db.usuarios||[]).filter(x=>String(x.estatus||'Activo').toLowerCase().includes('pendiente')); const bloque=`

Solicitudes de entrenadores

${pendientes.length?`${pendientes.map(x=>``).join('')}
NombreUsuarioContactoInstitución / procedenciaEstatusAcción
${esc(x.nombre)}${esc(x.usuario)}${esc(x.correo||'')}
${esc(x.telefono||'')}
${esc(x.institucion||'')}${esc(x.estatus||'Pendiente')}
`:'

No hay solicitudes pendientes.

'}
`; return bloque + usuariosOriginal(); }; } window.aprobarUsuarioEntrenador=function(id){let x=(db.usuarios||[]).find(u=>u.id==id); if(x){x.estatus='Activo'; save('Aprobó entrenador');}}; window.rechazarUsuarioEntrenador=function(id){let x=(db.usuarios||[]).find(u=>u.id==id); if(x){x.estatus='Rechazado'; save('Rechazó entrenador');}}; })(); /* === FIN PATCH REGISTRO ENTRENADOR FUNCIONAL === */ const _render=window.render; window.render=function(){try{ensureDeportesSeguro();}catch(e){}addToolsPage(); renderNav(); let map={dashboard,roles,resultados,estadisticas,tabla,reportes,protestas,sanciones,equipos,jugadores,credenciales,cotejo,cedulas_arbitro,deportes,facultades,usuarios,bitacora,avisos,herramientas}; if(!map[page]||!canSeePage(page)) page='dashboard'; app.innerHTML=map[page](); if(window.applyRoleChrome) applyRoleChrome();}; })(); /* === FIN PATCH FINAL PRODUCCIÓN === */ /* === PATCH JUGADOR CLASIFICADO EN FORMULARIO Y CREDENCIAL === */ (function(){ function h(v){ if(typeof escHtml==='function') return escHtml(v==null?'':String(v)); return String(v==null?'':v).replace(/[&<>\"]/g,function(m){return {'&':'&','<':'<','>':'>','\"':'"'}[m]||m;}); } function cls(j){ let v = (j && (j.jugadorClasificado || j.clasificado || j.esClasificado || j.clasificacion)) || 'No'; v = String(v).trim().toLowerCase(); return (v==='si'||v==='sí'||v==='s'||v==='true'||v==='1'||v==='clasificado') ? 'Sí' : 'No'; } function clasBadge(j){ return cls(j)==='Sí' ? 'CLASIFICADO' : 'NO CLASIFICADO'; } function equipoTxt(e){ if(!e) return 'Sin equipo'; let nombre = (typeof nombreEquipoVista==='function') ? nombreEquipoVista(e) : (e.nombre || e.procedencia || e.institucion || e.facultad || 'Equipo'); return [nombre, (typeof dep==='function'?dep(e.deporte):e.deporte), e.rama].filter(Boolean).join(' · '); } function equipoDeJugador(j){ if(typeof jugadorEquipo==='function') return jugadorEquipo(j) || {}; return (db.equipos||[]).find(e=>e.id===j.equipo) || (db.equipos||[]).find(e=>e.facultad===j.facultad && e.deporte===j.deporte) || {}; } window.jugadores=function(){ let q=String(window.jugSearch||'').toLowerCase(); let eqs=(typeof visibleEquipos==='function'?visibleEquipos(db.equipos||[]):db.equipos||[]); let base=(typeof visibleJugadores==='function'?visibleJugadores(db.jugadores||[]):db.jugadores||[]); let lista=base.filter(j=>{let e=equipoDeJugador(j)||{}; return [j.nombre,j.matricula,j.numero,equipoTxt(e),e.procedencia,e.institucion,e.tipoEquipo,(typeof dep==='function'?dep(j.deporte):j.deporte),j.rama,j.carrera,j.semestre,j.telefono,j.correo,j.estatus,j.observaciones,cls(j)].join(' ').toLowerCase().includes(q)}); let puede=(typeof isAdmin==='function'&&isAdmin())||(typeof isCoach==='function'&&isCoach()); let form=puede?`

Agregar jugador

El jugador se liga al equipo seleccionado. El entrenador solo puede cargar jugadores de sus equipos.

${!eqs.length?'

Primero registra un equipo.

':`
`}
`:''; return `${form}

Jugadores

${lista.length} visibles
${lista.map(j=>{let e=equipoDeJugador(j)||{};return ``}).join('')}
FotoNombreMatrículaNo.EquipoDeporteClasificadoContactoEstatusAcción
${j.foto?``:`
FOTO
PENDIENTE
`}
${h(j.nombre||'')}
${h(j.observaciones||'')}
${h(j.matricula||'')}${h(j.numero||'')}${h(equipoTxt(e))}${h(typeof dep==='function'?dep(j.deporte):j.deporte)}${clasBadge(j)}${h(j.telefono||'')}
${h(j.correo||'')}
${h(j.estatus||'Pendiente')}${((typeof isAdmin==='function'&&isAdmin())||(typeof esJugadorPropio==='function'&&esJugadorPropio(j)))?`${(typeof isAdmin==='function'&&isAdmin())?``:''}`:''}
`; }; window.addJugadorProd=async function(){ let e=(db.equipos||[]).find(x=>x.id==document.getElementById('jeq')?.value); if(!e||!((typeof isAdmin==='function'&&isAdmin())||(typeof esDueñoEquipo==='function'&&esDueñoEquipo(e)))) return alert('Selecciona un equipo válido.'); let foto=''; let jf=document.getElementById('jfoto'); if(jf&&jf.files&&jf.files[0]) foto=await subirArchivo(jf.files[0]); let clas=(document.getElementById('jclas')?.value)||'No'; db.jugadores.push({id:uid(),equipo:e.id,nombre:jn.value,matricula:jm.value,numero:jnum.value,facultad:e.facultad||'',deporte:e.deporte,rama:e.rama,carrera:jc.value,semestre:js.value,telefono:jt.value,correo:jco.value,estatus:(typeof isCoach==='function'&&isCoach())?'Pendiente':(jest.value||'Pendiente'),observaciones:jobs.value||'',foto,creadoPor:(user&&user.usuario)||'',jugadorClasificado:clas,clasificado:clas}); save('Registró jugador'); }; window.editJugador=function(id){ let j=(db.jugadores||[]).find(x=>x.id==id); if(!j||!((typeof isAdmin==='function'&&isAdmin())||(typeof esJugadorPropio==='function'&&esJugadorPropio(j)))) return alert('No tienes permiso para editar este jugador.'); let eqs=(typeof visibleEquipos==='function'?visibleEquipos(db.equipos||[]):db.equipos||[]); let clas=cls(j); openM(`

Editar jugador

${j.foto?``:'Sin foto'} ${clasBadge(j)}

`); }; window.saveJugador=async function(id){ let j=(db.jugadores||[]).find(x=>x.id==id); if(!j||!((typeof isAdmin==='function'&&isAdmin())||(typeof esJugadorPropio==='function'&&esJugadorPropio(j)))) return alert('No tienes permiso para editar este jugador.'); let e=(db.equipos||[]).find(x=>x.id==(document.getElementById('mjeq')?.value))||{}; j.equipo=e.id||j.equipo; j.nombre=mjn.value; j.matricula=mjm.value; j.numero=mjnum.value; j.facultad=e.facultad||j.facultad||''; j.deporte=e.deporte||j.deporte; j.rama=e.rama||j.rama; j.carrera=mjc.value; j.semestre=mjs.value; j.telefono=mjt.value; j.correo=mjco.value; j.observaciones=mjobs.value; j.jugadorClasificado=mjclas.value; j.clasificado=mjclas.value; if(typeof isAdmin==='function'&&isAdmin()) j.estatus=mjest.value; else j.estatus='Pendiente'; let f=document.getElementById('mjfoto'); if(f&&f.files&&f.files[0]) j.foto=await subirArchivo(f.files[0]); closeM(); save('Editó jugador'); }; window.credCard=function(j){ let e=equipoDeJugador(j)||{}; let est=j.estatus||'Pendiente'; return `
${h((db.config&&db.config.torneo)||'TORNEO UACH')}Credencial de jugador
${typeof qrImg==='function'?qrImg(j,58):''}
${j.foto?``:`
FOTO
PENDIENTE
`}

${h(j.nombre||'SIN NOMBRE')}

Matrícula/ID: ${h(j.matricula||j.id||'')}

Equipo: ${h(equipoTxt(e))}

Deporte: ${h(typeof dep==='function'?dep(j.deporte):j.deporte)}

Rama/Categoría: ${h(j.rama||e.rama||'')}

Número: ${h(j.numero||'')}

Regla: ${clasBadge(j)}

Folio: ${h(typeof verifyCode==='function'?verifyCode(j):(j.id||''))}${h(est)}
`; }; window.cotejoCard=function(j){ let ok=String(j.estatus||'').toLowerCase()==='aprobado'||String(j.estatus||'').toLowerCase()==='activo'; let e=equipoDeJugador(j)||{}; return `
${j.foto?``:`
FOTO
PENDIENTE
`}

${h(j.nombre||'SIN NOMBRE')}

Matrícula: ${h(j.matricula||'')}

Equipo: ${h(equipoTxt(e))}

Deporte: ${h(typeof dep==='function'?dep(j.deporte):j.deporte)}

Número: ${h(j.numero||'')}

Jugador clasificado: ${clasBadge(j)}

Folio: ${h(typeof verifyCode==='function'?verifyCode(j):j.id)}

Estatus: ${h(j.estatus||'Pendiente')}

${ok?'✅ JUGADOR ELEGIBLE':'⚠️ REVISAR ANTES DE PARTICIPAR'}

${ok?'Validado por comité.':'Pendiente, rechazado o con información incompleta.'}

Observaciones: ${h(j.observaciones||'')}

`; }; window.printCredencialesEquipo=function(){ let e=(db.equipos||[]).find(x=>x.id==(window.credEquipo||'')); if(!e) return alert('Selecciona un equipo.'); if(!((typeof isAdmin==='function'&&isAdmin())||(typeof esDueñoEquipo==='function'&&esDueñoEquipo(e)))) return alert('No tienes permiso para ese equipo.'); let jugadores=(db.jugadores||[]).filter(j=>j.equipo===e.id || (!j.equipo&&j.facultad===e.facultad&&j.deporte===e.deporte)); let html=`

Credenciales por equipo

${h(equipoTxt(e))}

${jugadores.map(j=>credCard(j)).join('')}
`; let w=window.open('','_blank'); w.document.write(`credenciales_equipo ${html} `); w.document.close(); }; try{let st=document.createElement('style');st.innerHTML='.clasificado{background:#E7D7F0;color:#4E1D6B;font-weight:800}.noClasificado{background:#f3f4f6;color:#374151;font-weight:800}.qrBox img{display:block;background:#fff;}';document.head.appendChild(st);}catch(e){} })(); /* === FIN PATCH JUGADOR CLASIFICADO === */ /* === CORRECCIÓN REAL: Roles usan EQUIPOS registrados + pendientes arriba === */ (function(){ function H(v){ if(typeof escHtml==='function') return escHtml(v==null?'':String(v)); if(typeof esc==='function') return esc(v==null?'':String(v)); return String(v==null?'':v).replace(/[&<>\"]/g,function(m){return {'&':'&','<':'<','>':'>','\"':'"'}[m]||m;}); } function N(v){return String(v||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]/g,'');} function depName(id){try{return (db.deportes||[]).find(d=>String(d.id)===String(id))?.nombre || id || '';}catch(e){return id||'';}} function eqName(e){ if(!e) return ''; let base=e.nombre||e.procedencia||e.institucion||''; if(!base && e.facultad && typeof fac==='function') base=fac(e.facultad); if(!base) base='Equipo'; return [base, depName(e.deporte), e.rama||e.categoria||e.category].filter(Boolean).join(' · '); } window.nombreEquipoRol=function(id){ let e=(db.equipos||[]).find(x=>String(x.id)===String(id)); if(e) return eqName(e); if(typeof fac==='function') return fac(id); return id||''; }; function deporteOK(eVal, dVal){ if(!dVal) return true; if(String(eVal)===String(dVal)) return true; let eDep=(db.deportes||[]).find(d=>String(d.id)===String(eVal)); let dDep=(db.deportes||[]).find(d=>String(d.id)===String(dVal)); let a=[eVal,eDep&&eDep.nombre].map(N).filter(Boolean); let b=[dVal,dDep&&dDep.nombre].map(N).filter(Boolean); return a.some(x=>b.includes(x)); } function ramaOK(e,r){ if(!r) return true; let er=e.rama||e.categoria||e.category||''; return N(er)===N(r); } function equiposRol(deporte,rama){ let activos=(db.equipos||[]).filter(e=>N(e.estatus||'Activo')!=='inactivo'); let lista=activos.filter(e=>deporteOK(e.deporte,deporte) && ramaOK(e,rama)); if(!lista.length) lista=activos.filter(e=>deporteOK(e.deporte,deporte)); return lista.sort((a,b)=>eqName(a).localeCompare(eqName(b))); } function opts(deporte,rama){ let lista=equiposRol(deporte,rama); if(!lista.length) return ''; return lista.map(e=>``).join(''); } window.actualizarEquiposRol=function(){ let d=document.getElementById('rd')?.value||''; let r=document.getElementById('rr')?.value||''; let html=opts(d,r); let a=document.getElementById('ra'), b=document.getElementById('rb'); if(a) a.innerHTML=html; if(b) b.innerHTML=html; }; window.roles=function(){ let dSel=window.rolDepSel || (db.deportes&&(db.deportes||[])[0]&&(db.deportes||[])[0].id) || ''; let rSel=window.rolRamaSel || 'Varonil'; let visibles=(typeof aplicaFiltrosPartidos==='function'?aplicaFiltrosPartidos(db.partidos||[],'rol'):(typeof partidosFiltrados==='function'?partidosFiltrados('rol'):(db.partidos||[]))); setTimeout(window.actualizarEquiposRol,0); return `${isAdmin()?`

Nuevo rol de juego

El rol ahora se arma con los equipos registrados. Primero elige deporte y rama; luego se cargan Equipo A y Equipo B.

`:adminMsg()}

Roles de juego

${typeof panelFiltros==='function'?panelFiltros('rol',{fase:true}):''}${tablaPartidos(visibles,false,true)}
`; }; window.addPartido=function(){ let f=rf.value||'', d=rd.value||'', r=rr.value||'', a=ra.value||'', b=rb.value||''; if(!f||!a||!b) return alert('Completa fecha y selecciona Equipo A y Equipo B.'); if(a===b) return alert('Equipo A y Equipo B no pueden ser el mismo.'); (db.partidos=db.partidos||[]).push({id:uid(),fecha:f,deporte:d,rama:r,a:a,b:b,lugar:rl.value,jornada:rj.value,fase:rfase.value,grupo:rg.value,arbitro:rar.value||'Sin asignar',observaciones:robs.value,estado:'Programado'}); save('Creó rol de juego'); }; window.tablaPartidos=function(list,usarFiltros=true,agrupar=false){ list=list||[]; if(usarFiltros){ if(typeof aplicaFiltrosPartidos==='function') list=aplicaFiltrosPartidos(list,'rol'); else if(typeof partidosFiltrados==='function') list=partidosFiltrados('rol',list); } if(!list.length) return '

Sin roles capturados con los filtros seleccionados.

'; const rows=a=>`${a.slice().sort((x,y)=>(x.fecha||'').localeCompare(y.fecha||'')).map(p=>``).join('')}
FechaDeporteRamaFase/JornadaJuegoLugarÁrbitroEstado
${typeof fmt==='function'?fmt(p.fecha):H(p.fecha)}${H(typeof dep==='function'?dep(p.deporte):depName(p.deporte))}${H(p.rama||'Sin rama')}${H(p.fase||'')}
${H(p.jornada||'')} ${p.grupo?'· Grupo '+H(p.grupo):''}
${H(nombreEquipoRol(p.a))} vs ${H(nombreEquipoRol(p.b))}${H(p.lugar||'')}${H(p.arbitro||'')}${H(p.estado||'')}${isAdmin()?``:''}
`; if(!agrupar) return rows(list); let groups={}; list.forEach(p=>{let k=(typeof keyComp==='function'?keyComp(p):[depName(p.deporte),p.rama||'Sin rama',p.fase||''].join(' · ')); (groups[k]=groups[k]||[]).push(p);}); return Object.entries(groups).sort().map(([k,a])=>`

${H(k)} ${a.length} roles

${rows(a)}`).join(''); }; function estOrden(j){let e=N(j.estatus||'Pendiente'); if(e==='pendiente')return 0; if(e==='rechazado')return 1; if(e==='aprobado'||e==='activo')return 2; return 3;} window.verPendientesJugadores=function(){window.jugSearch='Pendiente'; render();}; window.verTodosJugadores=function(){window.jugSearch=''; render();}; if(typeof window.jugadores==='function'){ const oldJug=window.jugadores; window.jugadores=function(){ let html=oldJug(); try{ let base=db.jugadores||[]; db.jugadores=base.slice().sort((a,b)=>estOrden(a)-estOrden(b)||String(a.nombre||'').localeCompare(String(b.nombre||''))); let pend=base.filter(j=>N(j.estatus||'Pendiente')==='pendiente').length; html=oldJug(); db.jugadores=base; html=html.replace('

Jugadores

',`

Jugadores

`); }catch(e){} return html; }; } })(); /* === FIN CORRECCIÓN REAL === */ /* === AJUSTE FINAL: nombres limpios de equipos + cédula por equipo === */ (function(){ function H(v){ if(typeof escHtml==='function') return escHtml(v==null?'':String(v)); return String(v==null?'':v).replace(/[&<>\"]/g,function(m){return {'&':'&','<':'<','>':'>','\"':'"'}[m]||m;}); } function norm(v){return String(v||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').trim().toLowerCase();} function equipoById(id){return (db.equipos||[]).find(e=>String(e.id)===String(id))||null;} function equipoNombreCorto(id){ let e=equipoById(id); if(e){return (e.nombre||e.procedencia||e.institucion||(e.facultad&&typeof fac==='function'?fac(e.facultad):'')||'Equipo').trim();} return (typeof fac==='function'?fac(id):id)||''; } function equipoNombreCompleto(id){ let e=equipoById(id); if(e){return [equipoNombreCorto(id), (typeof dep==='function'?dep(e.deporte):e.deporte), e.rama].filter(Boolean).join(' · ');} return equipoNombreCorto(id); } window.nombreEquipoRol=equipoNombreCorto; window.nombreEquipoCompleto=equipoNombreCompleto; window.partidoNombre=function(p){return `${equipoNombreCorto(p.a)} vs ${equipoNombreCorto(p.b)}`;}; window.jugadorPerteneceEquipo=function(j,teamId){ if(!j) return false; let e=equipoById(teamId); if(String(j.equipo||'')===String(teamId)) return true; if(e && j.equipo && String(j.equipo)===String(e.id)) return true; if(e && e.facultad && j.facultad && String(j.facultad)===String(e.facultad) && (!e.deporte||String(j.deporte)===String(e.deporte))) return true; if(!e && j.facultad && String(j.facultad)===String(teamId)) return true; return false; }; window.jugadoresDelPartido=function(p){ let deporte=String(p.deporte||''); let rama=norm(p.rama||''); return (db.jugadores||[]).filter(j=>{ let depOk=!deporte || String(j.deporte||'')===deporte; let ramaOk=!rama || !j.rama || norm(j.rama)===rama; let teamOk=jugadorPerteneceEquipo(j,p.a)||jugadorPerteneceEquipo(j,p.b); return depOk && ramaOk && teamOk; }).sort((a,b)=>{ let ea=jugadorPerteneceEquipo(a,p.a)?0:1, eb=jugadorPerteneceEquipo(b,p.a)?0:1; return ea-eb || (+a.numero||999)-(+b.numero||999) || String(a.nombre||'').localeCompare(String(b.nombre||'')); }); }; window.tablaPartidos=function(list,usarFiltros=true,agrupar=false){ list=list||[]; if(usarFiltros){ if(typeof aplicaFiltrosPartidos==='function') list=aplicaFiltrosPartidos(list,'rol'); else if(typeof partidosFiltrados==='function') list=partidosFiltrados('rol',list); } if(typeof canSeePartido==='function') list=list.filter(canSeePartido); if(!list.length) return '

Sin roles capturados con los filtros seleccionados.

'; const rows=a=>`${a.slice().sort((x,y)=>(x.fecha||'').localeCompare(y.fecha||'')).map(p=>``).join('')}
FechaDeporteRamaFase/JornadaJuegoLugarÁrbitroEstadoAcciones
${typeof fmt==='function'?fmt(p.fecha):H(p.fecha)}${H(typeof dep==='function'?dep(p.deporte):p.deporte)}${H(p.rama||'Sin rama')}${H(p.fase||'')}
${H(p.jornada||'')} ${p.grupo?'· Grupo '+H(p.grupo):''}
${H(equipoNombreCorto(p.a))} vs ${H(equipoNombreCorto(p.b))}${H(p.lugar||'')}${H(typeof arbLabel==='function'?arbLabel(p.arbitro):p.arbitro||'')}${H(p.estado||'')}
${user?``:''}${isAdmin()?``:''}
`; if(!agrupar) return rows(list); let groups={}; list.forEach(p=>{let k=[typeof dep==='function'?dep(p.deporte):p.deporte,p.rama||'Sin rama'].filter(Boolean).join(' · '); (groups[k]=groups[k]||[]).push(p);}); return Object.entries(groups).sort().map(([k,a])=>`

${H(k)} ${a.length} roles

${rows(a)}`).join(''); }; window.partidosCedulaDisponibles=function(){ return (db.partidos||[]).filter(p=>typeof canSeePartido==='function'?canSeePartido(p):true).sort((a,b)=>(a.fecha||'').localeCompare(b.fecha||'')); }; window.cedulas_arbitro=function(){ let list=partidosCedulaDisponibles(); let sel=window.cedulaPartido || (list[0]&&list[0].id) || ''; let p=list.find(x=>x.id===sel) || list[0]; window.cedulaPartido=p?p.id:''; return `

Cédula digital arbitral

Selecciona el partido, abre la cédula, valida jugadores, captura anotaciones y finaliza el resultado.

${!list.length?'

No hay partidos visibles para este usuario.

':``}
${p?cedulaDigitalDetalle(p):''}`; }; window.cedulaDigitalDetalle=function(p){ let c=cedulaForPartido(p,false); let abierta=!!c && c.estado!=='Finalizada'; return `
${H(typeof dep==='function'?dep(p.deporte):p.deporte)} · ${H(p.rama||'')}

${H(equipoNombreCorto(p.a))} vs ${H(equipoNombreCorto(p.b))}

Folio: ${H(typeof cedulaFolio==='function'?cedulaFolio(p):p.id)} · Fecha: ${typeof fmt==='function'?fmt(p.fecha):H(p.fecha)} · Lugar: ${H(p.lugar||'')} · Árbitro: ${H(typeof arbLabel==='function'?arbLabel(p.arbitro):p.arbitro||'')} · Estado: ${cedulaEstadoDigital(p)}

${!c?``:``}
${c?`

Validación manual de jugadores

${abierta?manualPanelJugadores(p,c):'

La cédula está cerrada. La validación queda bloqueada.

'}${tablaJugadoresCedula(p,c)}

Anotaciones del partido

${abierta?eventoPanel(p,c):'

Las anotaciones quedaron cerradas.

'}${tablaEventosCedula(c)}

Cierre de cédula

${cierrePanel(p,c)}
`:''}`; }; window.manualPanelJugadores=function(p,c){ let q=String(window.cedulaManualQ||'').toLowerCase(); let jugadores=jugadoresDelPartido(p).filter(j=>!q||[j.nombre,j.matricula,j.numero,equipoNombreCorto(j.equipo||j.facultad),j.estatus].join(' ').toLowerCase().includes(q)); let equipoA=jugadores.filter(j=>jugadorPerteneceEquipo(j,p.a)); let equipoB=jugadores.filter(j=>jugadorPerteneceEquipo(j,p.b)); return `
Captura: toca Validar cuando el jugador presente su credencial. La hora queda registrada.

${H(equipoNombreCorto(p.a))}

${equipoA.map(j=>playerManualCard(p,c,j)).join('')||'

Sin jugadores registrados para este equipo.

'}

${H(equipoNombreCorto(p.b))}

${equipoB.map(j=>playerManualCard(p,c,j)).join('')||'

Sin jugadores registrados para este equipo.

'}
`; }; window.eventoPanel=function(p,c){ let jugadores=(c.jugadores||[]).map(x=>(db.jugadores||[]).find(j=>j.id===x.jugador)).filter(Boolean); return `
`; }; window.tablaJugadoresCedula=function(p,c){ if(!c.jugadores.length) return '

Aún no hay jugadores validados.

'; return `${c.jugadores.map(x=>{let j=(db.jugadores||[]).find(y=>y.id===x.jugador)||{};return ``}).join('')}
HoraJugadorEquipoFolioAcción
${H(x.hora||'')}${H(j.nombre||'')}
${H(j.matricula||'')}
${H(equipoNombreCorto(x.equipo||j.equipo||j.facultad))}${H(x.folio||'')}${c.estado!=='Finalizada'?``:''}
`; }; window.validarJugadorManualCedula=function(pid,jid){ let p=(db.partidos||[]).find(x=>x.id===pid), c=p&&cedulaForPartido(p,true), j=(db.jugadores||[]).find(x=>x.id===jid); if(!p||!c||c.estado==='Finalizada') return alert('La cédula no está disponible para captura.'); if(!j) return alert('No se encontró el jugador.'); if(c.jugadores.some(x=>x.jugador===j.id)) return alert('Este jugador ya fue validado en esta cédula.'); let team=jugadorPerteneceEquipo(j,p.a)?p.a:(jugadorPerteneceEquipo(j,p.b)?p.b:(j.equipo||j.facultad||'')); c.jugadores.push({id:uid(),jugador:j.id,equipo:team,folio:(typeof verifyCode==='function'?verifyCode(j):(j.matricula||j.id)),hora:new Date().toLocaleString(),usuario:(user&&user.usuario)||'',metodo:'Manual'}); window.cedulaPartido=p.id; render(); if(typeof persist==='function') persist('Validó jugador en cédula'); else save('Validó jugador en cédula'); }; window.cierrePanel=function(p,c){let disabled=c.estado==='Finalizada'; return `
${disabled?`

Cédula cerrada: ${H(c.fechaCierre||'')} por ${H(c.cerradaPor||'')}

`:``}`;}; })(); /* === FIN AJUSTE FINAL === */ /* === CORRECCIÓN INTEGRAL 21 PUNTOS - ChatGPT === */ (function(){ function H(v){return String(v==null?'':v).replace(/[&<>\"]/g,m=>({'&':'&','<':'<','>':'>','\"':'"'}[m]||m));} function N(v){return String(v||'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').trim().toLowerCase();} function byId(arr,id){return (arr||[]).find(x=>String(x.id)===String(id))||null;} window.isAdmin=function(){return !!user && ['admin','administrador'].includes(N(user.rol));}; window.isCoach=function(){return !!user && ['entrenador','coach'].includes(N(user.rol));}; window.isArbitro=function(){return !!user && ['arbitro','árbitro','juez'].includes(N(user.rol));}; function facLabel(id){let f=byId(db.facultades,id);return f?(f.siglas||f.nombre):String(id||'');} function team(id){return byId(db.equipos,id);} function teamName(id){let e=team(id);if(!e)return facLabel(id);let base=(e.nombre||e.procedencia||facLabel(e.facultad)||'Equipo');let g=String(e.grupo||'').trim();return g?(base+' · '+g):base;} function teamFull(id){let e=team(id);return e?[teamName(id),dep(e.deporte),e.rama].filter(Boolean).join(' · '):teamName(id);} window.nombreEquipoRol=teamName; window.nombreEquipoCompleto=teamFull; window.partidoNombre=p=>`${teamName(p.a)} vs ${teamName(p.b)}`; function partidoPermitido(p){ if(!user) return true; if(isAdmin()) return true; if(isArbitro()) return !p.arbitro || [p.arbitro,p.arbitroUsuario,p.arbitroId].filter(Boolean).map(N).some(x=>[user.usuario,user.nombre,user.id].map(N).includes(x)); if(isCoach()){ let mine=(db.equipos||[]).filter(e=>N(e.entrenador)===N(user.nombre)||N(e.entrenadorUsuario)===N(user.usuario)||N(e.creadoPor)===N(user.usuario)||(!e.entrenador&&user.facultad&&String(e.facultad)===String(user.facultad))).map(e=>String(e.id)); return mine.includes(String(p.a))||mine.includes(String(p.b))||String(p.a)===String(user.facultad)||String(p.b)===String(user.facultad); } return true; } window.canSeePage=function(k){ if(!user) return ['roles','resultados','estadisticas'].includes(k); if(isAdmin()) return true; if(isArbitro()) return ['roles','credenciales','cedulas_arbitro'].includes(k); if(isCoach()) return ['dashboard','roles','resultados','estadisticas','tabla','reportes','protestas','equipos','jugadores','credenciales','cedulas_arbitro','avisos'].includes(k); return ['roles','resultados','estadisticas'].includes(k); }; window.renderNav=function(){ if(!canSeePage(page)) page=canSeePage('dashboard')?'dashboard':'roles'; nav.innerHTML=pages.filter(p=>canSeePage(p[0])).map(p=>``).join(''); }; function equiposVisibles(){let a=(db.equipos||[]).filter(e=>String(e.estatus||'Activo')!=='Inactivo'); if(isCoach()) a=a.filter(e=>N(e.entrenador)===N(user.nombre)||N(e.entrenadorUsuario)===N(user.usuario)||N(e.creadoPor)===N(user.usuario)||(!e.entrenador&&user.facultad&&String(e.facultad)===String(user.facultad))); return a;} function jugadorEquipo(j,tid){let e=team(tid); if(String(j.equipo||'')===String(tid)) return true; if(e && j.facultad && String(j.facultad)===String(e.facultad) && (!e.deporte||String(j.deporte)===String(e.deporte)) && (!e.rama||!j.rama||N(e.rama)===N(j.rama))) return true; if(!e && String(j.facultad)===String(tid)) return true; return false;} window.jugadorPerteneceEquipo=jugadorEquipo; window.jugadoresDelPartido=function(p){return (db.jugadores||[]).filter(j=>(!p.deporte||String(j.deporte)===String(p.deporte))&&(jugadorEquipo(j,p.a)||jugadorEquipo(j,p.b))).sort((a,b)=>(jugadorEquipo(a,p.a)?0:1)-(jugadorEquipo(b,p.a)?0:1)||(+a.numero||999)-(+b.numero||999));}; function facOptions(sel=''){return (db.facultades||[]).map(f=>``).join('');} function equipoOptions(sel='',depFilter='',ramaFilter=''){return equiposVisibles().filter(e=>(!depFilter||String(e.deporte)===String(depFilter))&&(!ramaFilter||N(e.rama)===N(ramaFilter))).map(e=>``).join('');} window.roles=function(){let visibles=(db.partidos||[]).filter(partidoPermitido); return `${isAdmin()?`

Nuevo rol de juego

Primero crea equipos. Aquí se selecciona Equipo A y Equipo B.

`:''}

Roles de juego

${panelFiltros('rol',{fase:true})}${tablaPartidos(visibles,true,true)}
`;}; window.addPartido=function(){ if(!rf.value||!ra.value||!rb.value) return alert('Completa fecha, Equipo A y Equipo B.'); if(ra.value===rb.value) return alert('Equipo A y Equipo B no pueden ser el mismo.'); let ea=team(ra.value), eb=team(rb.value); db.partidos.push({id:uid(),fecha:rf.value,deporte:rd.value,rama:rr.value,a:ra.value,b:rb.value,lugar:rl.value,jornada:rj.value,fase:rfase.value,grupo:rg.value,arbitro:rar.value,observaciones:robs.value,estado:'Programado'}); save('Creó rol de juego'); }; window.tablaPartidos=function(list,usarFiltros=true,agrupar=false){list=(list||[]).filter(partidoPermitido); if(usarFiltros&&typeof aplicaFiltrosPartidos==='function') list=aplicaFiltrosPartidos(list,'rol'); if(!list.length) return '

Sin roles capturados con los filtros seleccionados.

'; const rows=a=>`${a.sort((x,y)=>String(x.fecha||'').localeCompare(String(y.fecha||''))).map(p=>``).join('')}
FechaDeporteRamaFase/JornadaJuegoLugarÁrbitroEstadoAcciones
${fmt(p.fecha)}${H(dep(p.deporte))}${H(p.rama||'')}${H(p.fase||'')}
${H(p.jornada||'')} ${p.grupo?'· Grupo '+H(p.grupo):''}
${H(teamName(p.a))} vs ${H(teamName(p.b))}${H(p.lugar||'')}${H(p.arbitro||'Sin asignar')}${H(p.estado||'')}${isAdmin()?``:''}
`; if(!agrupar)return rows(list); let g={};list.forEach(p=>{let k=`${dep(p.deporte)} · ${p.rama||'Sin rama'}`;(g[k]=g[k]||[]).push(p)});return Object.entries(g).map(([k,a])=>`

${H(k)} ${a.length} roles

${rows(a)}`).join('');}; function puntosRes(r,p){let a=+r.ma||0,b=+r.mb||0,fut=N((depObj(p.deporte)||{}).regla)==='futbol'; if(a>b)return {a:fut?3:3,b:0}; if(b>a)return {a:0,b:fut?3:3}; return {a:1,b:1};} window.rankingFacultades=function(){let rows={}; function ensure(id){let k=String(id||''); if(!rows[k]) rows[k]={id:k,fac:teamName(k),nombre:teamFull(k),jj:0,jg:0,je:0,jp:0,pf:0,pc:0,dif:0,pts:0,deportes:new Set()}; return rows[k];} (db.resultados||[]).forEach(r=>{let p=byId(db.partidos,r.partido); if(!p)return; let A=ensure(p.a),B=ensure(p.b),pt=puntosRes(r,p),ma=+r.ma||0,mb=+r.mb||0; A.jj++;B.jj++;A.pf+=ma;A.pc+=mb;B.pf+=mb;B.pc+=ma;A.pts+=pt.a;B.pts+=pt.b;A.deportes.add(p.deporte);B.deportes.add(p.deporte); if(ma>mb){A.jg++;B.jp++}else if(mb>ma){B.jg++;A.jp++}else{A.je++;B.je++}}); return Object.values(rows).map(x=>{x.dif=x.pf-x.pc;x.deportes=x.deportes.size;return x}).sort((a,b)=>b.pts-a.pts||b.dif-a.dif||b.pf-a.pf||a.fac.localeCompare(b.fac));}; window.tablaDeporte=function(id){let rr=(db.resultados||[]).filter(r=>{let p=byId(db.partidos,r.partido);return p&&p.deporte==id}); if(!rr.length)return '

Sin resultados de este deporte.

'; let old=db.resultados; db.resultados=rr; let arr=rankingFacultades(); db.resultados=old; return `${arr.map((x,i)=>``).join('')}
LugarEquipoJJJGJEJPPFPCDIFPuntos
${i+1}${H(x.fac)}${x.jj}${x.jg}${x.je}${x.jp}${x.pf}${x.pc}${x.dif}${x.pts}
`;}; window.tabla=function(){let arr=rankingFacultades();return `

Ranking general por equipo

${!arr.length?'

Sin resultados capturados.

':`${arr.map((x,i)=>``).join('')}
LugarEquipoProcedenciaDeportesJJJGJEJPPFPCDIFPuntos
${i+1}${H(x.fac)}${H(x.nombre)}${x.deportes}${x.jj}${x.jg}${x.je}${x.jp}${x.pf}${x.pc}${x.dif}${x.pts}
`}

Tablas por deporte

${(db.deportes||[]).map(d=>`

${H(d.nombre)}

${tablaDeporte(d.id)}`).join('')}
`;}; window.tablaResultadosDeporte=function(id){let rr=(db.resultados||[]).filter(r=>{let p=byId(db.partidos,r.partido);return p&&p.deporte==id}); if(!rr.length)return '

Sin resultados.

'; return `${rr.map(r=>{let p=byId(db.partidos,r.partido)||{};let ma=+r.ma||0,mb=+r.mb||0;let ganador=ma>mb?teamName(p.a):mb>ma?teamName(p.b):'Empate';return ``}).join('')}
FechaPartidoMarcador claroGanadorObservaciones
${H(r.fecha||fmt(p.fecha))}${H(teamName(p.a))} vs ${H(teamName(p.b))}${H(teamName(p.a))} ${ma} - ${mb} ${H(teamName(p.b))}${H(ganador)}${H(r.obs||r.detalle||'')}
`;}; window.estadisticas=function(){let sel=$('estDep')?.value||''; let rr=(db.resultados||[]).filter(r=>{let p=byId(db.partidos,r.partido); return p&&(!sel||p.deporte==sel)}); let total=rr.reduce((a,r)=>a+(+r.ma||0)+(+r.mb||0),0); return `

Estadísticas filtrables

Partidos visibles
${rr.length}
Puntos/favor visibles
${total}
Promedio visible
${rr.length?(total/rr.length).toFixed(1):0}
Equipos visibles
${new Set(rr.flatMap(r=>{let p=byId(db.partidos,r.partido)||{};return [p.a,p.b]})).size}

Tabla estadística según filtros

${sel?tablaDeporte(sel):tabla()}

Resultados que alimentan esta estadística

${(sel?[{id:sel,nombre:dep(sel)}]:(db.deportes||[])).map(d=>`

${H(d.nombre)}

${tablaResultadosDeporte(d.id)}`).join('')}

Estadísticas individuales por jugador

${tablaStatsJugadores(sel)}
`;}; window.tablaStatsJugadores=function(sel=''){let rows={}; (db.cedulas||[]).forEach(c=>{let p=byId(db.partidos,c.partido); if(!p||sel&&p.deporte!==sel)return; (c.eventos||[]).forEach(e=>{if(!e.jugador)return; let j=byId(db.jugadores,e.jugador)||{}; let k=j.id||e.jugador; rows[k]=rows[k]||{jug:j.nombre||'Sin jugador',equipo:teamName(e.equipo||j.equipo||j.facultad),deporte:dep(p.deporte),total:0,det:{}}; let cant=+e.cantidad||1; rows[k].det[e.tipo]=(rows[k].det[e.tipo]||0)+cant; rows[k].total+=cant;});}); let stats=[...new Set(Object.values(rows).flatMap(x=>Object.keys(x.det)))]; if(!stats.length)return '

Sin anotaciones individuales capturadas.

'; return `${stats.map(s=>``).join('')}${Object.values(rows).map(x=>`${stats.map(s=>``).join('')}`).join('')}
JugadorEquipoDeporte${H(s)}Total
${H(x.jug)}${H(x.equipo)}${H(x.deporte)}${x.det[s]||0}${x.total}
`;}; window.reportes=function(){let sel=$('repDep')?.value||(db.deportes||[])[0]?.id||'';return `

Reportes oficiales

Reporte: ${H(dep(sel))}

${tablaDeporte(sel)}

Resultados del deporte

${tablaResultadosDeporte(sel)}

Roles del deporte

${tablaPartidos((db.partidos||[]).filter(p=>p.deporte==sel),false,true)}
`;}; window.resultados=function(){let rr=db.resultados||[];return `

Resultados

${(db.deportes||[]).map(d=>{let x=rr.filter(r=>{let p=byId(db.partidos,r.partido);return p&&p.deporte==d.id});return x.length?`

${H(d.nombre)} ${x.length} resultados

${tablaResultadosDeporte(d.id)}`:''}).join('')||'

Sin resultados capturados.

'}
`;}; window.equipos=function(){let q=N(window.eqSearch||'');let lista=equiposVisibles().filter(e=>!q||N([teamName(e.id),facLabel(e.facultad),dep(e.deporte),e.rama,e.grupo,e.entrenador,e.correo,e.telefono].join(' ')).includes(q));return `${(isAdmin()||isCoach())?`

Agregar equipo

El equipo debe ligarse a una Facultad o Centro de estudios creado por el administrador.

`:''}

Equipos

${lista.length} visibles
${lista.map(e=>``).join('')}
EquipoGrupoFacultad/CentroTipoDeporteRamaEntrenadorContactoEstatus
${H(teamName(e.id))}${H(e.grupo||'Sin grupo')}${H(facLabel(e.facultad))}${H(e.tipo||'')}${H(dep(e.deporte))}${H(e.rama||'')}${H(e.entrenador||'')}${H(e.correo||'')}
${H(e.telefono||'')}
${H(e.estatus||'Activo')}
`;}; window.addEquipoIntegral=function(){if(!ef.value||!en.value||!eg.value||!ed.value||!er.value||!ee.value)return alert('Completa Facultad/Centro, nombre del equipo, grupo, deporte, rama y nombre completo del entrenador.'); db.equipos.push({id:uid(),facultad:ef.value,nombre:en.value,grupo:eg.value,tipo:etipo.value,deporte:ed.value,rama:er.value,entrenador:ee.value,correo:ec.value,telefono:etel.value,estatus:'Activo',creadoPor:user?.usuario||''}); save('Registró equipo');}; window.jugadores=function(){let q=N(window.jugSearch||'');let lista=(db.jugadores||[]).filter(j=>!isCoach()||equiposVisibles().some(e=>jugadorEquipo(j,e.id))).filter(j=>!q||N([j.nombre,j.apellidoP,j.apellidoM,j.matricula,j.numero,teamName(j.equipo||j.facultad),facLabel(j.facultad),dep(j.deporte),j.carrera,j.estatus].join(' ')).includes(q)); let eq=equiposVisibles();return `${(isAdmin()||isCoach())?`

Agregar jugador

No se guarda si falta algún dato. Después de enviarlo, el entrenador no puede editar datos; solo el administrador.

`:''}

Jugadores

${lista.length} visibles
${lista.map(j=>``).join('')}
FotoNombreMatrículaNo.EquipoFacultad/CentroCarreraClasificadoContactoEstatusAcción
${j.foto?``:`
FOTO
PENDIENTE
`}
${H(j.nombre||'')}${H(j.matricula||'')}${H(j.numero||'')}${H(teamName(j.equipo||j.facultad))}${H(facLabel(j.facultad))}${H(j.carrera||'')}
${H(j.semestre||'')}
${H(j.clasificado||'No clasificado')}${H(j.telefono||'')}
${H(j.correo||'')}
${H(j.estatus||'Pendiente')}${isAdmin()?``:(isCoach()?``:'')}
`;}; window.addJugadorIntegral=async function(){let vals=[jn.value,jap.value,jam.value,jm.value,jnum.value,jf.value,jc.value,js.value,jt.value,jco.value,jeq.value]; if(vals.some(v=>!String(v||'').trim())) return alert('Completa todos los datos del jugador. No se permite guardar campos en blanco.'); if(!jfoto.files||!jfoto.files[0]) return alert('La foto del jugador es obligatoria.'); let e=team(jeq.value)||{}; if(db.jugadores.some(j=>j.matricula&&j.matricula==jm.value&&j.deporte==(e.deporte||''))) return alert('Esta matrícula ya está registrada en este deporte.'); let foto=await subirArchivo(jfoto.files[0]); db.jugadores.push({id:uid(),equipo:jeq.value,nombre:[jn.value,jap.value,jam.value].join(' '),nombres:jn.value,apellidoP:jap.value,apellidoM:jam.value,matricula:jm.value,numero:jnum.value,facultad:jf.value,deporte:e.deporte||'',rama:e.rama||'',carrera:jc.value,semestre:js.value,telefono:jt.value,correo:jco.value,clasificado:jclas.value,estatus:isCoach()?'Pendiente':jest.value,observaciones:jobs.value||'',foto,creadoPor:user?.usuario||''}); save('Registró jugador');}; window.credenciales=function(){let eqs=equiposVisibles();let sel=window.credEquipo||eqs[0]?.id||'';let js=(db.jugadores||[]).filter(j=>!sel||jugadorEquipo(j,sel));return `

Descargar credenciales por equipo

Selecciona solo el equipo. El sistema carga deporte y rama automáticamente.

Jugadores del equipo: ${H(teamName(sel))}

${js.length?`
${js.map(j=>credCard(j)).join('')}
`:'

Sin jugadores en este equipo.

'}
`;}; window.cotejo=function(){return `

Cotejo de jugadores

Validación previa al partido. Busca por folio, matrícula o nombre.

${typeof cotejoCard==='function'&&window.cotejoQ?cotejoCard(playerByVerify(window.cotejoQ)||{}):'

Selecciona o busca un jugador para cotejo.

'}`;}; window.deportes=function(){let templates={futbol_soccer:'Goles, Asistencias, Tarjetas amarillas, Tarjetas rojas, Faltas',basquetbol:'Puntos, Triples, Rebotes, Asistencias, Faltas',voleibol:'Puntos, Aces, Bloqueos, Errores, Sets ganados'};return `${isAdmin()?`

Agregar deporte

Las estadísticas que definas aparecerán en la cédula arbitral para capturar desempeño individual.

`:''}

Deportes

${(db.deportes||[]).map(d=>``).join('')}
DeporteReglaEstadísticas
${H(d.nombre)}${H(d.regla||'normal')}${H((d.estadisticas||[]).join(', '))}
`;}; window.addDeporteIntegral=function(){if(!dn.value)return alert('Escribe el nombre del deporte.'); let id=dn.value.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[^a-z0-9]+/g,'_'); db.deportes.push({id,nombre:dn.value,tipo:'equipo',regla:dreg.value,estadisticas:dstats.value.split(',').map(x=>x.trim()).filter(Boolean)}); save('Agregó deporte');}; window.dashboard=function(){ if(isCoach())return `

Panel del entrenador

Equipos asignados
${equiposVisibles().length}
Jugadores
${(db.jugadores||[]).filter(j=>equiposVisibles().some(e=>jugadorEquipo(j,e.id))).length}
Próximos partidos
${(db.partidos||[]).filter(partidoPermitido).filter(p=>p.estado!=='Finalizado').length}
Finalizados
${(db.partidos||[]).filter(partidoPermitido).filter(p=>p.estado==='Finalizado').length}

Próximos juegos

${tablaPartidos((db.partidos||[]).filter(partidoPermitido),false,false)}
`; return `
Deportes
${(db.deportes||[]).length}
Roles
${(db.partidos||[]).length}
Resultados
${(db.resultados||[]).length}
Equipos
${(db.equipos||[]).length}

Próximos roles

${tablaPartidos(db.partidos||[],false,false)}
`;}; window.partidosCedulaDisponibles=function(){let list=(db.partidos||[]).filter(partidoPermitido); if(isCoach()) list=list.filter(p=>p.estado==='Finalizado'); return list.sort((a,b)=>String(a.fecha||'').localeCompare(String(b.fecha||'')));}; window.cedulas_arbitro=function(){let list=partidosCedulaDisponibles();let p=byId(list,window.cedulaPartido)||list[0];window.cedulaPartido=p?p.id:''; if(isCoach()) return `

Cédulas finalizadas para consulta y protesta

Selecciona un partido finalizado para ver resultado oficial y presentar protesta.

${!list.length?'

No hay partidos finalizados.

':``}
${p?cedulaEntrenador(p):''}`; return `

Cédula digital arbitral

Selecciona tu juego asignado y captura la cédula.

${!list.length?'

No hay partidos asignados.

':``}
${p?cedulaDigitalDetalle(p):''}`;}; window.cedulaEntrenador=function(p){let r=(db.resultados||[]).find(x=>x.partido===p.id)||{};let ma=+r.ma||0,mb=+r.mb||0;let ganador=ma>mb?teamName(p.a):mb>ma?teamName(p.b):'Empate';return `
${H(dep(p.deporte))} · ${H(p.rama||'')}

${H(teamName(p.a))} ${ma} - ${mb} ${H(teamName(p.b))}

Ganador: ${H(ganador)} · Fecha: ${fmt(p.fecha)} · Lugar: ${H(p.lugar||'')}

Estadísticas individuales

${tablaStatsJugadores(p.deporte)}

Presentar protesta

`;}; window.eventoPanel=function(p,c){let equipoSel=window.evEquipoSel||p.a; let jugadores=(c.jugadores||[]).map(x=>byId(db.jugadores,x.jugador)).filter(j=>j&&jugadorEquipo(j,equipoSel)); let stats=(depObj(p.deporte).estadisticas||['Gol','Punto','Falta','Observación']);return `
`;}; window.agregarEventoCedula=function(id){let p=byId(db.partidos,id), c=p&&cedulaForPartido(p,false); if(!c||c.estado==='Finalizada')return alert('La cédula no está disponible.'); let cant=Math.max(1,+document.getElementById('evCant')?.value||1); c.eventos.push({id:uid(),tipo:evTipo.value,cantidad:cant,minuto:evMin.value,jugador:evJugador.value,equipo:evEquipo.value,obs:evObs.value,hora:new Date().toLocaleString(),usuario:user?.usuario||''}); save('Agregó anotación a cédula');}; window.tablaEventosCedula=function(c){if(!c.eventos||!c.eventos.length)return '

Sin anotaciones registradas.

';return c.eventos.slice().reverse().map(e=>{let j=byId(db.jugadores,e.jugador)||{};return `
${H(e.tipo)} x ${H(e.cantidad||1)} ${e.minuto?'· '+H(e.minuto):''}
${H(e.hora||'')} · ${H(e.usuario||'')}

${j.id?`Jugador: ${H(j.nombre)}
`:''}Equipo: ${H(teamName(e.equipo))}
${H(e.obs||'')}

`}).join('');}; window.cierrePanel=function(p,c){let disabled=c.estado==='Finalizada';return `
${disabled?`

Cédula cerrada: ${H(c.fechaCierre||'')} por ${H(c.cerradaPor||'')}

`:``}`;}; window.finalizarCedulaDigital=function(id){let p=byId(db.partidos,id), c=p&&cedulaForPartido(p,false); if(!c)return alert('Primero abre la cédula.'); c.resultadoA=document.getElementById('cerrA')?.value||'0';c.resultadoB=document.getElementById('cerrB')?.value||'0';c.observaciones=document.getElementById('cerrObs')?.value||'';c.estado='Finalizada';c.fechaCierre=new Date().toLocaleString();c.cerradaPor=(user&&(user.nombre||user.usuario))||p.arbitro||'';p.arbitro=p.arbitro||c.cerradaPor;p.estado='Finalizado';p.cedulaEstado='Finalizada';db.resultados=(db.resultados||[]).filter(r=>r.partido!==p.id);db.resultados.push({id:uid(),partido:p.id,ma:+c.resultadoA||0,mb:+c.resultadoB||0,penal:'',stats:{a:{},b:{}},obs:c.observaciones,detalle:'Resultado capturado desde cédula arbitral',fecha:new Date().toLocaleString()});save('Finalizó cédula digital');}; window.cedulaDigitalDetalle=function(p){let c=cedulaForPartido(p,false);return `
${H(dep(p.deporte))} · ${H(p.rama||'')}

${H(teamName(p.a))} vs ${H(teamName(p.b))}

Folio: ${H(cedulaFolio?cedulaFolio(p):p.id)} · Fecha: ${fmt(p.fecha)} · Lugar: ${H(p.lugar||'')} · Árbitro: ${H(p.arbitro||user?.nombre||'')} · Estado: ${cedulaEstadoDigital(p)}

${!c?``:``}
${c?`

Validación manual de jugadores

${c.estado==='Finalizada'?'

La cédula está cerrada.

':manualPanelJugadores(p,c)}${tablaJugadoresCedula(p,c)}

Anotaciones del partido

${c.estado==='Finalizada'?'

Las anotaciones quedaron cerradas.

':eventoPanel(p,c)}${tablaEventosCedula(c)}

Cierre de cédula

${cierrePanel(p,c)}
`:''}`;}; })(); /* === FIN CORRECCIÓN INTEGRAL 21 PUNTOS === */ /* === PATCH LIMPIEZA SEGURA ADMINISTRADOR === */ (function(){ function H(v){return String(v==null?'':v).replace(/[&<>\"]/g,m=>({'&':'&','<':'<','>':'>','\"':'"'}[m]||m));} function esAdmin(){try{return typeof isAdmin==='function' && isAdmin();}catch(e){return false;}} function addPage(){ if(Array.isArray(window.pages)){} try{ if(!pages.some(p=>p[0]==='limpieza')) pages.push(['limpieza','Limpiar sistema']); }catch(e){} } window.descargarBackupSistema=function(){ const blob=new Blob([JSON.stringify(db,null,2)],{type:'application/json'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='respaldo_sistema_uach_'+new Date().toISOString().slice(0,10)+'.json'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),1000); }; function resetPartidos(){ (db.partidos||[]).forEach(p=>{p.estado='Programado'; delete p.cedulaEstado; delete p.cedulaPdf; delete p.cedulaPdfNombre; delete p.cedulaPdfFecha;}); } window.limpiarTorneoSeguro=function(){ if(!esAdmin()) return alert('Solo administrador.'); const t=prompt('SEGURIDAD: primero descarga un respaldo. Para limpiar torneo escribe exactamente: LIMPIAR'); if(t!=='LIMPIAR') return alert('No se limpió nada. La palabra no coincide.'); db.partidos=[]; db.resultados=[]; db.protestas=[]; db.sanciones=[]; db.cedulas=[]; db.cedulaEventos=[]; db.bitacora=[]; db.avisos=[]; save('Limpió torneo: roles, resultados, cédulas, protestas, sanciones, bitácora y avisos'); }; window.limpiarJugadoresSeguro=function(){ if(!esAdmin()) return alert('Solo administrador.'); const t=prompt('SEGURIDAD: esto borra jugadores de la base. Para continuar escribe exactamente: LIMPIAR JUGADORES'); if(t!=='LIMPIAR JUGADORES') return alert('No se limpió nada. La palabra no coincide.'); db.jugadores=[]; db.cedulas=[]; db.cedulaEventos=[]; save('Limpió jugadores y validaciones relacionadas'); }; window.reinicioTotalSeguro=function(){ if(!esAdmin()) return alert('Solo administrador.'); const t=prompt('PELIGRO: reinicio total. Conserva solo el administrador actual. Escribe exactamente: REINICIO TOTAL'); if(t!=='REINICIO TOTAL') return alert('No se limpió nada. La palabra no coincide.'); const adminActual=user?{...user}:null; const admin=(db.usuarios||[]).find(u=>u.rol==='admin') || adminActual || {id:'admin',usuario:'admin',password:'1234',rol:'admin',nombre:'Administrador general'}; db.facultades=[]; db.deportes=[]; db.equipos=[]; db.jugadores=[]; db.partidos=[]; db.resultados=[]; db.protestas=[]; db.sanciones=[]; db.cedulas=[]; db.cedulaEventos=[]; db.bitacora=[]; db.avisos=[]; db.usuarios=[admin]; db.config=db.config||{}; save('Reinicio total del sistema'); }; window.limpieza=function(){ if(!esAdmin()) return '

Limpiar sistema

Solo administrador.

'; return `

Limpiar sistema

Área exclusiva de administrador. Antes de limpiar descarga un respaldo.

Limpiar torneo

Borra roles, resultados, cédulas, protestas, sanciones, bitácora y avisos.

Conserva: usuarios, facultades, deportes, equipos y jugadores.

Seguridad: escribir LIMPIAR.

Limpiar jugadores

Borra jugadores y validaciones/cédulas relacionadas.

Conserva: usuarios, facultades, deportes y equipos.

Seguridad: escribir LIMPIAR JUGADORES.

Reinicio total

Usar solo si vas a empezar de cero.

Borra todo excepto el usuario administrador.

Seguridad: escribir REINICIO TOTAL.

`; }; const oldCanSee=window.canSeePage; window.canSeePage=function(k){ if(k==='limpieza') return esAdmin(); return oldCanSee?oldCanSee(k):true; }; const oldRenderNav=window.renderNav; window.renderNav=function(){ addPage(); if(!canSeePage(page)) page=canSeePage('dashboard')?'dashboard':'roles'; nav.innerHTML=pages.filter(p=>canSeePage(p[0])).map(p=>``).join(''); }; window.render=function(){try{ensureDeportesSeguro();}catch(e){} addPage(); renderNav(); let map={dashboard,roles,resultados,estadisticas,tabla,reportes,protestas,sanciones,equipos,jugadores,credenciales,cotejo,cedulas_arbitro,deportes,facultades,usuarios,bitacora,avisos,herramientas,limpieza}; if(!map[page]||!canSeePage(page)) page=canSeePage('dashboard')?'dashboard':'roles'; app.innerHTML=map[page](); if(window.applyRoleChrome) applyRoleChrome(); }; })(); /* === FIN PATCH LIMPIEZA SEGURA ADMINISTRADOR === */ /* === PATCH FINAL: BUSQUEDA SIN PERDER FOCO + EDITAR/BORRAR EQUIPOS VISIBLE === */ function valEsc(v){return String(v??'').replace(/[&<>\"]/g,m=>({'&':'&','<':'<','>':'>','\"':'"'}[m]));} function filtroLocalTabla(input, tableId){ const q=String(input.value||'').toLowerCase(); const t=document.getElementById(tableId); if(!t) return; let visibles=0; t.querySelectorAll('tbody tr[data-search]').forEach(tr=>{ const ok=tr.dataset.search.toLowerCase().includes(q); tr.style.display=ok?'':'none'; if(ok) visibles++; }); const c=document.getElementById(tableId+'_count'); if(c) c.textContent=visibles+' visibles'; } function equipos(){ let q=(window.eqSearch||'').toLowerCase(); let lista=(db.equipos||[]).filter(e=>[e.nombre,e.grupo,fac(e.facultad),facN(e.facultad),dep(e.deporte),e.rama,e.entrenador,e.correo,e.telefono,e.estatus].join(' ').toLowerCase().includes(q)); let rows=lista.map(e=>{ let search=[e.nombre,e.grupo,fac(e.facultad),facN(e.facultad),dep(e.deporte),e.rama,e.entrenador,e.correo,e.telefono,e.estatus].join(' '); return `${isAdmin()?``:''}${escHtml(e.nombre||fac(e.facultad)||'')}${escHtml(e.grupo||'Sin grupo')}${escHtml(fac(e.facultad))}${escHtml(dep(e.deporte))}${escHtml(e.rama||'')}${escHtml(e.entrenador||'')}${escHtml(e.correo||'')}
${escHtml(e.telefono||'')}${escHtml(e.estatus||'Activo')}`; }).join(''); return `${isAdmin()?`

Nuevo equipo

`:''}

Equipos

${lista.length} visibles
${rows}
AccionesEquipoGrupoFacultad/CentroDeporteRamaEntrenadorContactoEstatus
`; } function editEquipo(id){ let e=(db.equipos||[]).find(x=>x.id==id); if(!e||!isAdmin()) return alert('Solo administrador.'); openM(`

Editar equipo

`); } function saveEquipo(id){ let e=(db.equipos||[]).find(x=>x.id==id); if(!e||!isAdmin()) return alert('Solo administrador.'); e.nombre=menombre.value; e.grupo=megrupo.value; e.facultad=mef.value; e.deporte=med.value; e.rama=mer.value; e.entrenador=mee.value; e.correo=mec.value; e.telefono=met.value; e.estatus=mest.value; closeM(); save('Editó equipo'); } function jugadores(){ let q=(window.jugSearch||'').toLowerCase(); let base=typeof visibleJugadores==='function'?visibleJugadores(db.jugadores):(db.jugadores||[]); let lista=base.filter(j=>[j.nombre,j.matricula,j.numero,fac(j.facultad),facN(j.facultad),dep(j.deporte),j.carrera,j.semestre,j.telefono,j.correo,j.estatus,j.observaciones,(typeof verifyCode==='function'?verifyCode(j):'')].join(' ').toLowerCase().includes(q)); let facOpts=(typeof isCoach==='function'&&isCoach())?(db.facultades||[]).filter(f=>!user.facultad||f.id==user.facultad):db.facultades; let rows=lista.map(j=>{ let search=[j.nombre,j.matricula,j.numero,fac(j.facultad),facN(j.facultad),dep(j.deporte),j.carrera,j.semestre,j.telefono,j.correo,j.estatus,j.observaciones,(typeof verifyCode==='function'?verifyCode(j):'')].join(' '); return `${j.foto?``:`
FOTO
PENDIENTE
`}${escHtml(j.nombre||'')}
${escHtml(j.observaciones||'')}${escHtml(j.matricula||'')}${escHtml(j.numero||'')}${escHtml(fac(j.facultad))}${escHtml(dep(j.deporte))}${escHtml(j.carrera||'')}
${escHtml(j.semestre||'')}${escHtml(j.telefono||'')}
${escHtml(j.correo||'')}${escHtml(j.estatus||'Pendiente')}${!j.foto?'
Foto pendiente':''}${typeof verifyCode==='function'?`${verifyCode(j)}`:''}${(typeof canManageJugador==='function'?canManageJugador(j):canEdit())?`${(typeof canApprove==='function'&&canApprove())?``:''}`:''}`; }).join(''); return `${canEdit()?`

Nuevo jugador

Se puede guardar sin foto y cargarla después.

`:''}

Jugadores

${lista.length} visibles
${rows}
FotoNombreMatrículaNo.Facultad/CentroDeporteCarreraContactoEstatusFolioAcciones
`; } /* === FIN PATCH FINAL === */ /* === PARCHE REAL 20260601: BUSQUEDAS SIN RECARGAR + EDITAR EQUIPOS VISIBLE === */ (function(){ function H(v){ if(typeof escHtml==='function') return escHtml(v==null?'':String(v)); return String(v==null?'':v).replace(/[&<>\"]/g,function(m){return {'&':'&','<':'<','>':'>','\\"':'"','"':'"'}[m]||m;}); } function N(v){return String(v==null?'':v).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'');} function teamNameLocal(e){return (e&&(e.nombre||e.equipo||e.procedencia||e.institucion)) || (e&&e.facultad&&typeof fac==='function'?fac(e.facultad):'') || 'Equipo';} function facLocal(id){try{return typeof fac==='function'?fac(id):(id||'');}catch(e){return id||'';}} function depLocal(id){try{return typeof dep==='function'?dep(id):(id||'');}catch(e){return id||'';}} function visibleEquiposLocal(){try{return typeof equiposVisibles==='function'?equiposVisibles():(typeof visibleEquipos==='function'?visibleEquipos(db.equipos||[]):(db.equipos||[]));}catch(e){return db.equipos||[];}} function visibleJugadoresLocal(){try{return typeof visibleJugadores==='function'?visibleJugadores(db.jugadores||[]):(db.jugadores||[]);}catch(e){return db.jugadores||[];}} window.filtroLocalTabla=function(input, tableId){ var q=N(input && input.value || ''); var table=document.getElementById(tableId); if(!table) return; var rows=table.querySelectorAll('tbody tr'); var c=0; rows.forEach(function(tr){var txt=N(tr.getAttribute('data-search')||tr.innerText||''); var ok=!q||txt.indexOf(q)>=0; tr.style.display=ok?'':'none'; if(ok)c++;}); var count=document.getElementById(tableId+'_count'); if(count) count.textContent=c+' visibles'; }; window.equipos=function(){ var q=N(window.eqSearch||''); var lista=visibleEquiposLocal().filter(function(e){return !q || N([teamNameLocal(e),e.grupo,facLocal(e.facultad),e.facultadNombre,e.tipo,e.tipoEquipo,e.procedencia,e.institucion,depLocal(e.deporte),e.rama,e.entrenador,e.correo,e.telefono,e.estatus].join(' ')).indexOf(q)>=0;}); var puedeCrear=(typeof isAdmin==='function'&&isAdmin())||(typeof isCoach==='function'&&isCoach()); var facOpts=(db.facultades||[]).map(function(f){return '';}).join(''); var depOpts=(db.deportes||[]).map(function(d){return '';}).join(''); var form=puedeCrear?'

Nuevo equipo

':''; var rows=lista.map(function(e){ var search=[teamNameLocal(e),e.grupo,facLocal(e.facultad),e.facultadNombre,e.tipo,e.tipoEquipo,e.procedencia,e.institucion,depLocal(e.deporte),e.rama,e.entrenador,e.correo,e.telefono,e.estatus].join(' '); var acciones=((typeof isAdmin==='function'&&isAdmin()) || (typeof esDueñoEquipo==='function'&&esDueñoEquipo(e)))?' '+((typeof isAdmin==='function'&&isAdmin())?'':''):''; return ''+acciones+''+H(teamNameLocal(e))+''+H(e.grupo||'Sin grupo')+''+H(facLocal(e.facultad)||e.procedencia||e.institucion||'')+''+H(e.tipo||e.tipoEquipo||'')+''+H(depLocal(e.deporte))+''+H(e.rama||'')+''+H(e.entrenador||'')+''+H(e.correo||'')+'
'+H(e.telefono||'')+''+H(e.estatus||'Activo')+''; }).join(''); return form+'

Equipos

'+lista.length+' visibles
'+rows+'
AccionesEquipoGrupoFacultad/CentroTipoDeporteRamaEntrenadorContactoEstatus

Versión corregida: edición visible y búsqueda continua sin recargar.

'; }; try{ equipos=window.equipos; }catch(e){} window.addEquipoUACHFix=function(){ if(!((typeof isAdmin==='function'&&isAdmin())||(typeof isCoach==='function'&&isCoach()))) return alert('Sin permiso.'); var obj={id:(typeof uid==='function'?uid():Date.now().toString(36)),nombre:(document.getElementById('enombre')?.value||'').trim(),grupo:document.getElementById('egrupo')?.value||'',facultad:document.getElementById('ef')?.value||'',deporte:document.getElementById('ed')?.value||'',rama:document.getElementById('er')?.value||'',entrenador:document.getElementById('ee')?.value||'',correo:document.getElementById('ec')?.value||'',telefono:document.getElementById('et')?.value||'',estatus:'Activo',creadoPor:(window.user&&user.usuario)||''}; if(!obj.nombre) obj.nombre=facLocal(obj.facultad)||'Equipo'; (db.equipos||(db.equipos=[])).push(obj); save('Registró equipo'); }; window.editEquipoUACHFix=function(id){ var e=(db.equipos||[]).find(function(x){return String(x.id)===String(id)}); if(!e) return alert('No se encontró el equipo.'); if(!((typeof isAdmin==='function'&&isAdmin())||(typeof esDueñoEquipo==='function'&&esDueñoEquipo(e)))) return alert('No tienes permiso para editar este equipo.'); var facOpts=(db.facultades||[]).map(function(f){return '';}).join(''); var depOpts=(db.deportes||[]).map(function(d){return '';}).join(''); openM('

Editar equipo

'); }; window.editEquipo=window.editEquipoUACHFix; window.saveEquipoUACHFix=function(id){ var e=(db.equipos||[]).find(function(x){return String(x.id)===String(id)}); if(!e) return; Object.assign(e,{nombre:menombre.value,grupo:megrupo.value,facultad:mef.value,tipo:metipo.value,tipoEquipo:metipo.value,deporte:med.value,rama:mer.value,entrenador:mee.value,correo:mec.value,telefono:met.value,estatus:mest.value}); closeM(); save('Editó equipo'); }; window.saveEquipo=window.saveEquipoUACHFix; window.jugadores=function(){ var q=N(window.jugSearch||''); var lista=visibleJugadoresLocal().filter(function(j){ var eq=(db.equipos||[]).find(function(e){return String(e.id)===String(j.equipo)})||{}; return !q || N([j.nombre,j.apellidoP,j.apellidoM,j.matricula,j.numero,teamNameLocal(eq),facLocal(j.facultad),depLocal(j.deporte),j.carrera,j.semestre,j.telefono,j.correo,j.estatus,j.observaciones,j.clasificado,j.jugadorClasificado].join(' ')).indexOf(q)>=0; }); var rows=lista.map(function(j){ var eq=(db.equipos||[]).find(function(e){return String(e.id)===String(j.equipo)})||{}; var nom=[j.nombre,j.apellidoP,j.apellidoM].filter(Boolean).join(' ') || j.nombre || ''; var search=[nom,j.matricula,j.numero,teamNameLocal(eq),facLocal(j.facultad),depLocal(j.deporte),j.carrera,j.semestre,j.telefono,j.correo,j.estatus,j.observaciones,j.clasificado,j.jugadorClasificado].join(' '); var acciones=((typeof isAdmin==='function'&&isAdmin()) || (typeof canManageJugador==='function'&&canManageJugador(j)) || (typeof esJugadorPropio==='function'&&esJugadorPropio(j)))?' '+((typeof isAdmin==='function'&&isAdmin())?'':''):''; return ''+(j.foto?'':'
FOTO
PENDIENTE
')+''+H(nom)+'
'+H(j.observaciones||'')+''+H(j.matricula||'')+''+H(j.numero||'')+''+H(teamNameLocal(eq)||facLocal(j.facultad))+''+H(facLocal(j.facultad))+''+H(depLocal(j.deporte||eq.deporte))+''+H(j.carrera||'')+'
'+H(j.semestre||'')+''+H(j.telefono||'')+'
'+H(j.correo||'')+''+H(j.estatus||'Pendiente')+''+acciones+''; }).join(''); var puede=(typeof canEdit==='function'?canEdit():false)||(typeof isAdmin==='function'&&isAdmin())||(typeof isCoach==='function'&&isCoach()); var form=puede && typeof addJugador==='function'?'

Nuevo jugador

Captura normal del jugador.

':''; return form+'

Jugadores

'+lista.length+' visibles
'+rows+'
FotoNombreMatrículaNo.EquipoFacultad/CentroDeporteCarreraContactoEstatusAcciones

Versión corregida: búsqueda continua sin recargar.

'; }; try{ jugadores=window.jugadores; }catch(e){} // Roles: no se vuelve a renderizar al cambiar deporte/rama; solo actualiza equipos A/B y conserva selección. if(typeof window.roles==='function'){ window.actualizarEquiposRolSinRender=function(){ if(typeof actualizarEquiposRol==='function') actualizarEquiposRol(); }; var oldRoles=window.roles; window.roles=function(){ var html=oldRoles(); return html.replaceAll('onchange="render()"','onchange="window.rolDepSel=this.value;window.actualizarEquiposRolSinRender()"').replaceAll('onchange="window.rolDepSel=this.value;actualizarEquiposRol()"','onchange="window.rolDepSel=this.value;actualizarEquiposRolSinRender()"').replaceAll('onchange="window.rolRamaSel=this.value;actualizarEquiposRol()"','onchange="window.rolRamaSel=this.value;actualizarEquiposRolSinRender()"'); }; try{ roles=window.roles; }catch(e){} } window.render=function(){try{ensureDeportesSeguro();}catch(e){} if(typeof addToolsPage==='function') addToolsPage(); if(typeof renderNav==='function') renderNav(); var map={dashboard:window.dashboard||dashboard,roles:window.roles||roles,resultados:window.resultados||resultados,estadisticas:window.estadisticas||estadisticas,tabla:window.tabla||tabla,reportes:window.reportes||reportes,protestas:window.protestas||protestas,sanciones:window.sanciones||sanciones,equipos:window.equipos,jugadores:window.jugadores,credenciales:window.credenciales||credenciales,cotejo:window.cotejo||cotejo,cedulas_arbitro:window.cedulas_arbitro||cedulas_arbitro,deportes:window.deportes||deportes,facultades:window.facultades||facultades,usuarios:window.usuarios||usuarios,bitacora:window.bitacora||bitacora,avisos:window.avisos||avisos,herramientas:window.herramientas||herramientas}; if(!map[page] || (typeof canSeePage==='function'&&!canSeePage(page))) page='dashboard'; app.innerHTML=map[page](); if(window.applyRoleChrome) applyRoleChrome(); }; try{ render=window.render; }catch(e){} })(); /* === FIN PARCHE REAL 20260601 === */ /* === PARCHE FINAL CONSOLIDADO PRODUCCIÓN 20260605 === */ (function(){ function H(v){return String(v==null?'':v).replace(/[&<>"']/g,function(m){return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[m];});} function N(v){return String(v==null?'':v).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').trim();} function id(){try{return uid()}catch(e){return 'id_'+Date.now().toString(36)+Math.random().toString(36).slice(2,7)}} function by(a,x){return (a||[]).find(y=>String(y.id)===String(x));} function okStatus(j){return ['aprobado','activo'].includes(N(j&&j.estatus));} function clsText(j){let v=(j&& (j.clasificado||j.jugadorClasificado||j.regla)) || 'No clasificado'; return /^(si|sí|clasificado)$/i.test(String(v).trim())?'Clasificado':'No clasificado';} function fullName(j){return [j&& (j.nombres||j.nombreBase), j&&j.apellidoP, j&&j.apellidoM].filter(Boolean).join(' ').replace(/\s+/g,' ').trim() || (j&&j.nombre) || '';} function facLbl(x){try{let f=by(db.facultades,x); return f?(f.siglas||f.nombre):String(x||'');}catch(e){return String(x||'');}} function depLbl(x){try{let d=by(db.deportes,x); return d?(d.nombre||d.id):String(x||'');}catch(e){return String(x||'');}} function teamObj(x){return by(db.equipos,x) || null;} function teamLbl(x){let e=teamObj(x); if(!e) return facLbl(x)||String(x||''); let base=(e.nombre||e.equipo||e.procedencia||e.institucion||facLbl(e.facultad)||'Equipo'); return String(base).trim();} function equipoDeJugador(j){return teamObj(j&&j.equipo) || (db.equipos||[]).find(e=>String(e.facultad)==String(j&&j.facultad)&&String(e.deporte)==String(j&&j.deporte));} function jugadorEnEquipo(j,eid){return String(j&&j.equipo)==String(eid) || (!j.equipo && String(j&&j.facultad)==String((teamObj(eid)||{}).facultad));} window.teamName=teamLbl; window.facLabel=facLbl; window.fullPlayerName=fullName; try{ if(typeof pages!=='undefined'){pages.forEach(p=>{if(p[0]==='tabla')p[1]='Ranking general';});}} catch(e){} // CSS final de producción: acceso oculto al iniciar sesión, cabecera, credenciales, responsive. if(!document.getElementById('patchFinalCSS')){ let st=document.createElement('style'); st.id='patchFinalCSS'; st.textContent=` body.sesion-activa aside > .card.noPrint:first-child{display:none!important} .top{display:flex;align-items:center;justify-content:space-between;gap:18px;min-height:92px}.top .topText{min-width:0}.top .interLogo{max-width:260px;max-height:72px;object-fit:contain;filter:drop-shadow(0 2px 4px rgba(0,0,0,.18))}.top p{font-size:15px}.top p .oldRank{display:none} .credGrid{grid-template-columns:repeat(auto-fit,minmax(360px,1fr))}.credNew{min-height:236px}.credHead .interLogoCred{max-width:138px;max-height:42px;object-fit:contain;background:rgba(255,255,255,.94);border-radius:8px;padding:3px}.credBody.final{grid-template-columns:92px 1fr 92px;align-items:start}.fotoCred{width:88px;height:104px}.qrBoxFinal{display:flex;align-items:center;justify-content:center;border:1px solid #e5e7eb;border-radius:8px;padding:4px;background:#fff}.qrBoxFinal img{width:78px;height:78px}.credClass{display:inline-block;margin-top:2px;padding:2px 6px;border-radius:999px;background:#f3f4f6;font-weight:800;font-size:10px}.sheetLogo{max-height:70px;max-width:310px;object-fit:contain}.logoLine{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px}.compactFilters{display:grid;grid-template-columns:repeat(5,minmax(140px,1fr));gap:10px}.pos{font-weight:900;color:#6F3296}.tableWrap{overflow-x:auto}.teamSheetHeader{display:flex;justify-content:space-between;align-items:center;gap:14px;border-bottom:4px solid #D4A52A;padding-bottom:10px;margin-bottom:12px}.teamSheetHeader img{max-height:70px;max-width:300px} @media(max-width:900px){.top{flex-direction:column;align-items:flex-start}.top .interLogo{max-width:210px}.compactFilters{grid-template-columns:1fr}.grid,.grid2,.grid3{grid-template-columns:1fr!important}.credBody.final{grid-template-columns:78px 1fr}.qrBoxFinal{grid-column:1/-1;justify-content:flex-end}.credGrid{grid-template-columns:1fr}.top h1{font-size:22px}} @media print{.topLogoForPrint{display:block}.credGridPrint{display:grid;grid-template-columns:repeat(2,1fr);gap:3mm;align-items:start}.credNew{break-inside:avoid;page-break-inside:avoid}.credNew:nth-child(6n){page-break-after:always}.credHead .interLogoCred{max-height:35px;max-width:120px}.qrBoxFinal img{width:70px;height:70px}.teamSheetHeader img{max-height:55px}.noPrint{display:none!important}} `; document.head.appendChild(st); } function marcaSesion(){document.body.classList.toggle('sesion-activa',!!window.user || (typeof user!=='undefined'&&!!user));} function ensureHeaderLogo(){let top=document.querySelector('.top'); if(!top || top.dataset.finalLogo)return; top.dataset.finalLogo='1'; let html=top.innerHTML; top.innerHTML='
'+html.replace('ranking por facultad','ranking general')+'
';} ensureHeaderLogo(); marcaSesion(); function equiposPermitidos(){let arr=(db.equipos||[]).filter(e=>N(e.estatus)!=='inactivo'); try{ if(typeof isCoach==='function'&&isCoach()){arr=arr.filter(e=>String(e.creadoPor||'')===String(user.usuario||'') || N(e.entrenador)===N(user.nombre) || N(e.entrenadorUsuario)===N(user.usuario) || (!e.creadoPor && user.facultad && String(e.facultad)===String(user.facultad))); }}catch(e){} return arr;} function grupoOpts(sel){let vals=[...(db.grupos||['A','B','C','D','Único','Libre'])]; return vals.map(g=>``).join('');} function ramaOpts(sel){return ['Varonil','Femenil','Mixto'].map(r=>``).join('');} function deporteOpts(sel){return (db.deportes||[]).map(d=>``).join('');} function tipoOpts(sel){return ['Facultad UACH','Invitado','Externo','Institución','Club'].map(t=>``).join('');} function facOpts(sel){return (db.facultades||[]).map(f=>``).join('');} function equipoOpts(sel, onlyVisible=true){let arr=onlyVisible?equiposPermitidos():(db.equipos||[]); return arr.map(e=>``).join('');} window.equipos=function(){ let q=N(window.eqSearch||''); let lista=equiposPermitidos().filter(e=>!q||N([teamLbl(e.id),e.procedencia,e.institucion,e.tipo,e.grupo,depLbl(e.deporte),e.rama,e.entrenador,e.correo,e.telefono,e.estatus].join(' ')).includes(q)); let puede=(typeof isAdmin==='function'&&isAdmin())||(typeof isCoach==='function'&&isCoach()); let form=puede?`

Nuevo equipo

El equipo se registra por nombre. Puede ser UACH, invitado, externo, institución o club; no depende de una facultad.

`:''; let rows=lista.map(e=>`${H(teamLbl(e.id))}${H(e.tipo||'')}${H(e.procedencia||e.institucion||facLbl(e.facultad)||'')}${H(depLbl(e.deporte))}${H(e.rama||'')}${H(e.grupo||'')}${H(e.entrenador||'')}${H(e.correo||'')}
${H(e.telefono||'')}${H(e.estatus||'Activo')}${puede?` ${(typeof isAdmin==='function'&&isAdmin())?``:''}`:''}`).join(''); return form+`

Equipos

${lista.length} visibles
${rows}
EquipoTipoProcedenciaDeporteRamaGrupoEntrenadorContactoEstatusAcciones
`; }; window.addEquipoFinal=function(){let obj={id:id(),nombre:(enombre.value||'').trim(),tipo:etipo.value,tipoEquipo:etipo.value,procedencia:eproc.value,institucion:eproc.value,deporte:ed.value,rama:er.value,grupo:egrupo.value,entrenador:ee.value,correo:ec.value,telefono:et.value,facultad:ef.value||'',estatus:'Activo',creadoPor:user&&user.usuario||''}; if(!obj.nombre)return alert('Escribe el nombre del equipo.'); (db.equipos||(db.equipos=[])).push(obj); save('Registró equipo');}; window.editEquipoFinal=function(eid){let e=teamObj(eid); if(!e)return; openM(`

Editar equipo

`);}; window.saveEquipoFinal=function(eid){let e=teamObj(eid); if(!e)return; Object.assign(e,{nombre:menombre.value,tipo:metipo.value,tipoEquipo:metipo.value,procedencia:meproc.value,institucion:meproc.value,deporte:med.value,rama:mer.value,grupo:megrupo.value,entrenador:mee.value,correo:mec.value,telefono:met.value,facultad:mef.value,estatus:mest.value}); closeM(); save('Editó equipo');}; window.editEquipo=window.editEquipoFinal; window.jugadores=function(){let q=N(window.jugSearch||'');let base=(db.jugadores||[]).filter(j=>{try{ if(typeof isCoach==='function'&&isCoach()) return equiposPermitidos().some(e=>jugadorEnEquipo(j,e.id));}catch(e){} return true;});let lista=base.filter(j=>!q||N([fullName(j),j.nombre,j.matricula,j.numero,teamLbl(j.equipo),facLbl(j.facultad),depLbl(j.deporte),j.carrera,j.semestre,j.telefono,j.correo,j.estatus,clsText(j)].join(' ')).includes(q));let puede=(typeof canEdit==='function'&&canEdit())||(typeof isCoach==='function'&&isCoach())||(typeof isAdmin==='function'&&isAdmin());let eq=equiposPermitidos();let form=puede?`

Nuevo jugador

El jugador queda ligado al equipo seleccionado. El administrador aprueba para que salga en credencial y cédula.

`:'';let rows=lista.map(j=>{let e=equipoDeJugador(j)||{};return `${j.foto?``:`
FOTO
PENDIENTE
`}${H(fullName(j))}
${H(j.observaciones||'')}${H(j.matricula||'')}${H(j.numero||'')}${H(teamLbl(j.equipo)||teamLbl(e.id))}${H(depLbl(j.deporte||e.deporte))}${H(j.carrera||'')}
${H(j.semestre||'')}${H(clsText(j))}${H(j.telefono||'')}
${H(j.correo||'')}${H(j.estatus||'Pendiente')} ${(typeof isAdmin==='function'&&isAdmin())?``:''}`}).join('');return form+`

Jugadores

${lista.length} visibles
${rows}
FotoNombreMatrículaNo.EquipoDeporteCarreraClasificadoContactoEstatusAcciones
`}; window.addJugadorFinal=async function(){let e=teamObj(jeq.value); if(!e)return alert('Selecciona un equipo.'); if(!jn.value||!jap.value||!jam.value||!jm.value)return alert('Completa nombre, apellidos y matrícula.'); let foto=''; if(jfoto.files&&jfoto.files[0]) foto=await subirArchivo(jfoto.files[0]); let obj={id:id(),equipo:jeq.value,nombres:jn.value,apellidoP:jap.value,apellidoM:jam.value,nombre:[jn.value,jap.value,jam.value].join(' ').replace(/\s+/g,' ').trim(),matricula:jm.value,numero:jnum.value,facultad:e.facultad||'',deporte:e.deporte||'',rama:e.rama||'',grupo:e.grupo||'',carrera:jc.value,semestre:js.value,telefono:jt.value,correo:jco.value,clasificado:jclas.value,jugadorClasificado:jclas.value,estatus:(typeof isCoach==='function'&&isCoach())?'Pendiente':(jest.value||'Pendiente'),observaciones:jobs.value||'',foto,creadoPor:user&&user.usuario||''}; (db.jugadores||(db.jugadores=[])).push(obj); save('Registró jugador');}; window.editJugadorFinal=function(jid){let j=by(db.jugadores,jid); if(!j)return; openM(`

Editar jugador

`)}; window.saveJugadorFinal=async function(jid){let j=by(db.jugadores,jid),e=teamObj(mjeq.value); if(!j||!e)return; Object.assign(j,{equipo:mjeq.value,nombres:mjn.value,apellidoP:mjap.value,apellidoM:mjam.value,nombre:[mjn.value,mjap.value,mjam.value].join(' ').replace(/\s+/g,' ').trim(),matricula:mjm.value,numero:mjnum.value,facultad:e.facultad||'',deporte:e.deporte||'',rama:e.rama||'',grupo:e.grupo||'',carrera:mjc.value,semestre:mjs.value,telefono:mjt.value,correo:mjco.value,clasificado:mjclas.value,jugadorClasificado:mjclas.value,observaciones:mjobs.value}); if(typeof isAdmin==='function'&&isAdmin())j.estatus=mjest.value; else j.estatus='Pendiente'; if(mjfoto.files&&mjfoto.files[0])j.foto=await subirArchivo(mjfoto.files[0]); closeM(); save('Editó jugador');}; window.editJugador=window.editJugadorFinal; function pseudoQR(txt){let safe=encodeURIComponent(String(txt||''));return `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${safe}`;} window.credCard=function(j){let e=equipoDeJugador(j)||{}; let est=j.estatus||'Pendiente'; return `
Sistema Integral de Gestión Deportiva UACHCredencial oficial de jugador
Interfacultades
${j.foto?``:`
FOTO
PENDIENTE
`}

${H(fullName(j))}

Matrícula/ID: ${H(j.matricula||'')}

Equipo: ${H(teamLbl(j.equipo||e.id))}

Deporte: ${H(depLbl(j.deporte||e.deporte))}

Rama: ${H(j.rama||e.rama||'')}

Grupo: ${H(j.grupo||e.grupo||'')}

Número: ${H(j.numero||'')}

${H(clsText(j))}
QR
Folio: ${H(j.folioCotejo||('UACH-'+(j.matricula||j.id)))}${H(est)}
`;}; function credList(){let sel=window.credEquipo||'';let js=(db.jugadores||[]).filter(j=>okStatus(j)); if(sel)js=js.filter(j=>jugadorEnEquipo(j,sel)); return js;} window.currentCredList=credList; window.credenciales=function(){let eqs=equiposPermitidos();let sel=window.credEquipo||eqs[0]?.id||'';let js=credList();return `

Credenciales por equipo

Candado activo: solo jugadores aprobados aparecen en credenciales.

Vista previa: ${H(sel?teamLbl(sel):'Todos')}

${js.length?`
${js.map(j=>credCard(j)).join('')}
`:'

Sin jugadores aprobados para imprimir.

'}
`;}; window.printDoc=function(title,html,orient='landscape'){let w=window.open('','_blank');w.document.write(`${H(title)} ${html} `);w.document.close();}; window.printCredenciales=function(){let lista=credList();if(!lista.length)return alert('No hay jugadores aprobados para imprimir.');let html=`

Credenciales oficiales

Generado: ${new Date().toLocaleString()} · Total: ${lista.length}

${lista.map(j=>credCard(j)).join('')}
`;printDoc('credenciales_interfacultades',html,'landscape');}; window.imprimirCedulaEquipo=function(){let eid=window.credEquipo||''; if(!eid)return alert('Selecciona un equipo.');let e=teamObj(eid),lista=(db.jugadores||[]).filter(j=>jugadorEnEquipo(j,eid)&&okStatus(j));let html=`

Cédula oficial de equipo

${H(teamLbl(eid))} · ${H(depLbl(e.deporte))} · ${H(e.rama||'')} · Grupo ${H(e.grupo||'')}

Solo jugadores aprobados · Total: ${lista.length}

${lista.map(j=>``).join('')}
No.JugadorMatrículaClasificadoEstatusFirma
${H(j.numero||'')}${H(fullName(j))}${H(j.matricula||'')}${H(clsText(j))}${H(j.estatus)}
`;printDoc('cedula_equipo_'+teamLbl(eid),html,'portrait');}; function puntosFinal(r,p){let a=+r.ma||0,b=+r.mb||0, fut=N((by(db.deportes,p&&p.deporte)||{}).regla)==='futbol'||N(depLbl(p&&p.deporte)).includes('futbol'); if(a>b)return {a:3,b:0}; if(b>a)return {a:0,b:3}; if(fut&&r.penal==p.a)return {a:2,b:1}; if(fut&&r.penal==p.b)return {a:1,b:2}; return {a:1,b:1};} window.puntos=puntosFinal; function rankingEquipos(f={}){let rows={};(db.equipos||[]).forEach(e=>rows[e.id]={id:e.id,equipo:teamLbl(e.id),procedencia:e.procedencia||e.institucion||facLbl(e.facultad),deporte:e.deporte,rama:e.rama,grupo:e.grupo,pj:0,pg:0,pe:0,pp:0,pf:0,pc:0,dif:0,pts:0});(db.resultados||[]).forEach(r=>{let p=by(db.partidos,r.partido); if(!p)return; if(f.dep&&p.deporte!==f.dep)return; if(f.rama&&p.rama!==f.rama)return; if(f.grupo&&p.grupo!==f.grupo)return; if(f.jornada&&String(p.jornada)!==String(f.jornada))return; let A=rows[p.a]||(rows[p.a]={id:p.a,equipo:teamLbl(p.a),procedencia:'',pj:0,pg:0,pe:0,pp:0,pf:0,pc:0,dif:0,pts:0}),B=rows[p.b]||(rows[p.b]={id:p.b,equipo:teamLbl(p.b),procedencia:'',pj:0,pg:0,pe:0,pp:0,pf:0,pc:0,dif:0,pts:0});let ma=+r.ma||0,mb=+r.mb||0,pt=puntosFinal(r,p);A.pj++;B.pj++;A.pf+=ma;A.pc+=mb;B.pf+=mb;B.pc+=ma;A.pts+=pt.a;B.pts+=pt.b;if(ma>mb){A.pg++;B.pp++}else if(mb>ma){B.pg++;A.pp++}else{A.pe++;B.pe++;}});return Object.values(rows).filter(x=>x.pj>0).map(x=>(x.dif=x.pf-x.pc,x)).sort((a,b)=>b.pts-a.pts||b.dif-a.dif||b.pf-a.pf||a.equipo.localeCompare(b.equipo,'es'));} function currentFilters(prefix){return {dep:document.getElementById(prefix+'Dep')?.value||'',rama:document.getElementById(prefix+'Rama')?.value||'',grupo:document.getElementById(prefix+'Grupo')?.value||'',jornada:document.getElementById(prefix+'Jornada')?.value||'',tipo:document.getElementById(prefix+'Tipo')?.value||'tabla'};} function filtrosCompact(prefix){let deps=(db.deportes||[]).map(d=>``).join('');let grupos=[...new Set((db.partidos||[]).map(p=>p.grupo).filter(Boolean))];let jorn=[...new Set((db.partidos||[]).map(p=>p.jornada).filter(Boolean))];return `
`;} function tablaRanking(f){let arr=rankingEquipos(f);return !arr.length?'

Sin resultados.

':`
${arr.map((x,i)=>``).join('')}
PosEquipoProcedenciaPJPGPEPPGF/PFGC/PCDIFPTS
${i+1}${H(x.equipo)}${H(x.procedencia)}${x.pj}${x.pg}${x.pe}${x.pp}${x.pf}${x.pc}${x.dif}${x.pts}
`;} function eventosRows(f){let rows={};(db.cedulas||[]).forEach(c=>{let p=by(db.partidos,c.partido); if(!p)return; if(f.dep&&p.deporte!==f.dep)return; if(f.rama&&p.rama!==f.rama)return; if(f.grupo&&p.grupo!==f.grupo)return; if(f.jornada&&String(p.jornada)!==String(f.jornada))return; (c.eventos||[]).forEach(e=>{if(!e.jugador)return; let j=by(db.jugadores,e.jugador)||{}; let k=j.id||e.jugador; rows[k]=rows[k]||{jug:fullName(j)||j.nombre||'Sin jugador',equipo:teamLbl(e.equipo||j.equipo),deporte:depLbl(p.deporte),total:0,det:{}};let cant=+e.cantidad||1; rows[k].det[e.tipo]=(rows[k].det[e.tipo]||0)+cant; rows[k].total+=cant;});}); return Object.values(rows).sort((a,b)=>b.total-a.total||a.jug.localeCompare(b.jug,'es'));} window.tablaStatsJugadores=function(sel=''){let f=typeof sel==='object'?sel:{dep:sel||''};let arr=eventosRows(f);let stats=[...new Set(arr.flatMap(x=>Object.keys(x.det)))]; if(!arr.length)return '

Sin estadísticas individuales capturadas.

'; return `
${stats.map(s=>``).join('')}${arr.map((x,i)=>`${stats.map(s=>``).join('')}`).join('')}
PosJugadorEquipoDeporte${H(s)}Total
${i+1}${H(x.jug)}${H(x.equipo)}${H(x.deporte)}${x.det[s]||0}${x.total}
`;}; function tablaResultadosFiltro(f){let arr=(db.resultados||[]).filter(r=>{let p=by(db.partidos,r.partido); return p&&(!f.dep||p.deporte===f.dep)&&(!f.rama||p.rama===f.rama)&&(!f.grupo||p.grupo===f.grupo)&&(!f.jornada||String(p.jornada)===String(f.jornada));}); if(!arr.length)return '

Sin resultados.

'; return `
${arr.map(r=>{let p=by(db.partidos,r.partido)||{};let ma=+r.ma||0,mb=+r.mb||0;let gan=ma>mb?teamLbl(p.a):mb>ma?teamLbl(p.b):(r.penal?('Empate ganado: '+teamLbl(r.penal)):'Empate');return ``}).join('')}
FechaPartidoMarcadorGanadorObs.
${H(r.fecha||fmt(p.fecha))}${H(teamLbl(p.a))} vs ${H(teamLbl(p.b))}${ma} - ${mb}${H(gan)}${H(r.obs||r.detalle||'')}
`;} window.estadisticas=function(){let f=currentFilters('est');let tipo=f.tipo;let body= tipo==='resultados'?tablaResultadosFiltro(f):tipo==='goleadores'?tablaStatsJugadores(f):tipo==='pendientes'?`
${tablaPartidos?tablaPartidos((db.partidos||[]).filter(p=>p.estado!=='Finalizado'),false,false):''}
`:tablaRanking(f);return `

Estadísticas

${filtrosCompact('est')}

${tipo==='goleadores'?'Goleadores / estadísticas individuales':tipo==='resultados'?'Resultados filtrados':tipo==='pendientes'?'Partidos pendientes':'Ranking general'}

${body}
`;}; window.tabla=function(){let f=currentFilters('rank');return `

Ranking general de equipos

${filtrosCompact('rank')}

Regla fútbol: empate ganado = 2 puntos, empate perdido = 1 punto.

${tablaRanking(f)}
`;}; // Cédula arbitral: cantidad manual y nombre completo. if(typeof window.eventoPanel==='function'){ window.eventoPanel=function(p,c){let equipoSel=window.evEquipoSel||p.a;let jugadores=(c.jugadores||[]).map(x=>by(db.jugadores,x.jugador)).filter(j=>j&&jugadorEnEquipo(j,equipoSel));let stats=((by(db.deportes,p.deporte)||{}).estadisticas||['Gol','Punto','Falta','Observación']);return `
`;}; } window.agregarEventoCedula=function(pid){let p=by(db.partidos,pid), c=p&&cedulaForPartido(p,false); if(!c||c.estado==='Finalizada')return alert('La cédula no está disponible.'); let cant=Math.max(1,+document.getElementById('evCant')?.value||1); (c.eventos||(c.eventos=[])).push({id:id(),tipo:evTipo.value,cantidad:cant,minuto:evMin.value,jugador:evJugador.value,equipo:evEquipo.value,obs:evObs.value,hora:new Date().toLocaleString(),usuario:user?.usuario||''}); save('Agregó anotación a cédula');}; // Datos de prueba FCCF vs FCA sin duplicar. window.crearDatosPruebaFinal=function(){let fut=(db.deportes||[]).find(d=>N(d.nombre).includes('futbol rapido'))||(db.deportes||[])[0]; if(!fut)return alert('No hay deporte fútbol rápido.');function addEq(nombre,facu,ent){let ex=(db.equipos||[]).find(e=>N(e.nombre)===N(nombre)); if(ex)return ex; let e={id:id(),nombre,tipo:'Facultad UACH',procedencia:facu.toUpperCase(),facultad:facu,deporte:fut.id,rama:'Varonil',grupo:'A',entrenador:ent,correo:'',telefono:'',estatus:'Activo',creadoPor:'admin'};(db.equipos||(db.equipos=[])).push(e);return e;}let A=addEq('Halcones FCCF','fccf','Ricardo López Martínez'),B=addEq('Leones FCA','fca','Karla Medina Ortiz');let jugadoresA=[['7','Diego','Hernández','Soto','Sí'],['9','Luis','Mendoza','Ruiz','Sí'],['10','Carlos','Pérez','Ramos','Sí'],['11','Andrés','Chávez','Flores','No'],['13','Javier','Torres','Salas','Sí'],['15','Marco','Núñez','Díaz','Sí']];let jugadoresB=[['5','Miguel','Ríos','Vega','Sí'],['8','Omar','Salazar','Campos','Sí'],['10','Eduardo','Castillo','Lara','Sí'],['12','Héctor','Ramírez','López','Sí'],['14','Iván','Morales','Peña','No'],['17','Daniel','García','Soto','Sí']];function addJug(arr,e){arr.forEach(x=>{let mat=e.nombre.slice(0,3).toUpperCase()+x[0]; if((db.jugadores||[]).some(j=>j.matricula===mat))return;(db.jugadores||(db.jugadores=[])).push({id:id(),equipo:e.id,nombres:x[1],apellidoP:x[2],apellidoM:x[3],nombre:[x[1],x[2],x[3]].join(' '),matricula:mat,numero:x[0],facultad:e.facultad,deporte:e.deporte,rama:e.rama,grupo:e.grupo,carrera:'Prueba',semestre:'1',telefono:'6140000000',correo:'prueba@uach.mx',clasificado:x[4]==='Sí'?'Clasificado':'No clasificado',estatus:'Aprobado',observaciones:'Dato de prueba',foto:'',creadoPor:'admin'});});}addJug(jugadoresA,A);addJug(jugadoresB,B); if(!(db.partidos||[]).some(p=>p.a===A.id&&p.b===B.id&&p.jornada==='1')){let p1={id:id(),fecha:'2026-06-15T18:00',deporte:fut.id,rama:'Varonil',a:A.id,b:B.id,lugar:'Campo Universitario 1',jornada:'1',fase:'Regular',grupo:'A',arbitro:'Juan Pérez Árbitro',observaciones:'Partido de prueba empatado ganado por FCCF',estado:'Finalizado'};(db.partidos||(db.partidos=[])).push(p1);(db.resultados||(db.resultados=[])).push({id:id(),partido:p1.id,ma:2,mb:2,penal:A.id,stats:{},obs:'Empate ganado por Halcones FCCF',detalle:'Prueba empate ganado/perdido',fecha:new Date().toLocaleString()});} if(!(db.partidos||[]).some(p=>p.a===A.id&&p.b===B.id&&p.jornada==='2'))(db.partidos||(db.partidos=[])).push({id:id(),fecha:'2026-06-20T18:00',deporte:fut.id,rama:'Varonil',a:A.id,b:B.id,lugar:'Campo Universitario 1',jornada:'2',fase:'Regular',grupo:'A',arbitro:'Juan Pérez Árbitro',observaciones:'Partido pendiente de prueba',estado:'Programado'}); save('Creó datos de prueba FCCF vs FCA');}; // Dashboard sin bloque de datos de prueba en producción. // render final let oldRender=window.render|| (typeof render!=='undefined'?render:null); window.render=function(){try{ensureDeportesSeguro();}catch(e){}try{marcaSesion();ensureHeaderLogo(); if(typeof addToolsPage==='function')addToolsPage(); if(typeof renderNav==='function')renderNav(); let map={dashboard:window.dashboard,roles:window.roles||roles,resultados:window.resultados||resultados,estadisticas:window.estadisticas,tabla:window.tabla,reportes:window.reportes||reportes,protestas:window.protestas||protestas,sanciones:window.sanciones||sanciones,equipos:window.equipos,jugadores:window.jugadores,credenciales:window.credenciales,cotejo:window.cotejo||cotejo,cedulas_arbitro:window.cedulas_arbitro||cedulas_arbitro,deportes:window.deportes||deportes,facultades:window.facultades||facultades,usuarios:window.usuarios||usuarios,bitacora:window.bitacora||bitacora,avisos:window.avisos||avisos,herramientas:window.herramientas}; if(!map[page])page='dashboard'; app.innerHTML=map[page](); if(window.applyRoleChrome)applyRoleChrome(); marcaSesion();}catch(e){console.error(e); if(oldRender)return oldRender();}}; try{render=window.render;}catch(e){} let oldLogin=window.login||login; window.login=function(){let r=oldLogin(); setTimeout(()=>{marcaSesion();ensureHeaderLogo();},50); return r;}; try{login=window.login;}catch(e){} let oldLogout=window.logout||logout; window.logout=function(){let r=oldLogout(); setTimeout(()=>{marcaSesion();},50); return r;}; try{logout=window.logout;}catch(e){} })(); /* === FIN PARCHE FINAL CONSOLIDADO PRODUCCIÓN 20260605 === */ /* === PATCH INTEGRAL FINAL 2: WEB/MÓVIL + FLUJO EQUIPO-JUGADOR 20260605 === */ (function(){ function H(v){return String(v==null?'':v).replace(/[&<>"']/g,function(m){return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[m];});} function N(v){return String(v==null?'':v).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').trim();} function makeId(){try{return uid()}catch(e){return 'id_'+Date.now().toString(36)+Math.random().toString(36).slice(2,7)}} function by(a,x){return (a||[]).find(y=>String(y.id)===String(x));} function facLbl(x){try{let f=by(db.facultades,x); return f?(f.siglas||f.nombre):String(x||'No aplica');}catch(e){return String(x||'No aplica');}} function depLbl(x){try{let d=by(db.deportes,x); return d?(d.nombre||d.id):String(x||'');}catch(e){return String(x||'');}} function teamObj(x){return by(db.equipos,x)||null;} function teamLbl(x){let e=teamObj(x); if(!e)return String(x||''); return String(e.nombre||e.equipo||facLbl(e.facultad)||'Equipo').trim();} function okStatus(j){return ['aprobado','activo'].includes(N(j&&j.estatus));} function clsText(j){let v=(j&&(j.clasificado||j.jugadorClasificado||j.regla))||'No clasificado';return /^(si|sí|clasificado)$/i.test(String(v).trim())?'Clasificado':'No clasificado';} function fullName(j){return [j&&(j.nombres||j.nombreBase),j&&j.apellidoP,j&&j.apellidoM].filter(Boolean).join(' ').replace(/\s+/g,' ').trim()||(j&&j.nombre)||'';} function equipoDeJugador(j){return teamObj(j&&j.equipo)||(db.equipos||[]).find(e=>String(e.facultad)==String(j&&j.facultad)&&String(e.deporte)==String(j&&j.deporte));} function jugadorEnEquipo(j,eid){return String(j&&j.equipo)==String(eid)||(!j.equipo&&String(j&&j.facultad)==String((teamObj(eid)||{}).facultad));} function isCoachUser(){try{return typeof isCoach==='function'&&isCoach()}catch(e){return false}} function isAdminUser(){try{return typeof isAdmin==='function'&&isAdmin()}catch(e){return false}} function canEditUser(){try{return typeof canEdit==='function'&&canEdit()}catch(e){return false}} function equiposPermitidos2(){let arr=(db.equipos||[]).filter(e=>N(e.estatus)!=='inactivo'); if(isCoachUser()){arr=arr.filter(e=>String(e.creadoPor||'')===String(user&&user.usuario||'')||N(e.entrenador)===N(user&&user.nombre)||N(e.entrenadorUsuario)===N(user&&user.usuario)||(!e.creadoPor&&user&&user.facultad&&String(e.facultad)===String(user.facultad)));} return arr;} function grupoOpts2(sel){return ['A','B'].map(g=>``).join('');} function ramaOpts2(sel){return ['Varonil','Femenil','Mixto'].map(r=>``).join('');} function deporteOpts2(sel){return (db.deportes||[]).map(d=>``).join('');} function tipoOpts2(sel){return ['Facultad UACH','Invitado','Externo','Institución','Club'].map(t=>``).join('');} function facOpts2(sel, includeNo=true){return (includeNo?``:'')+(db.facultades||[]).map(f=>``).join('');} function equipoOptsByFac(sel,facu,onlyVisible=true){let arr=onlyVisible?equiposPermitidos2():(db.equipos||[]); if(facu)arr=arr.filter(e=>String(e.facultad||'')===String(facu)); return arr.map(e=>``).join('');} window.equiposPermitidos=equiposPermitidos2; window.teamName=teamLbl; window.fullPlayerName=fullName; try{db.grupos=['A','B'];}catch(e){} if(!document.getElementById('patchIntegralFinal2CSS')){let st=document.createElement('style');st.id='patchIntegralFinal2CSS';st.textContent=` body.sesion-activa aside .card.noPrint:first-child{display:none!important} .top{display:grid!important;grid-template-columns:auto 1fr auto!important;align-items:center!important;gap:18px!important;min-height:108px!important;padding:16px 28px!important;overflow:hidden!important}.top .uachHeaderLogo{height:78px;max-width:150px;object-fit:contain;background:rgba(255,255,255,.93);border-radius:12px;padding:4px}.top .topText{text-align:left;min-width:0}.top .topText h1{margin:0}.top .interLogo{height:78px!important;max-width:290px!important;object-fit:contain!important;background:rgba(255,255,255,.93);border-radius:14px;padding:5px;filter:none!important}.top p{white-space:normal!important}.top p,.top .oldRank{font-size:15px}.top p{display:block}.top p .oldRank{display:none!important} .uachMiniLogo{height:34px;width:auto;object-fit:contain;background:#fff;border-radius:6px;padding:2px;margin-right:5px}.credHead{gap:8px}.credHead .brandLine{display:flex;align-items:center;gap:6px}.credHead .interLogoCred{max-width:130px;max-height:42px;object-fit:contain;background:#fff;border-radius:8px;padding:3px}.qrBoxFinal{align-self:end}.sheetLogoPair{display:flex;align-items:center;gap:12px}.sheetLogoPair img{max-height:64px;max-width:230px;object-fit:contain}.mobile-user-card,.uach-mobile-user,.userMobile,.mobileSession{max-width:100%!important}.mobile-user-card .btn,.uach-mobile-user .btn,.userMobile .btn,.mobileSession .btn{white-space:normal!important;font-size:14px!important;padding:10px 12px!important}.tableWrap{overflow-x:auto}.compactFilters{display:grid;grid-template-columns:repeat(5,minmax(140px,1fr));gap:10px}.pos{font-weight:900;color:#6F3296}.teamSheetHeader{display:flex;align-items:center;justify-content:space-between;gap:14px;border-bottom:4px solid #D4A52A;padding-bottom:10px;margin-bottom:12px}.teamSheetHeader .logos{display:flex;gap:10px;align-items:center}.teamSheetHeader img{max-height:58px;max-width:220px;object-fit:contain}.credClass{display:inline-block;margin-top:2px;padding:2px 6px;border-radius:999px;background:#f3f4f6;font-weight:800;font-size:10px} @media(max-width:900px){.top{grid-template-columns:1fr!important;text-align:center!important;justify-items:center!important;padding:14px 12px!important}.top .topText{text-align:center!important}.top .uachHeaderLogo{height:68px}.top .interLogo{height:62px!important;max-width:230px!important}.grid,.grid2,.grid3{grid-template-columns:1fr!important}.compactFilters{grid-template-columns:1fr}.credBody.final{grid-template-columns:80px 1fr!important}.qrBoxFinal{grid-column:1/-1;justify-content:flex-end!important}.credGrid{grid-template-columns:1fr!important}} @media print{.top .uachHeaderLogo,.top .interLogo{background:#fff!important}.credNew{break-inside:avoid;page-break-inside:avoid}.credNew:nth-child(6n){page-break-after:always}.noPrint{display:none!important}} `;document.head.appendChild(st);} function fixSession(){document.body.classList.toggle('sesion-activa',!!(window.user||(typeof user!=='undefined'&&user)));} function fixHeader(){let top=document.querySelector('.top'); if(!top)return; let text=top.querySelector('.topText')?top.querySelector('.topText').innerHTML:top.innerHTML; text=text.replace(/ranking por facultad/gi,'ranking general'); top.innerHTML=`
${text}
`;} window.actualizarEquiposJugador=function(pref){let fac=document.getElementById(pref+'fac')?.value||'';let sel=document.getElementById(pref+'eq'); if(sel)sel.innerHTML=equipoOptsByFac(sel.value,fac,true)||'';}; window.equipos=function(){let q=N(window.eqSearch||'');let lista=equiposPermitidos2().filter(e=>!q||N([teamLbl(e.id),e.tipo,e.grupo,facLbl(e.facultad),depLbl(e.deporte),e.rama,e.entrenador,e.correo,e.telefono,e.estatus].join(' ')).includes(q));let puede=isAdminUser()||isCoachUser();let form=puede?`

Nuevo equipo

El equipo se registra por nombre. Puede ser UACH, invitado, externo, institución o club; no depende de una facultad.

`:'';let rows=lista.map(e=>`${H(teamLbl(e.id))}${H(e.tipo||'')}${H(facLbl(e.facultad))}${H(depLbl(e.deporte))}${H(e.rama||'')}${H(e.grupo||'')}${H(e.entrenador||'')}${H(e.correo||'')}
${H(e.telefono||'')}${H(e.estatus||'Activo')}${puede?` ${isAdminUser()?``:''}`:''}`).join('');return form+`

Equipos

${lista.length} visibles
${rows}
EquipoTipoFacultad/CentroDeporteRamaGrupoEntrenadorContactoEstatusAcciones
`;}; window.addEquipoFinal=function(){let obj={id:makeId(),nombre:(enombre.value||'').trim(),tipo:etipo.value,tipoEquipo:etipo.value,facultad:ef.value||'',procedencia:'',institucion:'',deporte:ed.value,rama:er.value,grupo:egrupo.value,entrenador:ee.value,correo:ec.value,telefono:et.value,estatus:'Activo',creadoPor:user&&user.usuario||''};if(!obj.nombre)return alert('Escribe el nombre del equipo.');(db.equipos||(db.equipos=[])).push(obj);save('Registró equipo');}; window.editEquipoFinal=function(eid){let e=teamObj(eid);if(!e)return;openM(`

Editar equipo

`);}; window.saveEquipoFinal=function(eid){let e=teamObj(eid);if(!e)return;Object.assign(e,{nombre:menombre.value,tipo:metipo.value,tipoEquipo:metipo.value,facultad:mef.value,procedencia:'',institucion:'',deporte:med.value,rama:mer.value,grupo:megrupo.value,entrenador:mee.value,correo:mec.value,telefono:met.value,estatus:mest.value});closeM();save('Editó equipo');};window.editEquipo=window.editEquipoFinal; window.jugadores=function(){let q=N(window.jugSearch||'');let base=(db.jugadores||[]).filter(j=>{if(isCoachUser())return equiposPermitidos2().some(e=>jugadorEnEquipo(j,e.id));return true;});let lista=base.filter(j=>!q||N([fullName(j),j.nombre,j.matricula,j.numero,teamLbl(j.equipo),facLbl(j.facultad),depLbl(j.deporte),j.carrera,j.semestre,j.telefono,j.correo,j.estatus,clsText(j)].join(' ')).includes(q));let puede=canEditUser()||isCoachUser()||isAdminUser();let initFac=user&&user.facultad||'';let form=puede?`

Nuevo jugador

Elige primero la facultad/centro; después selecciona el equipo previamente registrado.

`:'';let rows=lista.map(j=>{let e=equipoDeJugador(j)||{};return `${j.foto?``:`
FOTO
PENDIENTE
`}${H(fullName(j))}
${H(j.observaciones||'')}${H(j.matricula||'')}${H(j.numero||'')}${H(facLbl(j.facultad||e.facultad))}${H(teamLbl(j.equipo||e.id))}${H(depLbl(j.deporte||e.deporte))}${H(j.carrera||'')}
${H(j.semestre||'')}${H(clsText(j))}${H(j.telefono||'')}
${H(j.correo||'')}${H(j.estatus||'Pendiente')} ${isAdminUser()?``:''}`}).join('');return form+`

Jugadores

${lista.length} visibles
${rows}
FotoNombreMatrículaNo.Facultad/CentroEquipoDeporteCarreraClasificadoContactoEstatusAcciones
`}; window.addJugadorFinal=async function(){let e=teamObj(jeq.value);if(!e)return alert('Selecciona un equipo.');if(!jn.value||!jap.value||!jam.value||!jm.value)return alert('Completa nombre, apellidos y matrícula.');let foto='';if(jfoto.files&&jfoto.files[0])foto=await subirArchivo(jfoto.files[0]);let obj={id:makeId(),equipo:jeq.value,nombres:jn.value,apellidoP:jap.value,apellidoM:jam.value,nombre:[jn.value,jap.value,jam.value].join(' ').replace(/\s+/g,' ').trim(),matricula:jm.value,numero:jnum.value,facultad:e.facultad||jfac.value||'',deporte:e.deporte||'',rama:e.rama||'',grupo:e.grupo||'',carrera:jc.value,semestre:js.value,telefono:jt.value,correo:jco.value,clasificado:jclas.value,jugadorClasificado:jclas.value,estatus:isCoachUser()?'Pendiente':(jest.value||'Pendiente'),observaciones:jobs.value||'',foto,creadoPor:user&&user.usuario||''};(db.jugadores||(db.jugadores=[])).push(obj);save('Registró jugador');}; window.editJugadorFinal=function(jid){let j=by(db.jugadores,jid);if(!j)return;openM(`

Editar jugador

`);}; window.saveJugadorFinal=async function(jid){let j=by(db.jugadores,jid),e=teamObj(mjeq.value);if(!j||!e)return;Object.assign(j,{equipo:mjeq.value,nombres:mjn.value,apellidoP:mjap.value,apellidoM:mjam.value,nombre:[mjn.value,mjap.value,mjam.value].join(' ').replace(/\s+/g,' ').trim(),matricula:mjm.value,numero:mjnum.value,facultad:e.facultad||mjfac.value||'',deporte:e.deporte||'',rama:e.rama||'',grupo:e.grupo||'',carrera:mjc.value,semestre:mjs.value,telefono:mjt.value,correo:mjco.value,clasificado:mjclas.value,jugadorClasificado:mjclas.value,observaciones:mjobs.value});if(isAdminUser())j.estatus=mjest.value;else j.estatus='Pendiente';if(mjfoto.files&&mjfoto.files[0])j.foto=await subirArchivo(mjfoto.files[0]);closeM();save('Editó jugador');};window.editJugador=window.editJugadorFinal; function pseudoQR2(txt){return `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${encodeURIComponent(String(txt||''))}`;} window.credCard=function(j){let e=equipoDeJugador(j)||{};let est=j.estatus||'Pendiente';return `
Sistema Integral de Gestión Deportiva UACHCredencial oficial de jugador
Interfacultades
${j.foto?``:`
FOTO
PENDIENTE
`}

${H(fullName(j))}

Matrícula/ID: ${H(j.matricula||'')}

Equipo: ${H(teamLbl(j.equipo||e.id))}

Facultad/Centro: ${H(facLbl(j.facultad||e.facultad))}

Deporte: ${H(depLbl(j.deporte||e.deporte))}

Rama: ${H(j.rama||e.rama||'')}

Grupo: ${H(j.grupo||e.grupo||'')}

Número: ${H(j.numero||'')}

${H(clsText(j))}
QR
Folio: ${H(j.folioCotejo||('UACH-'+(j.matricula||j.id)))}${H(est)}
`;}; function credList2(){let eid=window.credEquipo||'';return (db.jugadores||[]).filter(j=>okStatus(j)&&(!eid||jugadorEnEquipo(j,eid)));} window.credenciales=function(){let eqs=equiposPermitidos2();let sel=window.credEquipo||eqs[0]?.id||'';let js=credList2();return `

Credenciales por equipo

Candado activo: solo jugadores aprobados aparecen en credenciales y cédulas.

Vista previa: ${H(sel?teamLbl(sel):'Todos')}

${js.length?`
${js.map(j=>credCard(j)).join('')}
`:'

Sin jugadores aprobados para imprimir.

'}
`;}; window.printDoc=function(title,html,orient='landscape'){let w=window.open('','_blank');w.document.write(`${H(title)} ${html} `);w.document.close();}; window.printCredenciales=function(){let lista=credList2();if(!lista.length)return alert('No hay jugadores aprobados para imprimir.');let html=`

Credenciales oficiales

Generado: ${new Date().toLocaleString()} · Total: ${lista.length}

${lista.map(j=>credCard(j)).join('')}
`;printDoc('credenciales_interfacultades',html,'landscape');}; window.imprimirCedulaEquipo=function(){let eid=window.credEquipo||'';if(!eid)return alert('Selecciona un equipo.');let e=teamObj(eid),lista=(db.jugadores||[]).filter(j=>jugadorEnEquipo(j,eid)&&okStatus(j));let html=`

Cédula oficial de equipo

${H(teamLbl(eid))} · ${H(facLbl(e.facultad))} · ${H(depLbl(e.deporte))} · ${H(e.rama||'')} · Grupo ${H(e.grupo||'')}

Solo jugadores aprobados · Total: ${lista.length}

${lista.map(j=>``).join('')}
No.JugadorMatrículaClasificadoEstatusFirma
${H(j.numero||'')}${H(fullName(j))}${H(j.matricula||'')}${H(clsText(j))}${H(j.estatus)}
`;printDoc('cedula_equipo_'+teamLbl(eid),html,'portrait');}; function filtrosCompact2(prefix){let deps=(db.deportes||[]).map(d=>``).join('');let jorn=[...new Set((db.partidos||[]).map(p=>p.jornada).filter(Boolean))];return `
`;} if(typeof window.estadisticas==='function'){let oldEst=window.estadisticas; window.estadisticas=function(){let f={dep:document.getElementById('estDep')?.value||'',rama:document.getElementById('estRama')?.value||'',grupo:document.getElementById('estGrupo')?.value||'',jornada:document.getElementById('estJornada')?.value||'',tipo:document.getElementById('estTipo')?.value||'tabla'};let tipo=f.tipo;let body=(tipo==='goleadores'&&window.tablaStatsJugadores)?window.tablaStatsJugadores(f):(tipo==='resultados'&&typeof tablaResultadosFiltro==='function')?tablaResultadosFiltro(f):(tipo==='pendientes'&&typeof tablaPartidos==='function')?`
${tablaPartidos((db.partidos||[]).filter(p=>p.estado!=='Finalizado'),false,false)}
`:(typeof tablaRanking==='function'?tablaRanking(f):'');return `

Estadísticas

${filtrosCompact2('est')}

${tipo==='goleadores'?'Goleadores / estadísticas individuales':tipo==='resultados'?'Resultados filtrados':tipo==='pendientes'?'Partidos pendientes':'Ranking general'}

${body||'

Sin información.

'}
`;};} if(typeof window.tabla==='function'){window.tabla=function(){let f={dep:document.getElementById('rankDep')?.value||'',rama:document.getElementById('rankRama')?.value||'',grupo:document.getElementById('rankGrupo')?.value||'',jornada:document.getElementById('rankJornada')?.value||'',tipo:document.getElementById('rankTipo')?.value||'tabla'};return `

Ranking general de equipos

${filtrosCompact2('rank')}

Regla fútbol: empate ganado = 2 puntos, empate perdido = 1 punto.

${typeof tablaRanking==='function'?tablaRanking(f):''}
`;};} if(typeof window.roles==='function'){let oldRoles2=window.roles;window.roles=function(){return oldRoles2().replace(/