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:
" . implode("
", $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."); } }