// Tormentas — Pantallas administrativas // ── PAGOS ───────────────────────────────────────────────── function AdminPagos() { const { players } = window.TData; const { payments: init, MONTHLY_FEE, paymentMonths, monthLabels } = window.TAdmin; const [selMonth, setSelMonth] = React.useState(paymentMonths[paymentMonths.length - 1]); const [payments, setPayments] = React.useState(() => { try { const s = localStorage.getItem("tormentas_admin_payments"); return s ? JSON.parse(s) : init; } catch { return init; } }); const toggle = (pid, month) => { setPayments(prev => { const next = { ...prev, [pid]: prev[pid].map(p => p.month !== month ? p : { ...p, paid: !p.paid, date: !p.paid ? new Date().toISOString().split("T")[0] : null, method: !p.paid ? "Efectivo" : null, }) }; localStorage.setItem("tormentas_admin_payments", JSON.stringify(next)); return next; }); }; const curIdx = paymentMonths.indexOf(selMonth); const lastIdx = paymentMonths.length - 1; const rows = players.map(p => ({ player:p, pay: payments[p.id]?.find(x => x.month === selMonth) })); const paid = rows.filter(r => r.pay?.paid).length; const pending = rows.length - paid; const totalPaid = paid * MONTHLY_FEE; const totalPend = pending * MONTHLY_FEE; // Season totals const seasonPaid = players.reduce((s, p) => s + (payments[p.id]?.filter(x => x.paid).length || 0) * MONTHLY_FEE, 0); return (

Pagos mensuales

Cuota: ${MONTHLY_FEE} MXN / jugadora / mes

0?T.red:T.green} sub={`${pending} jugadoras`} /> =0.8?T.green:T.yellow} />
{/* Month tabs */}
{paymentMonths.map(m => ( ))}
{ const isPaid = !!pay?.paid; const overdue = !isPaid && curIdx < lastIdx; const statusColor = isPaid ? T.green : overdue ? T.red : T.yellow; const statusBg = isPaid ? T.greenLt : overdue ? T.redLt : T.yellowLt; const statusBdr = isPaid ? "#bbf7d0" : overdue ? "#fecaca" : "#fde68a"; const statusLabel = isPaid ? "Pagado" : overdue ? "Vencido" : "Pendiente"; return { player: (
{p.name}
), pos: , amount: `$${MONTHLY_FEE}`, status: ( {statusLabel} ), date: pay?.date || , method: pay?.method || , action: ( ), }; })} />
); } // ── REGISTRO ────────────────────────────────────────────── function AdminRegistro() { const { players } = window.TData; const { contacts } = window.TAdmin; const [search, setSearch] = React.useState(""); const [expanded, setExpanded] = React.useState(null); const calcAge = dob => { const d = new Date(dob), now = new Date("2026-04-23"); return now.getFullYear() - d.getFullYear() - (now < new Date(now.getFullYear(), d.getMonth(), d.getDate()) ? 1 : 0); }; const filtered = players.filter(p => p.name.toLowerCase().includes(search.toLowerCase()) || String(p.num).includes(search) ); return (

Registro de jugadoras

Información de contacto y datos personales

setSearch(e.target.value)} placeholder="Buscar..." style={{padding:"7px 12px 7px 32px",borderRadius:6,border:`1px solid ${T.border}`, fontSize:13,color:T.text,outline:"none",fontFamily:"inherit",width:200,background:"#fff"}} />
{filtered.map(p => { const c = contacts[p.id]; const isOpen = expanded === p.id; const posColor = posColorsP[p.pos] || T.accent; const age = calcAge(c.dob); return ( {/* Header */}
setExpanded(isOpen ? null : p.id)} style={{ display:"flex", alignItems:"center", gap:14, padding:"14px 20px", cursor:"pointer", background:isOpen?T.accentLt:"#fff", transition:"background 0.15s" }}>
{p.name}
#{p.num} · {age} años · {c.school}
{c.parents[0]?.phone}
{isOpen ? "▲" : "▼"}
{/* Expanded */} {isOpen && (
Datos personales {[ {l:"Fecha de nacimiento", v:c.dob}, {l:"Edad", v:`${age} años`}, {l:"Escuela", v:c.school}, {l:"Tipo de sangre", v:c.blood}, {l:"Alergias", v:c.allergies}, ].map(s=>(
{s.l} {s.v}
))}
Padres / tutores {c.parents.map((par, i) => (
{par.name} ({par.rel})
{par.phone}
{par.email}
))}
Contacto de emergencia
{c.emergency.name}
{c.emergency.phone}
)}
); })}
); } // ── DOCUMENTOS ──────────────────────────────────────────── function AdminDocumentos() { const { players } = window.TData; const { documents: init, DOC_TYPES } = window.TAdmin; const [docs, setDocs] = React.useState(() => { try { const s = localStorage.getItem("tormentas_admin_docs"); return s ? JSON.parse(s) : init; } catch { return init; } }); const CYCLE = ["ok","pendiente","faltante"]; const cycle = (pid, key) => { setDocs(prev => { const cur = prev[pid][key]; const next = { ...prev, [pid]: { ...prev[pid], [key]: CYCLE[(CYCLE.indexOf(cur)+1)%CYCLE.length] } }; localStorage.setItem("tormentas_admin_docs", JSON.stringify(next)); return next; }); }; const sc = s => s==="ok" ? T.green : s==="pendiente" ? T.yellow : T.red; const sb = s => s==="ok" ? T.greenLt: s==="pendiente" ? T.yellowLt: T.redLt; const sl = s => s==="ok" ? "OK" : s==="pendiente" ? "Pendiente": "Faltante"; const total = players.length * DOC_TYPES.length; const totalOk = players.reduce((n,p)=>n+DOC_TYPES.filter(d=>docs[p.id]?.[d.key]==="ok").length,0); const totalFalta = players.reduce((n,p)=>n+DOC_TYPES.filter(d=>docs[p.id]?.[d.key]==="faltante").length,0); const totalPend = players.reduce((n,p)=>n+DOC_TYPES.filter(d=>docs[p.id]?.[d.key]==="pendiente").length,0); return (

Documentos

Haz clic en cada celda para cambiar el estado

0?T.red:T.green} sub="sin entregar" /> 0?T.yellow:T.green} sub="en proceso" /> =0.8?T.green:T.yellow} />
{DOC_TYPES.map(dt=>( ))} {players.map((p, pi) => ( {DOC_TYPES.map(dt => { const s = docs[p.id]?.[dt.key] || "faltante"; return ( ); })} ))}
Jugadora {dt.label}
{p.name}
#{p.num}
cycle(p.id, dt.key)} style={{ display:"inline-block", padding:"4px 12px", borderRadius:4, background:sb(s), color:sc(s), fontWeight:700, fontSize:11, border:`1px solid ${sc(s)}30`, cursor:"pointer", transition:"all 0.12s", userSelect:"none" }}>{sl(s)}
OK = Entregado Pendiente = En revisión Faltante = Sin entregar Clic en celda para cambiar estado
); } window.AdminPagos = AdminPagos; window.AdminRegistro = AdminRegistro; window.AdminDocumentos = AdminDocumentos; // ── JUGADORAS (ROSTER) ──────────────────────────────────────── const POSITIONS = ["Base","Escolta","Alero","Ala-Pívot","Pívot"]; const AVATAR_COLORS = ["#2bbdf7","#e8508a","#8b6db8","#1a7ecf","#10b981","#f59e0b","#ef4444","#6366f1"]; function AdminJugadoras() { const [players, setPlayers] = React.useState(() => [...window.TData.players]); const [modal, setModal] = React.useState(null); // null | "add" | "edit" const [form, setForm] = React.useState({}); const [deleteId, setDeleteId] = React.useState(null); const persist = (list) => { window.TData.players = list; localStorage.setItem("tormentas_players", JSON.stringify(list)); setPlayers(list); }; const openAdd = () => { setForm({ name:"", num:"", pos:"Base", hand:"Derecha", color:"#2bbdf7" }); setModal("add"); }; const openEdit = (p) => { setForm({...p, num: String(p.num)}); setModal("edit"); }; const handleSubmit = () => { if (!form.name.trim() || !form.num) return; const words = form.name.trim().split(" ").filter(Boolean); const initials = words.map(w => w[0].toUpperCase()).join("").slice(0, 2); const num = parseInt(form.num) || 0; if (modal === "add") { const id = Math.max(0, ...players.map(p => p.id)) + 1; persist([...players, { ...form, id, num, initials, attendancePct: 90 }]); } else { persist(players.map(p => p.id === form.id ? { ...form, num, initials } : p)); } setModal(null); }; const confirmDelete = () => { persist(players.filter(p => p.id !== deleteId)); setDeleteId(null); }; const field = (label, key, type="text", opts=null) => (
{opts ? ( ) : ( setForm(f=>({...f,[key]:e.target.value}))} style={inputSt} /> )}
); const inputSt = { width:"100%", padding:"10px 12px", border:"1.5px solid #e2e8f0", borderRadius:8, fontSize:13, fontFamily:"inherit", outline:"none", color:"#0f172a", background:"#f8fafc" }; const btnSt = (color, bg) => ({ padding:"8px 16px", borderRadius:8, border:"none", cursor:"pointer", fontWeight:600, fontSize:13, fontFamily:"inherit", color, background:bg }); return (
{/* Header */}

Roster

{players.length} jugadoras registradas

{/* List */}
{players.map((p, i) => (
{p.name}
#{p.num} · {p.pos} · {p.hand}
))}
{/* Add/Edit Modal */} {modal && (

{modal === "add" ? "Agregar jugadora" : "Editar jugadora"}

{field("Nombre completo", "name")}
{field("Número", "num", "number")}
{field("Posición", "pos", "text", POSITIONS)}
{field("Mano dominante", "hand", "text", ["Derecha","Izquierda"])}
{AVATAR_COLORS.map(c => (
setForm(f=>({...f, color:c}))} style={{ width:28, height:28, borderRadius:"50%", background:c, cursor:"pointer", border: form.color===c ? "3px solid #0f172a" : "3px solid transparent", boxSizing:"border-box" }} /> ))}
)} {/* Delete confirmation */} {deleteId && (
⚠️

¿Eliminar jugadora?

{players.find(p=>p.id===deleteId)?.name} — Esta acción no se puede deshacer.

)}
); } window.AdminJugadoras = AdminJugadoras;