Panel de entrenador

Aplicación de gestión para fútbol 11.

Equipos0
Jugadores0
Ejercicios0

Próximas actividades

🛡️ Versión estable

Esta versión utiliza un almacenamiento independiente para no verse afectada por los cambios de las versiones posteriores.

💾 Copia de seguridad

Guarda todos los datos de esta versión antes de hacer cambios o actualizar.

Incluye equipos, jugadores, ejercicios, sesiones, calendario, partidos y convocatorias.

Equipos

Gestiona todos tus equipos.

Jugadores

Plantillas y fichas básicas.

Base de ejercicios

Biblioteca con diseño visual, material, objetivos y categorías.

Biblioteca

0

Diseñador de ejercicios avanzado

Elige el espacio y dibuja pases, disparos, conducciones y movimientos.

Tipo de campo / espacio

Material

100%

Arrastra los elementos. Doble clic para eliminarlos.

Acciones y líneas

Haz clic en origen y después en destino para dibujar.

Campo de diseño

Datos del ejercicio

Sesiones

Crea y guarda sesiones de entrenamiento.

Datos de sesión

Ejercicios

Total: 0'

Calendario

Planificación semanal.

🏆 Partidos

Historial completo de partidos. Cada partido se guarda de forma independiente.

📚 Historial de partidos
0 partidos guardados

📱 Pantalla interactiva

Control táctil del partido en directo.

LOCAL
0 - 0
RIVAL
PREPARADO00:00
Toca un jugador y después un jugador del banquillo para hacer un cambio.
Indicadores: 🟡 32' amarilla · 🔴 78' roja · ⚽ 57' gol · A 57' asistencia · ↔ ↑63' / ↓81' cambio

📋 Eventos del partido

📊 Estadísticas de jugadores

Resumen de rendimiento a partir de los partidos registrados.

JugadorEquipoPJTit.Min.🤝🟨🟥
";w.document.open();w.document.write(html);w.document.close();w.focus();w.print()} let anchor=new Date(); function iso(d){return d.toISOString().slice(0,10)}function monday(d){let x=new Date(d),day=x.getDay();x.setDate(x.getDate()+(day===0?-6:1-day));x.setHours(0,0,0,0);return x} function renderCalendar(){let m=monday(anchor),names=["Lun","Mar","Mié","Jue","Vie","Sáb","Dom"],end=new Date(m);end.setDate(m.getDate()+6);$("weekTitle").textContent=`${m.getDate()}/${m.getMonth()+1} — ${end.getDate()}/${end.getMonth()+1}`;$("week").innerHTML="";for(let i=0;i<7;i++){let d=new Date(m);d.setDate(m.getDate()+i);let c=document.createElement("div");c.className="day";c.innerHTML=`
${names[i]} ${d.getDate()}/${d.getMonth()+1}
`;db.activities.filter(a=>a.date===iso(d)).forEach(a=>{let t=db.teams.find(x=>x.id===a.team),q=document.createElement("div");q.className="activity "+(a.type==="Partido"?"match":"");q.innerHTML=`${esc(a.type)}
${esc(a.time||"")} · ${a.duration||0}'
${esc(t?.name||"")}
${esc(a.goal||"")}`;c.appendChild(q)});$("week").appendChild(c)}} function weekMove(n){anchor.setDate(anchor.getDate()+n);renderCalendar()}function weekNow(){anchor=new Date();renderCalendar()} function newActivity(){if(!db.teams.length)return toast("Crea primero un equipo");openModal("Nueva actividad",`
`)} function createActivity(){db.activities.push({id:"a"+Date.now(),date:$("ad").value,type:$("ak").value,time:$("ah").value,duration:+$("au").value||0,team:$("at").value,goal:$("ag").value});save();closeModal();renderCalendar();toast("Actividad añadida")} function convocationOptions(teamId, selected){ const ids=new Set(selected||[]); const players=db.players.filter(p=>p.team===teamId); if(!players.length)return '
Este equipo todavía no tiene jugadores. Añade jugadores desde 👤 Jugadores.
'; return players.map(p=>``).join(""); } function refreshConvocation(teamId,selected){ const box=$("convocationBox");if(box)box.innerHTML=convocationOptions(teamId,selected||[]); const conv=selected||[]; if($("startersBox"))refreshLineups([],[]); } function selectedConvocation(){ return [...document.querySelectorAll('input[name="convPlayer"]:checked')].map(x=>x.value); } function playerById(id){return db.players.find(p=>p.id===id)} function lineupOptions(selected, name){ const ids=new Set(selected||[]); const conv=selectedConvocation(); const list=conv.map(id=>playerById(id)).filter(Boolean); if(!list.length)return '
Selecciona primero la convocatoria.
'; return list.map(p=>``).join(""); } function selectedLineup(name){return [...document.querySelectorAll('input[name="'+name+'"]:checked')].map(x=>x.value)} function substitutionPlayerOptions(teamId){ const players=db.players.filter(p=>p.team===teamId); return players.map(p=>``).join(""); } function addSubstitutionRow(outId="",inId="",minute=""){ const box=$("subsBox");if(!box)return; const row=document.createElement("div");row.className="sub-row"; row.innerHTML=` `; box.appendChild(row); if(outId)row.querySelector(".sub-out").value=outId; if(inId)row.querySelector(".sub-in").value=inId; } function selectedSubstitutions(){ return [...document.querySelectorAll(".sub-row")].map(row=>({ out:row.querySelector(".sub-out").value, in:row.querySelector(".sub-in").value, minute:+row.querySelector(".sub-minute").value||0 })).filter(x=>x.out&&x.in); } function renderSubstitutionRows(items=[]){ const box=$("subsBox");if(!box)return;box.innerHTML=""; (items||[]).forEach(x=>addSubstitutionRow(x.out,x.in,x.minute)); } function goalPlayerOptions(teamId){ return db.players.filter(p=>p.team===teamId).map(p=>``).join(""); } function addGoalRow(playerId="",minute="",assistId=""){ const box=$("goalsBox");if(!box)return; const row=document.createElement("div");row.className="goal-row"; const opts=goalPlayerOptions($("mt").value); row.innerHTML=` `; box.appendChild(row); if(playerId)row.querySelector(".goal-player").value=playerId; if(assistId)row.querySelector(".goal-assist").value=assistId; } function selectedGoals(){ return [...document.querySelectorAll(".goal-row")].map(row=>({ player:row.querySelector(".goal-player").value, assist:row.querySelector(".goal-assist").value, minute:+row.querySelector(".goal-minute").value||0 })).filter(x=>x.player); } function renderGoalRows(items=[]){ const box=$("goalsBox");if(!box)return;box.innerHTML=""; (items||[]).forEach(x=>addGoalRow(x.player,x.minute,x.assist)); } function cardPlayerOptions(teamId){ return db.players.filter(p=>p.team===teamId).map(p=>``).join(""); } function addCardRow(playerId="",type="Amarilla",minute=""){ const box=$("cardsBox");if(!box)return; const row=document.createElement("div");row.className="card-row"; row.innerHTML=` `; box.appendChild(row); if(playerId)row.querySelector(".card-player").value=playerId; row.querySelector(".card-type").value=type||"Amarilla"; } function selectedCards(){ return [...document.querySelectorAll(".card-row")].map(row=>({ player:row.querySelector(".card-player").value, type:row.querySelector(".card-type").value, minute:+row.querySelector(".card-minute").value||0 })).filter(x=>x.player); } function renderCardRows(items=[]){ const box=$("cardsBox");if(!box)return;box.innerHTML=""; (items||[]).forEach(x=>addCardRow(x.player,x.type,x.minute)); } function refreshLineups(starters=[],bench=[]){ const s=$("startersBox"),b=$("benchBox"); if(s)s.innerHTML=lineupOptions(starters,"starterPlayer"); if(b)b.innerHTML=lineupOptions(bench,"benchPlayer"); } function newMatch(){ if(!db.teams.length)return toast("Crea primero un equipo"); openModal("Nuevo partido",`
👕 Titulares
🪑 Banquillo
🔄 Sustituciones
⚽ Goles y goleadores
🟨🟥 Tarjetas
`); refreshConvocation($("mt").value,[]); refreshLineups([],[]); renderSubstitutionRows([]); renderGoalRows([]); renderCardRows([]); } function createMatch(){ const m={id:"m"+Date.now()+"_"+Math.random().toString(36).slice(2,7),date:$("md").value||today(),time:$("mh").value,team:$("mt").value,rival:$("mr").value,competition:$("mc").value,matchday:$("mj").value,venue:$("mv").value,stadium:$("mst").value,for:+$("mf").value||0,against:+$("ma").value||0,convocation:selectedConvocation(),starters:selectedLineup("starterPlayer"),bench:selectedLineup("benchPlayer"),substitutions:selectedSubstitutions(),goals:selectedGoals(),cards:selectedCards(),notes:$("mnotes").value}; db.matches.push(m); db.activities.push({id:"a"+Date.now(),date:m.date,type:"Partido",time:m.time,duration:0,team:m.team,goal:"vs "+m.rival,matchId:m.id}); save();closeModal();renderMatches();renderCalendar();toast("Partido guardado"); } function editBasicMatch(id){ const m=db.matches.find(x=>x.id===id);if(!m)return; openModal("Editar partido",`
👕 Titulares
🪑 Banquillo
🔄 Sustituciones
`); $("mt").value=m.team; refreshConvocation(m.team,m.convocation||[]); refreshLineups(m.starters||[],m.bench||[]); renderSubstitutionRows(m.substitutions||[]); renderGoalRows(m.goals||[]); renderCardRows(m.cards||[]); } function updateBasicMatch(id){ const i=db.matches.findIndex(x=>x.id===id);if(i<0)return; const m=db.matches[i]; Object.assign(m,{date:$("md").value||today(),time:$("mh").value,team:$("mt").value,rival:$("mr").value,competition:$("mc").value,matchday:$("mj").value,venue:$("mv").value,stadium:$("mst").value,for:+$("mf").value||0,against:+$("ma").value||0,convocation:selectedConvocation(),starters:selectedLineup("starterPlayer"),bench:selectedLineup("benchPlayer"),substitutions:selectedSubstitutions(),goals:selectedGoals(),notes:$("mnotes").value}); const a=db.activities.find(x=>x.matchId===id);if(a)Object.assign(a,{date:m.date,time:m.time,team:m.team,goal:"vs "+m.rival}); save();closeModal();renderMatches();renderCalendar();toast("Partido actualizado"); } function deleteBasicMatch(id){ askDelete("¿Eliminar este partido?",()=>{ db.matches=db.matches.filter(x=>x.id!==id); db.activities=db.activities.filter(x=>x.matchId!==id); save();renderMatches();renderCalendar();toast("Partido eliminado"); }); } function renderMatches(){ const box=$("matches");if(!box)return; const search=(($("matchSearch")?.value)||"").trim().toLowerCase(); const all=[...(db.matches||[])].sort((a,b)=>(b.date||"").localeCompare(a.date||"")||(b.id||"").localeCompare(a.id||"")); const filtered=all.filter(m=>{ if(!search)return true; const t=db.teams.find(x=>x.id===m.team); const text=[t?.name,m.rival,m.competition,m.stadium,m.venue,m.matchday,m.date].join(" ").toLowerCase(); return text.includes(search); }); const count=$("matchCount");if(count)count.textContent=`${all.length} partido${all.length===1?"":"s"} guardado${all.length===1?"":"s"}${search?` · ${filtered.length} mostrados`:""}`; if(!filtered.length){ box.innerHTML=search?'
🔎 No hay partidos que coincidan con la búsqueda.
': '
🏆 Todavía no hay partidos guardados.

'; return; } box.innerHTML=filtered.map((m,idx)=>{ const t=db.teams.find(x=>x.id===m.team); const conv=(m.convocation||[]).map(id=>db.players.find(p=>p.id===id)?.name||id); const status=m.liveState?.status==="finished"?"🏁 Finalizado":m.liveState?.status==="running"?"🔴 En directo":m.liveState?.status==="paused"?"⏸️ En pausa":"📋 Registrado"; return `
${esc(t?.name||"")} vs ${esc(m.rival||"")}
📅 ${esc(m.date||"—")} · 🕐 ${esc(m.time||"—")} · ${esc(m.competition||"Sin competición")} · ${esc(m.venue||"")} · ${status}
🏟️ ${esc(m.stadium||"Sin campo")} · Jornada ${esc(m.matchday||"—")}
${m.for||0}-${m.against||0}
📋 Convocatoria (${conv.length})
${esc(conv.join(", ")||"Sin jugadores convocados")}
👕 Titulares: ${esc((m.starters||[]).map(id=>db.players.find(p=>p.id===id)?.name||id).join(", ")||"No registrados")}
🪑 Banquillo: ${esc((m.bench||[]).map(id=>db.players.find(p=>p.id===id)?.name||id).join(", ")||"No registrados")}
🔄 Cambios: ${esc((m.substitutions||[]).map(x=>(db.players.find(p=>p.id===x.out)?.name||x.out)+" → "+(db.players.find(p=>p.id===x.in)?.name||x.in)+" · "+(x.minute||0)+"'").join(", ")||"No registrados")}
⚽ Goles: ${esc((m.goals||[]).map(x=>(db.players.find(p=>p.id===x.player)?.name||x.player)+" · "+(x.minute||0)+"'"+(x.assist?" (asist. "+(db.players.find(p=>p.id===x.assist)?.name||x.assist)+")":"")).join(", ")||"No registrados")}
🟨🟥 Tarjetas: ${esc((m.cards||[]).map(x=>(db.players.find(p=>p.id===x.player)?.name||x.player)+" · "+x.type+" · "+(x.minute||0)+"'").join(", ")||"No registradas")}
`; }).join(""); } function openInteractiveForMatch(id){ interactiveMatchId=id;nav("interactiva");setTimeout(()=>{const s=$("interactiveMatch");if(s){s.value=id;loadInteractiveMatch()}},0); } function dash(){$("dTeams").textContent=db.teams.length;$("dPlayers").textContent=db.players.length;$("dExercises").textContent=db.exercises.length;let a=db.activities.slice(-5).reverse();$("dActivities").innerHTML=a.length?a.map(x=>`
${esc(x.type)}
${esc(x.date)} · ${esc(x.goal||"")}
`).join(""):'
No hay actividades.
'} let deleteAction=null; function askDelete(message,action){deleteAction=action;$("confirmMessage").textContent=message;$("confirmModal").classList.add("show")} function finishDelete(ok){$("confirmModal").classList.remove("show");let a=deleteAction;deleteAction=null;if(ok&&a)a()} $("confirmDelete").addEventListener("click",()=>finishDelete(true)); $("confirmCancel").addEventListener("click",()=>finishDelete(false)); $("confirmClose").addEventListener("click",()=>finishDelete(false)); $("confirmModal").addEventListener("click",e=>{if(e.target===$("confirmModal"))finishDelete(false)}); let interactiveMatchId="",interactiveSelected=null,interactiveBenchSelected=null; const interactivePositions=[ [50,90],[25,74],[50,74],[75,74],[12,52],[35,52],[50,52],[65,52],[88,52],[40,30],[60,30] ]; function interactivePlayerName(id){const p=db.players.find(x=>x.id===id);return p?p.name:"Jugador";} function interactiveTeamPlayers(teamId){return db.players.filter(p=>p.team===teamId)} function renderInteractive(){ if(interactiveClockTimer && !document.getElementById("interactiva")?.classList.contains("active")){clearInterval(interactiveClockTimer);interactiveClockTimer=null;interactiveClockRunning=false} const sel=$("interactiveMatch");if(!sel)return; const matches=db.matches||[]; sel.innerHTML=matches.length?matches.map(m=>{const t=db.teams.find(x=>x.id===m.team);return ``}).join(""):""; if(interactiveMatchId && matches.some(m=>m.id===interactiveMatchId))sel.value=interactiveMatchId; else interactiveMatchId=sel.value||""; loadInteractiveMatch(); } function currentInteractiveMatch(){return (db.matches||[]).find(m=>m.id===interactiveMatchId)} function loadInteractiveMatch(){ const sel=$("interactiveMatch");interactiveMatchId=sel?.value||""; interactiveSelected=null;interactiveBenchSelected=null; const stats=$("interactivePlayerStats");if(stats)stats.style.display="none"; const m=currentInteractiveMatch(); if(!m){$("interactivePitch").innerHTML='
Crea un partido para usar la pantalla interactiva.
';$("interactiveBench").innerHTML="";$("interactiveScore").textContent="0 - 0";$("interactiveLog").innerHTML="";return} $("interactiveScore").textContent=`${m.for||0} - ${m.against||0}`; const team=db.teams.find(t=>t.id===m.team); $("interactiveHomeName").textContent=team?.name||"LOCAL"; $("interactiveAwayName").textContent=m.rival||"RIVAL"; renderInteractiveClock(m); const starters=(m.starters||[]).slice(0,11); const conv=(m.convocation||[]); const bench=(m.bench||[]).length?(m.bench||[]):conv.filter(id=>!starters.includes(id)); const pitch=$("interactivePitch");pitch.innerHTML='
'; starters.forEach((id,i)=>interactiveAddPlayer(pitch,id,i)); $("interactiveBench").innerHTML=bench.map(id=>{const p=db.players.find(x=>x.id===id);if(!p)return"";return ``}).join("")||'
No hay jugadores en el banquillo.
'; renderInteractiveLog(m); const st=$("interactiveStatus"); if(st)st.innerHTML=`👕 Titulares ${(m.starters||[]).length}🪑 Banquillo ${(m.bench||[]).length}🔄 Cambios ${(m.substitutions||[]).length}⚽ Goles ${(m.goals||[]).length}🟨🟥 Tarjetas ${(m.cards||[]).length}`; } function getInteractiveIncidents(m,id){ return { yellow:(m.cards||[]).filter(c=>c.player===id && c.type==="Amarilla").length, red:(m.cards||[]).filter(c=>c.player===id && (c.type==="Roja"||c.type==="Doble amarilla")).length, goals:(m.goals||[]).filter(g=>g.player===id).length, assists:(m.goals||[]).filter(g=>g.assist===id).length, subsIn:(m.substitutions||[]).filter(s=>s.in===id).length, subsOut:(m.substitutions||[]).filter(s=>s.out===id).length }; } function incidentBadgeHtml(m,id){ const a=[]; const yell=(m.cards||[]).filter(c=>c.player===id && c.type==="Amarilla"); const reds=(m.cards||[]).filter(c=>c.player===id && (c.type==="Roja"||c.type==="Doble amarilla")); const goals=(m.goals||[]).filter(g=>g.player===id); const assists=(m.goals||[]).filter(g=>g.assist===id); yell.forEach(c=>a.push(`● ${c.minute||0}'`)); reds.forEach(c=>a.push(`● ${c.minute||0}'`)); goals.forEach(g=>a.push(`⚽ ${g.minute||0}'`)); assists.forEach(g=>a.push(`A ${g.minute||0}'`)); const subsIn=(m.substitutions||[]).filter(s=>s.in===id),subsOut=(m.substitutions||[]).filter(s=>s.out===id); if(subsIn.length||subsOut.length){ const mins=[...subsIn.map(s=>`↑${s.minute||0}'`),...subsOut.map(s=>`↓${s.minute||0}'`)].join(" "); a.push(`↔ ${mins}`); } return a.length?`${a.join("")}`:""; } function interactiveAddPlayer(pitch,id,i){ const p=db.players.find(x=>x.id===id);if(!p)return; const [x,y]=interactivePositions[i]||[50,50];const el=document.createElement("button");el.className="interactive-player"+(i===0?" goalkeeper":"");el.dataset.player=id;el.style.left=x+"%";el.style.top=y+"%";el.innerHTML=`${esc(p.number||"—")}${esc(p.name||"Jugador")}${incidentBadgeHtml(currentInteractiveMatch(),id)}`;el.onclick=()=>interactiveSelectField(id);pitch.appendChild(el); } function interactiveSelectField(id){ interactiveSelected=id;interactiveBenchSelected=null;interactivePlayerStats(id); document.querySelectorAll(".interactive-player").forEach(x=>x.classList.toggle("selected",x.dataset.player===id)); document.querySelectorAll(".bench-touch").forEach(x=>x.classList.remove("selected")); $("interactiveHint").textContent=`Jugador seleccionado: ${interactivePlayerName(id)}. Ahora toca un jugador del banquillo para cambiarlo.`; } function interactiveSelectBench(id){ interactiveBenchSelected=id; document.querySelectorAll(".bench-touch").forEach(x=>x.classList.toggle("selected",x.dataset.iPlayer===id)); $("interactiveHint").textContent=`Banquillo seleccionado: ${interactivePlayerName(id)}. Pulsa CAMBIO para registrar la sustitución.`; } function interactiveChange(){ const m=currentInteractiveMatch();if(!m)return; if(!interactiveSelected||!interactiveBenchSelected)return toast("Selecciona un titular y un jugador del banquillo"); const out=interactiveSelected,inn=interactiveBenchSelected,minute=+$("interactiveMinute").value||0; const starters=m.starters||[],bench=m.bench||[]; const si=starters.indexOf(out),bi=bench.indexOf(inn); if(si<0)return toast("El jugador seleccionado no figura como titular"); if(bi<0)return toast("El jugador seleccionado no figura en el banquillo"); starters[si]=inn;bench[bi]=out;m.starters=starters;m.bench=bench; m.substitutions=m.substitutions||[];m.substitutions.push({out,in:inn,minute}); save();loadInteractiveMatch();toast("Cambio registrado"); } function categoryPlayerPickerHtml(ids,action){ const groups={}; ids.forEach(id=>{const p=db.players.find(x=>x.id===id);if(!p)return;const t=db.teams.find(x=>x.id===p.team);const cat=p.category||t?.category||"Sin categoría";(groups[cat]??=[]).push(p)}); return `
${Object.keys(groups).sort((a,b)=>a.localeCompare(b)).map(cat=>`
📂 ${esc(cat)} (${groups[cat].length})
${groups[cat].sort((a,b)=>(a.number||99)-(b.number||99)).map(p=>``).join("")}
`).join("")}
`; } function interactivePickPlayer(type){ const m=currentInteractiveMatch();if(!m)return; const ids=[...(m.convocation||[])]; openModal(type==="goal"?"⚽ Seleccionar goleador":type==="yellow"?"🟨 Seleccionar jugador":"🟥 Seleccionar jugador", categoryPlayerPickerHtml(ids,`interactiveRecordEvent('${type}',`)); } function interactiveEvent(type){ const m=currentInteractiveMatch();if(!m)return toast("Selecciona un partido"); if(m.liveState?.status==="running"){const live=interactiveCurrentMinute();$("interactiveMinute").value=live;} if(interactiveSelected){interactiveRecordEvent(type,interactiveSelected);return} interactivePickPlayer(type); } function interactiveCurrentMinute(){ const m=currentInteractiveMatch();if(!m)return 0; const st=m.liveState||{}; let elapsed=+st.elapsed||0; if(st.status==="running"&&st.startedAt) elapsed+=(Date.now()-st.startedAt)/1000; return Math.max(0,Math.min(130,Math.floor(elapsed/60))); } function interactiveRecordEvent(type,id){ const m=currentInteractiveMatch();if(!m)return; const minute=interactiveCurrentMinute(); if(type==="goal"){ m.for=(+m.for||0)+1;m.goals=m.goals||[];m.goals.push({player:id,assist:"",minute}); }else if(type==="yellow"||type==="red"){ m.cards=m.cards||[];m.cards.push({player:id,type:type==="yellow"?"Amarilla":"Roja",minute}); }else{ m.events=m.events||[];m.events.push({type:"Evento",player:id,minute}); } save();closeModal();loadInteractiveMatch();toast(type==="goal"?"Gol registrado":type==="yellow"?"Tarjeta amarilla registrada":type==="red"?"Tarjeta roja registrada":"Evento registrado"); } let interactiveClockTimer=null,interactiveClockRunning=false,interactiveClockLastMs=null; function interactiveClockState(m){ if(!m.liveState)m.liveState={status:"stopped",elapsed:0,minute:0}; return m.liveState; } function formatClock(sec){ sec=Math.max(0,Math.floor(sec||0));return String(Math.floor(sec/60)).padStart(2,"0")+":"+String(sec%60).padStart(2,"0"); } function interactiveSyncClock(){ const m=currentInteractiveMatch();if(!m)return; const st=interactiveClockState(m); if(st.status==="running" && st.startedAt){ st.elapsed=(st.elapsed||0)+Math.max(0,(Date.now()-st.startedAt)/1000); st.startedAt=Date.now(); } const total=Math.floor(st.elapsed||0); const minute=Math.min(130,Math.floor(total/60)); st.minute=minute; $("interactiveMinute").value=minute; const d=$("interactiveClockDisplay");if(d)d.textContent=formatClock(total); } function interactiveStart(){ const m=currentInteractiveMatch();if(!m)return toast("Selecciona un partido"); const st=interactiveClockState(m); if(st.status==="finished")return toast("El partido ya está finalizado"); st.status="running";st.startedAt=Date.now();interactiveClockRunning=true; if(interactiveClockTimer)clearInterval(interactiveClockTimer); interactiveClockTimer=setInterval(()=>{interactiveSyncClock();renderInteractiveClock(m)},250); save();renderInteractiveClock(m);toast("Cronómetro iniciado"); } function interactivePause(){ const m=currentInteractiveMatch();if(!m)return; const st=interactiveClockState(m); if(st.status!=="running")return toast("El cronómetro no está en marcha"); interactiveSyncClock();st.status="paused";st.startedAt=null;interactiveClockRunning=false; if(interactiveClockTimer){clearInterval(interactiveClockTimer);interactiveClockTimer=null} save();renderInteractiveClock(m);toast("Cronómetro en pausa"); } function interactiveResetClock(){ const m=currentInteractiveMatch();if(!m)return; const st=interactiveClockState(m); if(st.status==="running")return toast("Pausa el cronómetro antes de ponerlo a cero"); if(!confirm("¿Poner el cronómetro a 00:00?"))return; st.status="stopped";st.elapsed=0;st.minute=0;st.startedAt=null; interactiveClockRunning=false; if(interactiveClockTimer){clearInterval(interactiveClockTimer);interactiveClockTimer=null} save();renderInteractiveClock(m);toast("Cronómetro puesto a cero"); } function interactiveFinish(){ const m=currentInteractiveMatch();if(!m)return; const st=interactiveClockState(m); if(st.status==="running")interactiveSyncClock(); st.status="finished";st.startedAt=null;interactiveClockRunning=false; if(interactiveClockTimer){clearInterval(interactiveClockTimer);interactiveClockTimer=null} save();renderInteractiveClock(m);interactiveShowFinish(m); } function renderInteractiveClock(m){ const st=interactiveClockState(m),state=st.status||"stopped"; const labels={running:"EN DIRECTO",paused:"EN PAUSA",finished:"FINAL",stopped:"PREPARADO"}; const el=$("interactiveClockState");if(el)el.textContent=labels[state]||"PREPARADO"; const display=$("interactiveClockDisplay");if(display)display.textContent=formatClock(st.elapsed||0); const min=$("interactiveMinute");if(min)min.value=Math.min(130,Math.floor((st.elapsed||0)/60)); } function interactiveSetMinute(value){ const m=currentInteractiveMatch();if(!m)return; const st=interactiveClockState(m); if(st.status==="running"){interactiveSyncClock();return} const min=Math.max(0,Math.min(130,+value||0));st.minute=min;st.elapsed=min*60;renderInteractiveClock(m);save(); } function changeInteractiveMinute(delta){ const m=currentInteractiveMatch();if(!m)return; const st=interactiveClockState(m); if(st.status==="running")return toast("Pausa el cronómetro para ajustar el minuto"); st.minute=Math.max(0,Math.min(130,(st.minute||0)+delta)); st.elapsed=st.minute*60; const min=$("interactiveMinute");if(min)min.value=st.minute; renderInteractiveClock(m);save(); } function interactivePlayerStats(id){ const m=currentInteractiveMatch();if(!m)return; const goals=(m.goals||[]).filter(g=>g.player===id).length,assists=(m.goals||[]).filter(g=>g.assist===id).length; const cards=(m.cards||[]).filter(c=>c.player===id); const starters=(m.starters||[]).includes(id),subsIn=(m.substitutions||[]).filter(s=>s.in===id),subsOut=(m.substitutions||[]).filter(s=>s.out===id); const end=+$("interactiveMinute").value||90; const start=subsIn.length?Math.min(...subsIn.map(s=>+s.minute||0)):0; const out=subsOut.length?Math.min(...subsOut.map(s=>+s.minute||end)):end; const minutes=Math.max(0,out-start); const box=$("interactivePlayerStats");if(!box)return; box.style.display="grid";box.innerHTML=`
${minutes}'MINUTOS
${goals}GOLES
${assists}ASIST.
${cards.length}TARJETAS
`; } function changeInteractiveMinute(delta){ const el=$("interactiveMinute");if(!el)return; el.value=Math.max(0,Math.min(130,(+el.value||0)+delta)); } function interactiveClearSelection(){ interactiveSelected=null;interactiveBenchSelected=null; document.querySelectorAll(".interactive-player").forEach(x=>x.classList.remove("selected")); document.querySelectorAll(".bench-touch").forEach(x=>x.classList.remove("selected")); $("interactiveHint").textContent="Toca un jugador del campo para seleccionarlo."; } function interactiveQuickGoalAgainst(){ const m=currentInteractiveMatch();if(!m)return; m.against=(+m.against||0)+1;m.events=m.events||[]; m.events.push({type:"Gol rival",minute:+$("interactiveMinute").value||0,text:"Gol rival"}); save();loadInteractiveMatch();toast("Gol rival registrado"); } function interactiveUndoLast(){ const m=currentInteractiveMatch();if(!m)return; const events=[]; (m.goals||[]).forEach((x,i)=>events.push({kind:"goal",idx:i,minute:x.minute||0})); (m.cards||[]).forEach((x,i)=>events.push({kind:"card",idx:i,minute:x.minute||0})); (m.substitutions||[]).forEach((x,i)=>events.push({kind:"sub",idx:i,minute:x.minute||0})); (m.events||[]).forEach((x,i)=>events.push({kind:"event",idx:i,minute:x.minute||0})); if(!events.length)return toast("No hay eventos para deshacer"); const last=events[events.length-1]; if(last.kind==="goal"){m.goals.splice(last.idx,1);m.for=Math.max(0,(+m.for||0)-1)} else if(last.kind==="card")m.cards.splice(last.idx,1); else if(last.kind==="sub")m.substitutions.splice(last.idx,1); else {const ev=m.events[last.idx];if(ev&&ev.text==="Gol rival")m.against=Math.max(0,(+m.against||0)-1);m.events.splice(last.idx,1)} save();loadInteractiveMatch();toast("Último evento deshecho"); } function interactiveRefreshStats(){save();loadInteractiveMatch();toast("Datos actualizados")} function renderInteractiveLog(m){ const items=[]; (m.goals||[]).forEach(g=>items.push({minute:g.minute||0,text:`⚽ ${interactivePlayerName(g.player)}`})); (m.cards||[]).forEach(c=>items.push({minute:c.minute||0,text:`${c.type==="Amarilla"?"🟨":"🟥"} ${interactivePlayerName(c.player)}`})); (m.substitutions||[]).forEach(s=>items.push({minute:s.minute||0,text:`🔄 ${interactivePlayerName(s.out)} → ${interactivePlayerName(s.in)}`})); (m.events||[]).forEach(e=>items.push({minute:e.minute||0,text:`📋 ${e.text||"Evento"}${e.player?" · "+interactivePlayerName(e.player):""}`})); $("interactiveLog").innerHTML=items.length?items.sort((a,b)=>a.minute-b.minute).map(x=>`
${x.minute}'${esc(x.text)}
`).join(""):'
Todavía no hay eventos registrados.
'; } function getMatchPlayerMinutes(m,playerId){ const starters=new Set(m.starters||[]); const bench=new Set(m.bench||[]); const conv=new Set(m.convocation||[]); const relevant=starters.has(playerId)||bench.has(playerId)||conv.has(playerId)|| (m.substitutions||[]).some(s=>s.in===playerId||s.out===playerId); if(!relevant)return 0; const st=m.liveState||{}; let endMinute; if(st.elapsed!=null && +st.elapsed>0) endMinute=Math.floor(+st.elapsed/60); else if(st.minute!=null && +st.minute>0) endMinute=+st.minute; else endMinute=90; const ins=(m.substitutions||[]).filter(s=>s.in===playerId).map(s=>Math.max(0,+s.minute||0)); const outs=(m.substitutions||[]).filter(s=>s.out===playerId).map(s=>Math.max(0,+s.minute||0)); let startMinute=starters.has(playerId)?0:(ins.length?Math.min(...ins):null); if(startMinute===null)return 0; let finishMinute=outs.length?Math.min(...outs):endMinute; finishMinute=Math.max(startMinute,Math.min(endMinute,finishMinute)); return Math.max(0,finishMinute-startMinute); } function playerStatsRow(p){ const s=getPlayerMatchStats(p.id); const starts=(db.matches||[]).filter(m=>(m.starters||[]).includes(p.id)).length; const team=db.teams.find(t=>t.id===p.team); return {...s,starts,team:team?.name||"Sin equipo",name:p.name||"Sin nombre",id:p.id}; } function renderPlayerStats(){ const teamSel=$("statsTeam");if(!teamSel)return; const current=teamSel.value; teamSel.innerHTML=''+db.teams.map(t=>``).join(""); teamSel.value=current; const team=teamSel.value,search=(($("statsSearch")?.value)||"").toLowerCase().trim(),sort=$("statsSort")?.value||"minutes"; let rows=db.players.map(playerStatsRow).filter(r=>(!team||db.players.find(p=>p.id===r.id)?.team===team)&&(!search||r.name.toLowerCase().includes(search))); rows.sort((a,b)=>sort==="name"?a.name.localeCompare(b.name):(b[sort]||0)-(a[sort]||0)); const totals=rows.reduce((a,r)=>{a.pj+=r.appearances;a.min+=r.minutes;a.goles+=r.goals;a.assists+=r.assists;a.yellow+=r.yellow;a.red+=r.red;return a},{pj:0,min:0,goles:0,assists:0,yellow:0,red:0}); $("statsSummary").innerHTML=`
${rows.length}JUGADORES
${totals.pj}PARTICIPACIONES
${totals.min}'MINUTOS
${totals.goles}GOLES
${totals.assists}ASISTENCIAS
`; $("playerStatsBody").innerHTML=rows.length?rows.map(r=>`${esc(r.name)}${esc(r.team)}${r.appearances}${r.starts}${r.minutes}'${r.goals}${r.assists}${r.yellow}${r.red}`).join(""):`No hay datos que mostrar.`; } function renderAll(){dash();renderTeams();renderPlayers();renderExercises();renderSession();renderCalendar();renderMatches();if(typeof renderInteractive==="function")renderInteractive();if(typeof renderPlayerStats==="function")renderPlayerStats()} renderAll(); let advancedTool="pass",advancedField="full",advancedStart=null; function paintPitch(target){ const f=target||$("field");if(!f)return; f.querySelectorAll(".pitch-svg,.pitch-label").forEach(x=>x.remove()); const ns="http://www.w3.org/2000/svg"; const svg=document.createElementNS(ns,"svg");svg.setAttribute("class","pitch-svg");svg.setAttribute("viewBox","0 0 1000 600");svg.setAttribute("preserveAspectRatio","none"); const el=(tag,attrs={})=>{const x=document.createElementNS(ns,tag);Object.entries(attrs).forEach(([k,v])=>x.setAttribute(k,v));svg.appendChild(x);return x}; const line=(x1,y1,x2,y2,c={})=>el("line",{x1,y1,x2,y2,...c}); const rect=(x,y,w,h,c={})=>el("rect",{x,y,width:w,height:h,...c}); const circle=(cx,cy,r,c={})=>el("circle",{cx,cy,r,...c}); const path=(d,c={})=>el("path",{d,...c}); // Outer pitch and halfway line. rect(10,10,980,580); line(500,10,500,590); circle(500,300,75); circle(500,300,4,{class:"spot"}); // Penalty areas, six-yard boxes, penalty spots and penalty arcs. rect(10,190,165,220);rect(10,245,55,110); rect(825,190,165,220);rect(935,245,55,110); circle(115,300,4,{class:"spot"});circle(885,300,4,{class:"spot"}); path("M 175 245 A 75 75 0 0 1 175 355");path("M 825 245 A 75 75 0 0 0 825 355"); // Goals. rect(0,255,10,90,{class:"goal"});rect(990,255,10,90,{class:"goal"}); // Corner arcs. path("M 10 28 A 18 18 0 0 0 28 10");path("M 10 572 A 18 18 0 0 1 28 590"); path("M 990 28 A 18 18 0 0 1 972 10");path("M 990 572 A 18 18 0 0 0 972 590"); f.appendChild(svg); // Space-specific overlays. const type=f.className.match(/\b(full|half|quarter|final|reduced|small)\b/)?.[1]||"full"; if(type==="half"){ // Cover one half visually, leaving a clearly marked half-pitch. const ov=document.createElementNS(ns,"svg");ov.setAttribute("class","pitch-svg");ov.setAttribute("viewBox","0 0 1000 600");ov.setAttribute("preserveAspectRatio","none"); const r=document.createElementNS(ns,"rect");r.setAttribute("x","0");r.setAttribute("y","0");r.setAttribute("width","500");r.setAttribute("height","600");r.setAttribute("fill","#218c43");r.setAttribute("stroke","none");r.setAttribute("opacity",".0");ov.appendChild(r); f.appendChild(ov); const lab=document.createElement("div");lab.className="pitch-label";lab.style.left="3%";lab.style.top="5%";lab.textContent="MEDIO CAMPO";f.appendChild(lab); }else if(type==="quarter"){ const lab=document.createElement("div");lab.className="pitch-label";lab.style.left="4%";lab.style.top="5%";lab.textContent="1/4 DE CAMPO";f.appendChild(lab); }else if(type==="final"){ const lab=document.createElement("div");lab.className="pitch-label";lab.style.right="4%";lab.style.top="5%";lab.textContent="ÚLTIMO TERCIO";f.appendChild(lab); }else if(type==="reduced"||type==="small"){ const box=document.createElementNS(ns,"svg");box.setAttribute("class","pitch-svg");box.setAttribute("viewBox","0 0 1000 600");box.setAttribute("preserveAspectRatio","none"); const x=type==="reduced"?140:220,y=type==="reduced"?105:150,w=type==="reduced"?720:560,h=type==="reduced"?390:300; const r=document.createElementNS(ns,"rect");r.setAttribute("x",x);r.setAttribute("y",y);r.setAttribute("width",w);r.setAttribute("height",h);r.setAttribute("stroke","#fff");r.setAttribute("stroke-width","3");r.setAttribute("fill","none");box.appendChild(r); const l=document.createElementNS(ns,"line");l.setAttribute("x1",x+w/2);l.setAttribute("y1",y);l.setAttribute("x2",x+w/2);l.setAttribute("y2",y+h);l.setAttribute("stroke","#fff");l.setAttribute("stroke-width","2");box.appendChild(l); const c=document.createElementNS(ns,"circle");c.setAttribute("cx",x+w/2);c.setAttribute("cy",y+h/2);c.setAttribute("r",type==="reduced"?55:45);c.setAttribute("stroke","#fff");c.setAttribute("fill","none");box.appendChild(c); f.appendChild(box); } } function paintPitch(){ const f=$("field");if(!f)return;f.querySelectorAll(".pitch-mark").forEach(x=>x.remove()); ["pitch-center","pitch-circle","pitch-box-left","pitch-box-right","pitch-goal-left","pitch-goal-right"].forEach(c=>{let d=document.createElement("div");d.className=c+" pitch-mark";f.appendChild(d)}); } function chooseField(type,btn){advancedField=type;$("field").className="field board "+type;document.querySelectorAll("[data-field-type]").forEach(b=>b.classList.toggle("selected",b===btn));paintPitch($("field"))} function chooseTool(type,btn){advancedTool=type;advancedStart=null;document.querySelectorAll("[data-tool]").forEach(b=>b.classList.toggle("selected",b===btn))} let playerVariant=0,selectedPiece=null; function selectPiece(p){ if(selectedPiece)selectedPiece.classList.remove("selected-piece"); selectedPiece=p; if(p){ p.classList.add("selected-piece"); if($("pieceSizeRange"))$("pieceSizeRange").value=p.dataset.size||100; if($("pieceSizeValue"))$("pieceSizeValue").textContent=(p.dataset.size||100)+"%"; } } function resizeSelectedPiece(value){ if(!selectedPiece)return; const v=Math.max(40,Math.min(220,+value||100)); selectedPiece.dataset.size=String(v); selectedPiece.style.transform=`translate(-50%,-50%) scale(${v/100})`; if($("pieceSizeValue"))$("pieceSizeValue").textContent=v+"%"; } function setSelectedPieceSize(v){ if($("pieceSizeRange"))$("pieceSizeRange").value=v; resizeSelectedPiece(v); } function advancedPiece(type,label,color){ let p=document.createElement("div");p.className="piece "+type;p.dataset.type=type;p.dataset.label=label;p.dataset.size="100"; const icons={ball:"⚽",cone:"🔺",pole:"📍",hurdle:"▱",hoop:"⭕",ladder:"🪜",goal:"🥅",mannequin:"🧍",flag:"🚩",bib:"🟢",mini:"▣",zone:"⬚"}; if(type==="player"||type==="rival"){ const vars=type==="player"?["blue","yellow","green","orange"]:["red","black","purple"]; color=color||vars[playerVariant%vars.length]; p.classList.add(type==="player"?"player-piece":"rival-piece",color); p.textContent=String(playerVariant%11+1); p.title=label; playerVariant++; }else p.textContent=icons[type]||""; let n=$("field").querySelectorAll(".piece").length; p.style.left=(10+(n*12)%82)+"%";p.style.top=(12+(n*15)%76)+"%"; p.style.transform="translate(-50%,-50%)"; p.addEventListener("pointerdown",function(e){ e.preventDefault();e.stopPropagation();selectPiece(p); const field=$("field"),r=field.getBoundingClientRect(); p.setPointerCapture?.(e.pointerId); p._drag={id:e.pointerId,dx:e.clientX-r.left-p.offsetLeft,dy:e.clientY-r.top-p.offsetTop}; p.classList.add("dragging-piece"); }); p.addEventListener("pointermove",function(e){ if(!p._drag || p._drag.id!==e.pointerId)return; e.preventDefault();e.stopPropagation(); const field=$("field"),r=field.getBoundingClientRect(); let x=e.clientX-r.left-p._drag.dx, y=e.clientY-r.top-p._drag.dy; const halfW=p.offsetWidth/2,halfH=p.offsetHeight/2; x=Math.max(halfW,Math.min(r.width-halfW,x));y=Math.max(halfH,Math.min(r.height-halfH,y)); p.style.left=(x/r.width*100)+"%";p.style.top=(y/r.height*100)+"%"; }); p.addEventListener("pointerup",function(e){ if(p._drag && p._drag.id===e.pointerId){ p.releasePointerCapture?.(e.pointerId);p._drag=null;p.classList.remove("dragging-piece"); } }); p.addEventListener("pointercancel",function(){p._drag=null;p.classList.remove("dragging-piece")}); p.addEventListener("dblclick",function(e){e.stopPropagation();if(selectedPiece===p)selectedPiece=null;p.remove()}); $("field").appendChild(p);selectPiece(p); } function clearAdvancedField(){$("field").querySelectorAll(".piece,.board-line").forEach(x=>x.remove());advancedStart=null} function saveAdvancedDesign(){let pieces=[...$("field").querySelectorAll(".piece")].map(p=>({type:p.dataset.type,label:p.dataset.label,left:p.style.left,top:p.style.top,size:+p.dataset.size||100,anim:p.dataset.anim||""}));let lines=[...$("field").querySelectorAll(".board-line")].map(p=>({type:["pass","shoot","dribble","run","defend"].find(x=>p.classList.contains(x)),left:p.style.left,top:p.style.top,width:p.style.width,transform:p.style.transform}));let material=pieces.filter(p=>!["player","rival","ball"].includes(p.type)).map(p=>p.label);db.exercises.push({id:"e"+Date.now(),name:$("designName").value||"Ejercicio diseñado",category:$("designCat").value,time:+$("designTime").value||15,players:+$("designPlayers").value||1,goal:$("designGoal").value,notes:$("designNotes").value,space:$("designSpace").value,fieldType:advancedField,material:[...new Set(material)],pieces,lines});save();renderExercises();toast("Ejercicio guardado con campo y acciones")} if(document.getElementById('field')){paintPitch(document.getElementById('field'));} try{renderExercises();}catch(e){console.error(e);} document.addEventListener("keydown",function(e){ if(!selectedPiece)return; if(e.key==="+"||e.key==="="){e.preventDefault();setSelectedPieceSize(Math.min(220,(+(selectedPiece.dataset.size||100))+10))} if(e.key==="-"||e.key==="_"){e.preventDefault();setSelectedPieceSize(Math.max(40,(+(selectedPiece.dataset.size||100))-10))} }); (function(){ function initMobile(){ const btn=document.getElementById("mobileMenuBtn"), side=document.querySelector(".side"), ov=document.getElementById("mobileOverlay"); if(!btn||!side)return; function close(){side.classList.remove("mobile-open");if(ov)ov.classList.remove("show")} btn.onclick=function(){side.classList.toggle("mobile-open");if(ov)ov.classList.toggle("show")}; if(ov)ov.onclick=close; side.addEventListener("click",function(e){if(e.target.closest("a,button")){setTimeout(close,80)}}); window.addEventListener("resize",function(){if(window.innerWidth>600)close()}); } if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",initMobile);else initMobile(); })(); (function(){ function bootMobile(){ var btn=document.getElementById("mobileMenuBtn"); var side=document.querySelector(".side"); var ov=document.getElementById("mobileOverlay"); if(!btn||!side)return; function closeMenu(){side.classList.remove("mobile-open");if(ov)ov.classList.remove("show")} btn.onclick=function(e){e.preventDefault();e.stopPropagation();side.classList.toggle("mobile-open");if(ov)ov.classList.toggle("show")}; if(ov)ov.onclick=closeMenu; side.addEventListener("click",function(e){ var target=e.target.closest("button"); if(target && target.closest(".nav")) setTimeout(closeMenu,100); }); window.addEventListener("resize",function(){if(window.innerWidth>600)closeMenu()}); } if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",bootMobile);else bootMobile(); })(); (function(){ function mobileBottom(){ var nav=document.getElementById("mobileBottomNav");if(!nav)return; nav.querySelectorAll("button[data-page]").forEach(function(b){ b.addEventListener("click",function(){ var target=document.querySelector('.nav button[data-page="'+b.dataset.page+'"]'); if(target)target.click(); nav.querySelectorAll("button").forEach(function(x){x.classList.remove("active")}); b.classList.add("active"); window.scrollTo(0,0); }); }); document.addEventListener("click",function(e){ var p=e.target.closest(".nav button[data-page]"); if(!p)return; var b=nav.querySelector('button[data-page="'+p.dataset.page+'"]'); if(b){nav.querySelectorAll("button").forEach(function(x){x.classList.remove("active")});b.classList.add("active")} }); } if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",mobileBottom);else mobileBottom(); })(); function resetStableData(){ if(confirm("Esto reiniciará únicamente los datos de esta versión estable. Los datos de versiones posteriores no se borran.")){ localStorage.removeItem("fcm_stable_v67");location.reload(); } } function backupData(){ try{ const payload={app:"Football Coach Manager",version:"6.7.2",exportedAt:new Date().toISOString(),data:db}; const blob=new Blob([JSON.stringify(payload,null,2)],{type:"application/json"}); const url=URL.createObjectURL(blob),a=document.createElement("a"); a.href=url;a.download="football-coach-manager-backup-"+new Date().toISOString().slice(0,10)+".json"; document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000); toast("Copia de seguridad creada"); }catch(e){console.error(e);toast("No se pudo crear la copia")} } function restoreData(event){ const file=event.target.files&&event.target.files[0];if(!file)return; const reader=new FileReader(); reader.onload=function(){ try{ const raw=JSON.parse(reader.result),incoming=raw&&raw.data?raw.data:raw; const keys=["teams","players","exercises","sessions","activities","matches"]; if(!incoming||!keys.some(k=>Array.isArray(incoming[k])))throw new Error("Formato"); if(!confirm("Restaurar esta copia sustituirá los datos actuales de esta versión. ¿Continuar?"))return; keys.forEach(k=>{if(Array.isArray(incoming[k]))db[k]=incoming[k]}); localStorage.setItem("fcm_stable_v67",JSON.stringify(db)); renderAll();toast("Copia restaurada correctamente"); }catch(e){console.error(e);toast("El archivo no es una copia válida")} event.target.value=""; }; reader.readAsText(file); }