Agrego archivos iniciales

This commit is contained in:
Laucha1312
2026-06-04 14:47:50 -03:00
commit ed94601e34
76 changed files with 7737 additions and 0 deletions
@@ -0,0 +1,380 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Torneo;
use App\Services\FixtureService;
class FixtureController extends Controller
{
private FixtureService $fixtureService;
public function __construct(FixtureService $fixtureService)
{
$this->fixtureService = $fixtureService;
}
private function checkSuperAdmin()
{
if (!session('admin_logged_in') || session('admin_role') != 1) {
abort(403, 'Solo Súper Administradores pueden generar fixtures.');
}
}
/**
* POST /admin/torneos/{id}/generar-fixture — Preview del fixture
*/
public function preview(Request $request, int $id)
{
$this->checkSuperAdmin();
$torneo = Torneo::with('equipos.club')->findOrFail($id);
$request->validate([
'fecha_inicio' => 'required|date|after_or_equal:today',
'dias_entre_jornadas' => 'required|integer|min:1|max:60',
'sede_default' => 'nullable|string|max:200',
'doble_rueda' => 'nullable|boolean',
]);
try {
$partidos = $this->fixtureService->generarRoundRobin(
torneo: $torneo,
fechaInicio: $request->input('fecha_inicio'),
diasEntreJornadas: (int) $request->input('dias_entre_jornadas', 7),
sedeDefault: $request->input('sede_default', ''),
dobleRueda: (bool) $request->input('doble_rueda', false),
);
// Enriquecer con nombres para la vista
$partidosEnriquecidos = collect($partidos)->map(function ($p) {
$local = \App\Models\Equipo::with('club')->find($p['id_equipo_local']);
$visitante = \App\Models\Equipo::with('club')->find($p['id_equipo_visitante']);
return array_merge($p, [
'nombre_local' => ($local->club->nombre ?? '?') . ' (' . ($local->categoria ?? '') . ')',
'nombre_visitante' => ($visitante->club->nombre ?? '?') . ' (' . ($visitante->categoria ?? '') . ')',
]);
})->toArray();
} catch (\InvalidArgumentException $e) {
return back()->with('admin_error', $e->getMessage());
}
$fixtureParams = [
'fecha_inicio' => $request->input('fecha_inicio'),
'dias_entre_jornadas' => $request->input('dias_entre_jornadas', 7),
'sede_default' => $request->input('sede_default', ''),
'doble_rueda' => (bool) $request->input('doble_rueda', false),
];
return view('admin.torneos.fixture_preview', compact('torneo', 'partidosEnriquecidos', 'fixtureParams'));
}
/**
* POST /admin/torneos/{id}/confirmar-fixture — Persiste el fixture
*/
public function confirmar(Request $request, int $id)
{
$this->checkSuperAdmin();
$torneo = Torneo::with('equipos.club')->findOrFail($id);
$request->validate([
'fecha_inicio' => 'required|date',
'dias_entre_jornadas' => 'required|integer|min:1',
'sede_default' => 'nullable|string|max:200',
'doble_rueda' => 'nullable|boolean',
]);
try {
$partidos = $this->fixtureService->generarRoundRobin(
torneo: $torneo,
fechaInicio: $request->input('fecha_inicio'),
diasEntreJornadas: (int) $request->input('dias_entre_jornadas', 7),
sedeDefault: $request->input('sede_default', ''),
dobleRueda: (bool) $request->input('doble_rueda', false),
);
$creados = $this->fixtureService->persistirFixture($partidos, $torneo);
} catch (\InvalidArgumentException $e) {
return back()->with('admin_error', $e->getMessage());
}
return redirect()->route('admin.torneos.edit', $id)
->with('admin_msg', "✅ Fixture generado correctamente: {$creados} partidos creados.");
}
public function importForm(int $id)
{
$this->checkSuperAdmin();
$torneo = Torneo::findOrFail($id);
return view('admin.torneos.importar', compact('torneo'));
}
public function importStore(Request $request, int $id)
{
$this->checkSuperAdmin();
$torneo = Torneo::with(['equipos.club'])->findOrFail($id);
$request->validate([
'texto_importar' => 'required|string',
]);
$lineas = explode("\n", $request->input('texto_importar'));
$creados = 0;
$errores = [];
foreach ($lineas as $index => $linea) {
$linea = trim($linea);
if (empty($linea)) continue;
try {
// Formato esperado: Fecha, ClubL, CatL, ClubV, CatV, MarcadorL, MarcadorV, Sede, Grupo
$partes = str_getcsv($linea);
if (count($partes) < 7) {
throw new \Exception("Formato insuficiente. Se esperan al menos 7 columnas.");
}
$fechaRaw = trim($partes[0]);
$fecha = \Carbon\Carbon::parse(str_replace('/', '-', $fechaRaw))->format('Y-m-d');
$nombreClubL = trim($partes[1]);
$catL = trim($partes[2]);
$nombreClubV = trim($partes[3]);
$catV = trim($partes[4]);
$marcadorL = trim($partes[5]);
$marcadorV = trim($partes[6]);
$sede = $partes[7] ?? null;
$grupo = $partes[8] ?? null;
// Buscar equipos con validación de grupo (prioridad) y categoría
$equipoL = $this->buscarEquipo($nombreClubL, $catL, $torneo, $grupo);
$equipoV = $this->buscarEquipo($nombreClubV, $catV, $torneo, $grupo);
if (!$equipoL || !$equipoV) {
$missing = !$equipoL ? "'{$nombreClubL} ({$catL})'" : "'{$nombreClubV} ({$catV})'";
throw new \Exception("No se encontró el equipo {$missing} en el grupo '" . ($grupo ?? 'N/A') . "' ni por categoría.");
}
\App\Models\Evento::create([
'id_evento' => uniqid('ev_imp_'),
'id_torneo' => $id,
'fase' => \App\Models\Evento::FASE_REGULAR,
'fecha_evento' => $fecha,
'id_equipo_local' => $equipoL->id_equipo,
'id_equipo_visitante' => $equipoV->id_equipo,
'marcador_local' => $marcadorL !== '' ? (int)$marcadorL : null,
'marcador_visitante' => $marcadorV !== '' ? (int)$marcadorV : null,
'nombre_evento' => $equipoL->club->nombre . ' vs ' . $equipoV->club->nombre,
'hora_inicio' => '19:00:00',
'hora_fin' => '21:00:00',
'sede' => $sede,
'precio' => 0,
]);
$creados++;
} catch (\Exception $e) {
$errores[] = "Línea " . ($index + 1) . ": " . $e->getMessage();
continue;
}
}
$msg = "Se importaron {$creados} partidos exitosamente.";
if (count($errores) > 0) {
return back()->with('admin_msg', $msg)->with('admin_error', "Se detectaron errores en " . count($errores) . " líneas:<br>" . implode("<br>", $errores));
}
return redirect()->route('admin.torneos.edit', $id)->with('admin_msg', $msg);
}
private function buscarEquipo(string $clubName, string $category, Torneo $torneo, ?string $grupo = null)
{
$clubNameNorm = $this->normalizeString($clubName);
$categoryNorm = $this->normalizeString($category);
$grupoNorm = $grupo ? $this->normalizeString($grupo) : null;
// PASO 1: Prioridad absoluta al GRUPO (vía pivot)
if ($grupoNorm) {
$match = $torneo->equipos->first(function($e) use ($clubNameNorm, $grupoNorm) {
$eClubName = $this->normalizeString($e->club->nombre ?? '');
$eGrupo = $this->normalizeString($e->pivot->grupo ?? '');
return ($eGrupo === $grupoNorm) &&
(strpos($eClubName, $clubNameNorm) !== false || strpos($clubNameNorm, $eClubName) !== false);
});
if ($match) return $match;
}
// PASO 2: Fallback a búsqueda por CATEGORÍA si el grupo no coincide o no se especificó
return $torneo->equipos->first(function($e) use ($clubNameNorm, $categoryNorm) {
$eClubName = $this->normalizeString($e->club->nombre ?? '');
$eCategory = $this->normalizeString($e->categoria ?? '');
$matchClub = (strpos($eClubName, $clubNameNorm) !== false || strpos($clubNameNorm, $eClubName) !== false);
$matchCat = (strpos($eCategory, $categoryNorm) !== false || strpos($categoryNorm, $eCategory) !== false);
return $matchClub && $matchCat;
});
}
private function normalizeString(?string $str): string
{
if (!$str) return '';
$str = mb_strtolower(trim($str), 'UTF-8');
$str = str_replace(
['á', 'é', 'í', 'ó', 'ú', 'ü', 'ñ'],
['a', 'e', 'i', 'o', 'u', 'u', 'n'],
$str
);
// Eliminar caracteres no alfanuméricos para un matching más flexible
return preg_replace('/[^a-z0-9]/', '', $str);
}
public function generarPlayoffs(Request $request, int $id)
{
$this->checkSuperAdmin();
$torneo = Torneo::findOrFail($id);
$grupo = $request->input('grupo');
$formato = (int) $request->input('formato', 1); // 1, 3, 5
if (!$grupo) {
return back()->with('admin_error', 'Debes seleccionar un grupo para generar los playoffs.');
}
$ts = new \App\Services\TournamentService();
$standings = $ts->getStandings($id, true);
if (!isset($standings[$grupo])) {
return back()->with('admin_error', "No hay equipos en el grupo {$grupo}.");
}
$top8 = array_slice($standings[$grupo], 0, 8);
if (count($top8) < 2) {
return back()->with('admin_error', "Se necesitan al menos 2 equipos para generar playoffs.");
}
// Determinar emparejamientos (1 vs 8, 4 vs 5, 2 vs 7, 3 vs 6)
// Usamos el seeding oficial: 1v8, 4v5, 2v7, 3v6
$parejas = [
['local' => $top8[0]['id'], 'visit' => $top8[7]['id'] ?? null, 'nro' => 1],
['local' => $top8[3]['id'] ?? null, 'visit' => $top8[4]['id'] ?? null, 'nro' => 2],
['local' => $top8[1]['id'] ?? null, 'visit' => $top8[6]['id'] ?? null, 'nro' => 3],
['local' => $top8[2]['id'] ?? null, 'visit' => $top8[5]['id'] ?? null, 'nro' => 4],
];
$count = 0;
foreach ($parejas as $p) {
if (!$p['local'] || !$p['visit']) continue;
$local = \App\Models\Equipo::with('club')->find($p['local']);
$visit = \App\Models\Equipo::with('club')->find($p['visit']);
// Crear N partidos según el formato
for ($i = 0; $i < $formato; $i++) {
\App\Models\Evento::create([
'id_evento' => uniqid('ply_'),
'id_torneo' => $id,
'fase' => \App\Models\Evento::FASE_CUARTOS,
'numero_partido_bracket' => $p['nro'],
'id_equipo_local' => $p['local'],
'id_equipo_visitante' => $p['visit'],
'nombre_evento' => "4tos (J".($i+1)."): " . $local->club->nombre . " vs " . $visit->club->nombre . " ({$grupo})",
'fecha_evento' => now()->addDays(7 + ($i * 3))->format('Y-m-d'),
'hora_inicio' => '20:00:00',
'hora_fin' => '22:00:00',
'precio' => 0,
]);
$count++;
}
}
return redirect()->route('admin.torneos.playoffs.manage', $id)
->with('admin_msg', "Playoffs generados: {$count} partidos creados para el grupo {$grupo}.");
}
public function managePlayoffs(int $id)
{
$this->checkSuperAdmin();
$torneo = Torneo::findOrFail($id);
$ts = new \App\Services\TournamentService();
$bracket = $ts->getPlayoffBrackets($id);
$grupos = \DB::table('torneo_equipo')->where('id_torneo', $id)->distinct()->pluck('grupo')->filter();
return view('admin.torneos.playoff_manage', compact('torneo', 'bracket', 'grupos'));
}
public function avanzarGanador(Request $request, int $id)
{
$this->checkSuperAdmin();
$faseActual = (int) $request->input('fase');
$nroBracket = (int) $request->input('nro_bracket');
$idGanador = (int) $request->input('id_ganador');
$torneo = Torneo::findOrFail($id);
$ganador = \App\Models\Equipo::with('club')->findOrFail($idGanador);
// Mapeo de avance
// Cuartos 1 & 2 -> Semis 1
// Cuartos 3 & 4 -> Semis 2
// Semis 1 & 2 -> Final 1
$mapping = [
\App\Models\Evento::FASE_CUARTOS => [
1 => ['next_fase' => \App\Models\Evento::FASE_SEMIS, 'next_nro' => 1, 'side' => 'local'],
2 => ['next_fase' => \App\Models\Evento::FASE_SEMIS, 'next_nro' => 1, 'side' => 'visitante'],
3 => ['next_fase' => \App\Models\Evento::FASE_SEMIS, 'next_nro' => 2, 'side' => 'local'],
4 => ['next_fase' => \App\Models\Evento::FASE_SEMIS, 'next_nro' => 2, 'side' => 'visitante'],
],
\App\Models\Evento::FASE_SEMIS => [
1 => ['next_fase' => \App\Models\Evento::FASE_FINAL, 'next_nro' => 1, 'side' => 'local'],
2 => ['next_fase' => \App\Models\Evento::FASE_FINAL, 'next_nro' => 1, 'side' => 'visitante'],
]
];
if (!isset($mapping[$faseActual][$nroBracket])) {
return back()->with('admin_error', 'No se puede avanzar desde esta fase.');
}
$next = $mapping[$faseActual][$nroBracket];
// Buscar si ya existe el partido en la siguiente fase
$eventoNext = \App\Models\Evento::where('id_torneo', $id)
->where('fase', $next['next_fase'])
->where('numero_partido_bracket', $next['next_nro'])
->first();
if ($eventoNext) {
if ($next['side'] == 'local') {
$eventoNext->id_equipo_local = $idGanador;
} else {
$eventoNext->id_equipo_visitante = $idGanador;
}
$eventoNext->nombre_evento = ($eventoNext->equipoLocal->club->nombre ?? 'TBD') . ' vs ' . ($eventoNext->equipoVisitante->club->nombre ?? 'TBD');
$eventoNext->save();
} else {
// Crear el primer partido de la serie (por defecto 1 partido, el admin puede agregar más)
\App\Models\Evento::create([
'id_evento' => uniqid('ply_adv_'),
'id_torneo' => $id,
'fase' => $next['next_fase'],
'numero_partido_bracket' => $next['next_nro'],
'id_equipo_local' => $next['side'] == 'local' ? $idGanador : null,
'id_equipo_visitante' => $next['side'] == 'visitante' ? $idGanador : null,
'nombre_evento' => $next['side'] == 'local' ? ($ganador->club->nombre . " vs TBD") : ("TBD vs " . $ganador->club->nombre),
'fecha_evento' => now()->addDays(14)->format('Y-m-d'),
'hora_inicio' => '20:00:00',
'hora_fin' => '22:00:00',
'precio' => 0,
]);
}
return back()->with('admin_msg', "✅ Equipo {$ganador->club->nombre} avanzado a la siguiente ronda.");
}
}