// 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: (
),
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 (
{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} />
|
Jugadora
|
{DOC_TYPES.map(dt=>(
{dt.label}
|
))}
{players.map((p, pi) => (
|
|
{DOC_TYPES.map(dt => {
const s = docs[p.id]?.[dt.key] || "faltante";
return (
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;