Saltar al contenido
import React, { useState, useEffect, useMemo } from ‘react’; import { Users, Trophy, PlayCircle, Settings, Plus, Trash2, Printer, Edit2, X, Save, UserPlus, Download, MapPin, ImageIcon, Upload, RefreshCw, Check, RotateCcw, FileSpreadsheet, FileText, ChevronUp, ChevronDown, ListOrdered } from ‘lucide-react’; // — Constantes — const RESULTS = { PENDING: ‘P’, WHITE_WIN: ‘1-0’, BLACK_WIN: ‘0-1’, DRAW: ‘½-½’, BYE: ‘1-0F’ }; const TIEBREAKS = [ { id: ‘buchholz’, name: ‘Buchholz Total’ }, { id: ‘buchholzCut1’, name: ‘Buchholz Mediano (Cut 1)’ }, { id: ‘berger’, name: ‘Sonneborn-Berger’ }, { id: ‘progressive’, name: ‘Progresivo’ }, { id: ‘direct’, name: ‘Resultado Directo’ }, { id: ‘elo’, name: ‘Mayor Elo’ } ]; const CATEGORIES = [‘General’, ‘Sub-18’, ‘Sub-16’, ‘Sub-14’, ‘Sub-12’, ‘Veterano’]; export default function App() { const [activeTab, setActiveTab] = useState(‘players’); const [players, setPlayers] = useState([]); const [rounds, setRounds] = useState([]); const [isTournamentStarted, setIsTournamentStarted] = useState(false); const [editingPlayer, setEditingPlayer] = useState(null); const [tournamentInfo, setTournamentInfo] = useState({ name: ‘Torneo de Ajedrez Pro’, year: new Date().getFullYear(), location: ‘Ciudad, País’, clubLogo: », tournamentLogo: », tiebreakOrder: [‘buchholz’, ‘buchholzCut1’, ‘berger’] }); // — Utilidades de Exportación — const downloadCSV = (data, filename) => { const csvContent = «data:text/csv;charset=utf-8,» + data.map(e => e.join(«,»)).join(«\n»); const encodedUri = encodeURI(csvContent); const link = document.createElement(«a»); link.setAttribute(«href», encodedUri); link.setAttribute(«download», `${filename}.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link); }; const exportPlayersExcel = () => { const headers = [«Nombre», «Apellidos», «Elo FIDE», «Elo FEDA», «Elo Local», «Categoria», «Ciudad»]; const rows = players.map(p => [p.name, p.lastName, p.eloFide, p.eloFeda, p.eloLocal, p.category, p.city]); downloadCSV([headers, …rows], «lista_jugadores»); }; const exportStandingsExcel = () => { const headers = [«Pos», «Jugador», «Puntos», …tournamentInfo.tiebreakOrder.map(t => TIEBREAKS.find(tb => tb.id === t).name)]; const rows = sortedPlayers.map((p, i) => [ i + 1, `${p.lastName}, ${p.name}`, p.score, …tournamentInfo.tiebreakOrder.map(t => p.tiebreaks[t]) ]); downloadCSV([headers, …rows], «clasificacion_final»); }; const exportPairingsExcel = () => { if (rounds.length === 0) return; const currentRound = rounds[rounds.length – 1]; const headers = [«Mesa», «Blancas», «Resultado», «Negras»]; const rows = currentRound.matches.map((m, idx) => { const w = players.find(p => p.id === m.white); const b = m.black === ‘BYE’ ? { name: ‘BYE’, lastName: ‘DESCANSO’ } : players.find(p => p.id === m.black); return [idx + 1, `${w?.lastName}, ${w?.name}`, m.result, `${b?.lastName}, ${b?.name}`]; }); downloadCSV([headers, …rows], `pareos_ronda_${currentRound.number}`); }; // — Lógica de Torneo — const handleAddOrUpdatePlayer = (e) => { e.preventDefault(); const formData = new FormData(e.target); const playerData = { name: formData.get(‘name’), lastName: formData.get(‘lastName’), eloFide: parseInt(formData.get(‘eloFide’)) || 0, eloFeda: parseInt(formData.get(‘eloFeda’)) || 0, eloLocal: parseInt(formData.get(‘eloLocal’)) || 0, birthYear: parseInt(formData.get(‘birthYear’)) || new Date().getFullYear(), category: formData.get(‘category’), city: formData.get(‘city’), }; if (editingPlayer) { setPlayers(players.map(p => p.id === editingPlayer.id ? { …p, …playerData } : p)); setEditingPlayer(null); } else { setPlayers([…players, { …playerData, id: crypto.randomUUID(), score: 0, history: [], colors: [], tiebreaks: { buchholz: 0, buchholzCut1: 0, berger: 0, progressive: 0, direct: 0, elo: Math.max(playerData.eloFide, playerData.eloFeda, playerData.eloLocal) }, resultsHistory: [] }]); } e.target.reset(); }; useEffect(() => { if (isTournamentStarted) recalculateStandings(); }, [rounds]); const generateRound = () => { const nextNum = rounds.length + 1; const sorted = […players].sort((a, b) => { if (b.score !== a.score) return b.score – a.score; return Math.max(b.eloFide, b.eloFeda) – Math.max(a.eloFide, a.eloFeda); }); const used = new Set(); const matches = []; for (let i = 0; i < sorted.length; i++) { if (used.has(sorted[i].id)) continue; let matched = false; for (let j = i + 1; j < sorted.length; j++) { if (!used.has(sorted[j].id) && !sorted[i].history.includes(sorted[j].id)) { const wCount = sorted[i].colors.filter(c => c === 1).length; const bCount = sorted[j].colors.filter(c => c === 1).length; const w = wCount <= bCount ? sorted[i] : sorted[j]; const b = w.id === sorted[i].id ? sorted[j] : sorted[i]; matches.push({ id: `r${nextNum}-${matches.length + 1}`, white: w.id, black: b.id, result: RESULTS.PENDING }); used.add(w.id); used.add(b.id); matched = true; break; } } if (!matched) { matches.push({ id: `r${nextNum}-bye`, white: sorted[i].id, black: 'BYE', result: RESULTS.BYE }); used.add(sorted[i].id); } } setRounds([...rounds, { number: nextNum, matches }]); setIsTournamentStarted(true); setActiveTab('rounds'); }; const recalculateStandings = () => { setPlayers(prevPlayers => { const updated = prevPlayers.map(p => ({ …p, score: 0, history: [], colors: [], resultsHistory: [], tiebreaks: { buchholz: 0, buchholzCut1: 0, berger: 0, progressive: 0, direct: 0, elo: Math.max(p.eloFide, p.eloFeda, p.eloLocal) } })); rounds.forEach(r => { r.matches.forEach(m => { const w = updated.find(p => p.id === m.white); const b = updated.find(p => p.id === m.black); if (w) { w.colors.push(1); if (b && m.black !== ‘BYE’) w.history.push(b.id); if (m.result === RESULTS.WHITE_WIN || m.result === RESULTS.BYE) w.score += 1; else if (m.result === RESULTS.DRAW) w.score += 0.5; w.resultsHistory.push({ opp: m.black, res: m.result, scoreAtMoment: w.score }); } if (b && m.black !== ‘BYE’) { b.colors.push(-1); b.history.push(w.id); if (m.result === RESULTS.BLACK_WIN) b.score += 1; else if (m.result === RESULTS.DRAW) b.score += 0.5; b.resultsHistory.push({ opp: m.white, res: m.result, scoreAtMoment: b.score }); } }); }); updated.forEach(p => { const opponentScores = p.history.map(id => updated.find(o => o.id === id)?.score || 0); p.tiebreaks.buchholz = opponentScores.reduce((a, b) => a + b, 0); p.tiebreaks.buchholzCut1 = opponentScores.length > 1 ? p.tiebreaks.buchholz – Math.min(…opponentScores) : p.tiebreaks.buchholz; p.tiebreaks.progressive = p.resultsHistory.reduce((a, b) => a + b.scoreAtMoment, 0); p.resultsHistory.forEach(rh => { const opp = updated.find(o => o.id === rh.opp); if (!opp) return; if (rh.res === RESULTS.WHITE_WIN || (rh.res === RESULTS.BLACK_WIN && opp.id !== ‘BYE’)) { p.tiebreaks.berger += opp.score; } else if (rh.res === RESULTS.DRAW) { p.tiebreaks.berger += (opp.score / 2); } }); }); return updated; }); }; const sortedPlayers = useMemo(() => { return […players].sort((a, b) => { if (b.score !== a.score) return b.score – a.score; for (let tbId of tournamentInfo.tiebreakOrder) { if (b.tiebreaks[tbId] !== a.tiebreaks[tbId]) return b.tiebreaks[tbId] – a.tiebreaks[tbId]; } return 0; }); }, [players, tournamentInfo.tiebreakOrder]); const moveTiebreak = (index, direction) => { const newOrder = […tournamentInfo.tiebreakOrder]; const newPos = index + direction; if (newPos < 0 || newPos >= newOrder.length) return; [newOrder[index], newOrder[newPos]] = [newOrder[newPos], newOrder[index]]; setTournamentInfo({…tournamentInfo, tiebreakOrder: newOrder}); }; const updateMatchResult = (rIdx, mId, res) => { const newRounds = […rounds]; const match = newRounds[rIdx].matches.find(m => m.id === mId); if (match) { match.result = res; setRounds(newRounds); } }; // — Sub-Componentes de UI — const ActionButtons = ({ onExcel, onPrint, label }) => (
); const tabs = [ { id: ‘players’, icon: Users, label: ‘JUGADORES’ }, { id: ‘rounds’, icon: PlayCircle, label: ‘RONDAS Y PAREOS’ }, { id: ‘standings’, icon: ListOrdered, label: ‘CLASIFICACIÓN’ }, { id: ‘settings’, icon: Settings, label: ‘SISTEMAS Y CONFIG.’ } ]; return (
{/* HEADER */}

{tournamentInfo.name}

{tournamentInfo.location} · {tournamentInfo.year}

{/* MENU HORIZONTAL */}
{/* HEADER IMPRESIÓN */}
{tournamentInfo.clubLogo && Club Logo} {tournamentInfo.tournamentLogo && Tournament Logo}

{tournamentInfo.name}

{tournamentInfo.location} – {tournamentInfo.year}

{activeTab === ‘players’ && «LISTADO DE INSCRITOS»} {activeTab === ‘rounds’ && `PAREOS Y RESULTADOS – RONDA ${rounds.length}`} {activeTab === ‘standings’ && «CLASIFICACIÓN ACTUALIZADA»}
{/* TAB: PLAYERS */} {activeTab === ‘players’ && (
window.print()} label=»Jugadores» />

{editingPlayer ? ‘Editar Datos Jugador’ : ‘Inscripción de Jugador’}

{editingPlayer && }
{players.map(p => ( ))}
Jugador Elo (FIDE/FEDA) Categoría Origen Acciones
{p.lastName}, {p.name} {p.eloFide || ‘-‘} / {p.eloFeda || ‘-‘} {p.category} {p.city}
)} {/* TAB: ROUNDS */} {activeTab === ‘rounds’ && (
window.print()} label=»Pareos» />
{rounds.length > 0 && ( )}

Estado

{rounds.length} RONDAS FINALIZADAS

{rounds.length === 0 ? (
Esperando el inicio del torneo…
) : (
{rounds.slice().reverse().map((r) => (
Índice

Mesa de Juego – Ronda {r.number}

{r.matches.map((m) => { const w = players.find(p => p.id === m.white); const b = m.black === ‘BYE’ ? { name: ‘BYE’, lastName: ‘DESCANSO’, score: 0 } : players.find(p => p.id === m.black); return (

Blancas

{w?.lastName}, {w?.name}

[{w?.score || 0} pts]

{m.result}

Negras

{b?.lastName}, {b?.name}

[{b?.score || 0} pts]

{[RESULTS.WHITE_WIN, RESULTS.DRAW, RESULTS.BLACK_WIN].map(res => ( ))}
#{(m.id.split(‘-‘)[1])}
); })}
))}
)}
)} {/* TAB: STANDINGS */} {activeTab === ‘standings’ && (
window.print()} label=»Clasificación» />
{tournamentInfo.tiebreakOrder.map(tId => ( ))} {sortedPlayers.map((p, i) => ( {tournamentInfo.tiebreakOrder.map(tId => ( ))} ))}
RK Nombre del Jugador PTS {TIEBREAKS.find(tb => tb.id === tId)?.name.split(‘ ‘)[0]}
#{i + 1}
{p.lastName}, {p.name}
{p.city} | {p.category}
{p.score} {typeof p.tiebreaks[tId] === ‘number’ && p.tiebreaks[tId] % 1 !== 0 ? p.tiebreaks[tId].toFixed(2) : p.tiebreaks[tId]}
)} {/* TAB: SETTINGS */} {activeTab === ‘settings’ && (

Sistemas de Desempate

Configura la prioridad de los sistemas de desempate en la clasificación.

{tournamentInfo.tiebreakOrder.map((tId, idx) => { const tb = TIEBREAKS.find(t => t.id === tId); return (
{idx + 1} {tb?.name}
); })}

Configuración Visual

setTournamentInfo({…tournamentInfo, name: e.target.value})} className=»w-full p-4 rounded-2xl border-2 border-slate-100 mt-2 font-black uppercase italic focus:ring-2 ring-blue-500 outline-none» />
setTournamentInfo({…tournamentInfo, year: e.target.value})} className=»w-full p-4 rounded-2xl border-2 border-slate-100 mt-2 font-black italic focus:ring-2 ring-blue-500 outline-none» />
setTournamentInfo({…tournamentInfo, location: e.target.value})} className=»w-full p-4 rounded-2xl border-2 border-slate-100 mt-2 font-black italic focus:ring-2 ring-blue-500 outline-none» />
setTournamentInfo({…tournamentInfo, clubLogo: e.target.value})} placeholder=»https://…» className=»w-full p-4 rounded-2xl border-2 border-slate-100 mt-2 font-bold focus:ring-2 ring-blue-500 outline-none text-xs» />
)}
Swiss Pro Manager v8.1 | Sistema Avanzado de Gestión Deportiva
ESTADO: ACTIVO
); }
Settings