checkGeneralAdmin($request); if (session('admin_role') == 1) { $stats = [ 'clubes' => Club::count(), 'equipos' => Equipo::count(), 'jugadores' => Jugador::count(), 'eventos' => Evento::count(), 'promociones' => Promocion::count(), 'noticias' => Noticia::count(), ]; $miClub = null; } else { $idClub = session('admin_id_club'); $miClub = Club::find($idClub); $stats = [ 'equipos' => Equipo::where('id_club', $idClub)->count(), 'jugadores' => Jugador::where('id_club_actual', $idClub)->count(), 'eventos' => Evento::whereHas('equipoLocal', function($q) use ($idClub) { $q->where('id_club', $idClub); })->orWhereHas('equipoVisitante', function($q) use ($idClub) { $q->where('id_club', $idClub); })->count(), 'promociones' => Promocion::count(), // Promociones son generales ]; } return view('admin.dashboard', compact('stats', 'miClub')); } // ══════════════════════════════════ // CLUBES // ══════════════════════════════════ public function clubesIndex(Request $request) { $this->checkSuperAdmin($request); $search = trim((string) $request->input('q', '')); // Recién creados/editados primero; registros viejos sin timestamps caen al fallback por id. $query = Club::withCount('equipos', 'jugadores') ->orderByRaw("COALESCE(updated_at, '1970-01-01') DESC") ->orderBy('id_club', 'desc'); if ($search !== '') { $query->where('nombre', 'like', '%' . $search . '%'); } $clubes = $query->get(); return view('admin.clubes.index', compact('clubes', 'search')); } public function clubesCreate(Request $request) { $this->checkSuperAdmin($request); return view('admin.clubes.form', ['club' => null]); } public function clubesStore(Request $request) { $this->checkSuperAdmin($request); $data = $request->validate([ 'id_club' => 'nullable|integer', 'nombre' => 'required|string|max:100', 'es_seleccion' => 'nullable|boolean', ]); $data['es_seleccion'] = $request->boolean('es_seleccion'); Club::create($data); return redirect()->route('admin.clubes.index')->with('admin_msg', 'Club creado correctamente.'); } public function clubesEdit(Request $request, $id) { $this->checkGeneralAdmin($request); if (session('admin_role') == 2 && session('admin_id_club') != $id) { abort(403, 'No tienes permiso para editar este club.'); } $club = Club::findOrFail($id); return view('admin.clubes.form', compact('club')); } public function clubesUpdate(Request $request, $id) { $this->checkGeneralAdmin($request); if (session('admin_role') == 2 && session('admin_id_club') != $id) { abort(403, 'No tienes permiso para editar este club.'); } $club = Club::findOrFail($id); $rules = [ 'qr_color_texto' => 'nullable|string|max:20', 'qr_background' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:2048', 'logo_club' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:1024' ]; if (session('admin_role') == 1) { $rules['nombre'] = 'required|string|max:100'; $rules['es_seleccion'] = 'nullable|boolean'; } $data = $request->validate($rules); if (session('admin_role') == 1) { $data['es_seleccion'] = $request->boolean('es_seleccion'); } // Manejo de Logo del Club if ($request->hasFile('logo_club')) { // Eliminar logo anterior si existe if ($club->imagen && file_exists(public_path($club->imagen))) { @unlink(public_path($club->imagen)); } $logoPath = app(ImageOptimizer::class)->storeAndOptimize($request->file('logo_club'), 'clubes'); $data['imagen'] = 'storage/' . $logoPath; } // Manejo de Fondo QR if ($request->hasFile('qr_background')) { // Eliminar fondo anterior si existe if ($club->qr_background && file_exists(public_path($club->qr_background))) { @unlink(public_path($club->qr_background)); } $path = app(ImageOptimizer::class)->storeAndOptimize($request->file('qr_background'), 'qr'); $data['qr_background'] = 'storage/' . $path; } $club->update($data); if (session('admin_role') == 2) { return back()->with('admin_msg', 'Plantilla de QR actualizada correctamente.'); } return redirect()->route('admin.clubes.index')->with('admin_msg', 'Club actualizado correctamente.'); } public function clubesDestroy(Request $request, $id) { $this->checkSuperAdmin($request); $club = Club::findOrFail($id); $club->delete(); return redirect()->route('admin.clubes.index')->with('admin_msg', 'Club eliminado correctamente.'); } // ══════════════════════════════════ // EQUIPOS // ══════════════════════════════════ public function equiposIndex(Request $request) { $this->checkGeneralAdmin($request); $search = trim((string) $request->input('q', '')); $query = Equipo::with('club')->withCount('jugadores') ->orderByRaw("COALESCE(updated_at, '1970-01-01') DESC") ->orderBy('id_equipo', 'desc'); if (session('admin_role') == 2) { $query->where('id_club', session('admin_id_club')); } if ($search !== '') { $like = '%' . $search . '%'; $query->where(function ($q) use ($like, $search) { $q->where('categoria', 'like', $like) ->orWhere('division', 'like', $like) ->orWhereHas('club', function ($c) use ($like) { $c->where('nombre', 'like', $like); }); if (ctype_digit($search)) { $q->orWhere('id_equipo', (int) $search); } }); } $equipos = $query->get(); return view('admin.equipos.index', compact('equipos', 'search')); } public function equiposCreate(Request $request) { $this->checkGeneralAdmin($request); if (session('admin_role') == 1) { $clubes = Club::orderBy('nombre')->get(); } else { $clubes = Club::where('id_club', session('admin_id_club'))->get(); } $categorias = \App\Models\Categoria::orderBy('nombre')->get(); return view('admin.equipos.form', ['equipo' => null, 'clubes' => $clubes, 'categorias' => $categorias]); } public function equiposStore(Request $request) { $this->checkGeneralAdmin($request); $data = $request->validate([ 'id_club' => session('admin_role') == 1 ? 'required|integer|exists:clubes,id_club' : 'nullable', 'categoria' => 'required|string|max:20', 'division' => 'required|string|max:5', ]); if (session('admin_role') == 2) { $data['id_club'] = session('admin_id_club'); } Equipo::create($data); return redirect()->route('admin.equipos.index')->with('admin_msg', 'Equipo creado correctamente.'); } public function equiposEdit(Request $request, $id) { $this->checkGeneralAdmin($request); $equipo = Equipo::findOrFail($id); if (session('admin_role') == 2 && $equipo->id_club != session('admin_id_club')) { abort(403, 'No tienes permiso para editar este equipo.'); } if (session('admin_role') == 1) { $clubes = Club::orderBy('nombre')->get(); } else { $clubes = Club::where('id_club', session('admin_id_club'))->get(); } $categorias = \App\Models\Categoria::orderBy('nombre')->get(); return view('admin.equipos.form', compact('equipo', 'clubes', 'categorias')); } public function equiposUpdate(Request $request, $id) { $this->checkGeneralAdmin($request); $equipo = Equipo::findOrFail($id); if (session('admin_role') == 2 && $equipo->id_club != session('admin_id_club')) { abort(403, 'No tienes permiso para editar este equipo.'); } $data = $request->validate([ 'id_club' => session('admin_role') == 1 ? 'required|integer|exists:clubes,id_club' : 'nullable', 'categoria' => 'required|string|max:20', 'division' => 'required|string|max:5', ]); if (session('admin_role') == 2) { $data['id_club'] = session('admin_id_club'); } $equipo->update($data); return redirect()->route('admin.equipos.index')->with('admin_msg', 'Equipo actualizado correctamente.'); } public function equiposDestroy(Request $request, $id) { $this->checkGeneralAdmin($request); $equipo = Equipo::findOrFail($id); if (session('admin_role') == 2 && $equipo->id_club != session('admin_id_club')) { abort(403, 'No tienes permiso para eliminar este equipo.'); } $equipo->delete(); return redirect()->route('admin.equipos.index')->with('admin_msg', 'Equipo eliminado correctamente.'); } public function equipoJugadores($id) { $this->checkGeneralAdmin(request()); $equipo = Equipo::with('club', 'jugadores')->findOrFail($id); if (session('admin_role') == 2 && $equipo->id_club != session('admin_id_club')) { abort(403, 'No tienes permiso para gestionar jugadores de este equipo.'); } return view('admin.equipos.jugadores', compact('equipo')); } public function equipoAddJugador(Request $request, $id) { $this->checkGeneralAdmin($request); $equipo = Equipo::findOrFail($id); if (session('admin_role') == 2 && $equipo->id_club != session('admin_id_club')) { abort(403); } $data = $request->validate([ 'id_jugador' => 'required|string|exists:jugadores,id_jugador', ]); // Verificar que el jugador sea del mismo club, salvo que el equipo pertenezca a una selección $jugador = Jugador::where('id_jugador', $data['id_jugador'])->firstOrFail(); $esSeleccion = $equipo->club && $equipo->club->es_seleccion; if (!$esSeleccion && $jugador->id_club_actual != $equipo->id_club) { return back()->with('admin_error', 'El jugador no pertenece al mismo club que el equipo.'); } // Evitar duplicados if ($equipo->jugadores()->where('jugador_equipo.id_jugador', $data['id_jugador'])->exists()) { return back()->with('admin_error', 'El jugador ya está asignado a este equipo.'); } $equipo->jugadores()->attach($data['id_jugador'], ['fecha_alta' => now()]); return back()->with('admin_msg', 'Jugador asignado correctamente.'); } public function equipoRemoveJugador($id, $id_jugador) { $this->checkGeneralAdmin(request()); $equipo = Equipo::findOrFail($id); if (session('admin_role') == 2 && $equipo->id_club != session('admin_id_club')) { abort(403); } $equipo->jugadores()->detach($id_jugador); return back()->with('admin_msg', 'Jugador removido del equipo.'); } public function jugadoresSearchAjax(Request $request) { $this->checkGeneralAdmin($request); $q = $request->input('q'); $query = Jugador::query(); if (session('admin_role') == 2) { $query->where('id_club_actual', session('admin_id_club')); } if ($q) { $query->where(function($sub) use ($q) { $sub->where('nombre', 'like', "%$q%") ->orWhere('apellido', 'like', "%$q%") ->orWhere('documento', 'like', "%$q%"); }); } $jugadores = $query->limit(10)->get(['id_jugador', 'nombre', 'apellido', 'documento']); return response()->json($jugadores); } public function jugadoresCategoriaPorEdad(Request $request) { $this->checkGeneralAdmin($request); $fecha = $request->input('fecha'); if (!$fecha) return response()->json(['categoria' => 'Sin categoría']); $anio = \Carbon\Carbon::parse($fecha)->format('Y'); $edadCategoria = date('Y') - $anio; $categoria = \App\Models\Categoria::where('edad_min', '<=', $edadCategoria) ->where('edad_max', '>=', $edadCategoria) ->first(); return response()->json(['categoria' => $categoria ? $categoria->nombre : 'Sin categoría']); } // ══════════════════════════════════ // JUGADORES // ══════════════════════════════════ public function jugadoresIndex(Request $request) { $this->checkGeneralAdmin($request); $search = $request->input('q'); $query = Jugador::with('clubActual') ->orderByRaw("COALESCE(updated_at, '1970-01-01') DESC") ->orderByRaw('CAST(id_jugador AS UNSIGNED) DESC'); if (session('admin_role') == 2) { $query->where('id_club_actual', session('admin_id_club')); } if ($search) { // Tokenizar por espacios. Cada token debe matchear como INICIO de palabra // dentro de "apellido nombre" — así "Man Adriel" encuentra a MAN ADRIEL pero // no a ROMAN PEREZ. El DNI sigue aceptando match por substring directo. $tokens = array_values(array_filter( array_map( fn($t) => preg_replace('/[^\p{L}\p{N}]/u', '', $t), preg_split('/\s+/u', $search) ), fn($t) => mb_strlen($t) > 0 )); $query->where(function ($q) use ($tokens, $search) { $q->where('documento', 'like', '%' . $search . '%'); if (!empty($tokens)) { $q->orWhere(function ($qq) use ($tokens) { foreach ($tokens as $token) { $qq->whereRaw( "LOWER(CONCAT(apellido, ' ', nombre)) REGEXP ?", ['[[:<:]]' . mb_strtolower($token)] ); } }); } }); } $jugadores = $query->paginate(25); $clubes = session('admin_role') == 1 ? Club::orderBy('nombre')->get() : []; return view('admin.jugadores.index', compact('jugadores', 'search', 'clubes')); } public function jugadoresCreate(Request $request) { $this->checkGeneralAdmin($request); $clubes = Club::orderBy('nombre')->get(); return view('admin.jugadores.form', ['jugador' => null, 'clubes' => $clubes]); } public function jugadoresStore(Request $request) { $this->checkGeneralAdmin($request); $data = $request->validate([ 'documento' => 'required|string|max:20', 'nombre' => 'required|string|max:100', 'apellido' => 'required|string|max:100', 'fecha_nacimiento' => 'required|date', 'id_club_actual' => 'nullable|integer|exists:clubes,id_club', 'id_club_origen' => 'required|integer|exists:clubes,id_club', ]); // Verificar si el DNI ya existe. withTrashed: el índice UNIQUE de la BD considera // también soft-deleted, así que la validación debe contemplarlos para no caer en error 1062. $existente = Jugador::withTrashed()->where('documento', $data['documento'])->first(); if ($existente) { if ($existente->trashed()) { return back()->withInput()->withErrors([ 'documento' => "Este DNI pertenece a un jugador que está en la papelera. Restauralo desde allí o contactá al superadmin." ]); } $clubNombre = $existente->clubActual ? $existente->clubActual->nombre : 'Sin Club'; return back()->withInput()->withErrors([ 'documento' => "No se puede registrar al jugador dado que ya pertenece al club $clubNombre." ]); } if (session('admin_role') == 2) { $data['id_club_actual'] = session('admin_id_club'); // Nota: id_club_origen podría ser el mismo o diferente, // pero el Id del jugador siempre se basa en el de origen. } $data['nombre'] = strtoupper(trim($data['nombre'])); $data['apellido'] = strtoupper(trim($data['apellido'])); $data['activo'] = 0; // Se valida luego en /asociate if (isset($data['fecha_nacimiento'])) { $data['edad'] = \Carbon\Carbon::parse($data['fecha_nacimiento'])->age; // $data['categoria'] ya no se asigna, ahora es dinámica } $idClub = $data['id_club_origen']; $yearFull = \Carbon\Carbon::parse($data['fecha_nacimiento'])->format('Y'); $data['id_jugador'] = $this->generarIdJugador($idClub, $yearFull); Jugador::create($data); return redirect()->route('admin.jugadores.index')->with('admin_msg', 'Jugador creado correctamente. Puede activarse en /asociate.'); } private function generarIdJugador($idClub, $yearFull) { $yearShort = \Carbon\Carbon::parse($yearFull . '-01-01')->format('y'); $prefix = $idClub . $yearShort; $secuencia = $this->obtenerSiguienteSecuencia($idClub, $yearFull, $prefix); return sprintf('%s%02d', $prefix, $secuencia); } private function obtenerSiguienteSecuencia($idClub, $yearFull, $prefix) { // withTrashed: incluir jugadores soft-deleted para no reusar IDs y chocar con la PK. $ultimoId = (string)Jugador::withTrashed() ->where('id_jugador', 'LIKE', $prefix . '%') ->whereRaw("id_jugador REGEXP '^[0-9]+$'") ->orderByRaw('CAST(id_jugador AS UNSIGNED) DESC') ->value('id_jugador'); $secuencia = 1; if ($ultimoId && str_starts_with($ultimoId, $prefix)) { $secuenciaStr = substr($ultimoId, strlen($prefix)); $secuencia = (int)$secuenciaStr + 1; } return $secuencia; } public function jugadoresImport(Request $request) { $this->checkGeneralAdmin($request); $request->validate([ 'csv_file' => 'required|file|mimes:csv,txt', 'id_club' => session('admin_role') == 1 ? 'nullable|integer' : 'nullable' ]); $file = $request->file('csv_file'); $handle = fopen($file->getRealPath(), 'r'); $successCount = 0; $omittedCount = 0; $errorCount = 0; $teamAssignedCount = 0; $errors = []; // Determinar Club Target $targetClubId = session('admin_role') == 2 ? session('admin_id_club') : ($request->input('id_club') ?? 99); $currentCategory = null; $formatType = 'legacy'; // legacy, cab, internal $localSequences = []; // Cache para evitar duplicados en el mismo loop // Leer primeras líneas para detectar formato (el CAB puede empezar con una categoría) $detectLines = []; for ($i=0; $i<5; $i++) { $l = fgets($handle); if ($l) $detectLines[] = $l; } rewind($handle); $fullContentSample = implode("\n", $detectLines); if (str_contains($fullContentSample, 'ESTADO LICENCIA') || str_contains($fullContentSample, 'TIPO') || str_contains($fullContentSample, 'JUGADOR')) { $formatType = 'cab'; } elseif (str_contains($fullContentSample, 'DNI;Apellido;Nombre;Fecha Nacimiento')) { $formatType = 'internal'; } while (($row = fgetcsv($handle, 1000, ";")) !== FALSE) { if (empty(array_filter($row))) continue; // --- DETECCION DE CATEGORIA (Solo CAB) --- // Si la primera columna tiene texto y el resto está vacío, es una categoría (ej: "PREMINI B;;;;;") if ($formatType == 'cab' && !empty($row[0]) && empty(array_filter(array_slice($row, 1)))) { $currentCategory = trim($row[0]); continue; } // Saltos de cabecera if ($formatType == 'cab' && $row[0] == 'ESTADO LICENCIA') continue; if ($formatType == 'internal' && $row[0] == 'DNI') continue; try { $dni = ''; $apellido = ''; $nombre = ''; $fechaNac = ''; $idClubOrigen = $targetClubId; $idClubActual = $targetClubId; $isJugador = true; if ($formatType == 'cab') { // Formato: ESTADO LICENCIA;NIF;NOMBRE;FECHA_ALTA;BAJA;TIPO;FECHA NACIMIENTO;NACIONALIDAD if (count($row) < 7) continue; $dni = trim($row[1]); $fullName = trim($row[2]); // "APELLIDO, NOMBRE" $tipo = strtoupper(trim($row[5])); $fechaRaw = trim($row[6]); // d/m/Y if ($tipo !== 'JUGADOR') { $isJugador = false; } if ($isJugador) { $parts = explode(',', $fullName); $apellido = trim($parts[0]); $nombre = isset($parts[1]) ? trim($parts[1]) : ''; // Parsear d/m/Y $dateParts = explode('/', $fechaRaw); if (count($dateParts) == 3) { $fechaNac = "{$dateParts[2]}-{$dateParts[1]}-{$dateParts[0]}"; } } } elseif ($formatType == 'internal') { // Formato: DNI; Apellido; Nombre; Fecha Nacimiento (d/m/Y); ID Club Origen; ID Club Actual; Categoria; Activo $dni = trim($row[0]); $apellido = trim($row[1]); $nombre = trim($row[2]); $fechaRaw = trim($row[3]); $idClubOrigen = isset($row[4]) ? (int)trim($row[4]) : $targetClubId; $idClubActual = isset($row[5]) ? (int)trim($row[5]) : $targetClubId; $dateParts = explode('/', $fechaRaw); if (count($dateParts) == 3) { $fechaNac = "{$dateParts[2]}-{$dateParts[1]}-{$dateParts[0]}"; } } else { // Formato Legado: DNI; Apellido; Nombre; ddmmaaaa; id_club_origen if (count($row) < 4) continue; $dni = trim($row[0]); // SEGURIDAD: Si el DNI no es numérico en formato legado, probablemente sea una cabecera o ruido if (!is_numeric($dni)) continue; $apellido = trim($row[1]); $nombre = trim($row[2]); $fechaRaw = trim($row[3]); // ddmmaaaa $idClubOrigen = isset($row[4]) ? (int)trim($row[4]) : $targetClubId; if (strlen($fechaRaw) == 8) { $fechaNac = substr($fechaRaw, 4, 4) . "-" . substr($fechaRaw, 2, 2) . "-" . substr($fechaRaw, 0, 2); } } if (!$isJugador || !$dni || !$apellido || !$nombre || !$fechaNac) continue; // Verificar existencia $jugador = Jugador::where('documento', $dni)->first(); $anioNac = date('Y', strtotime($fechaNac)); $data = [ 'documento' => $dni, 'apellido' => strtoupper(trim($apellido)), 'nombre' => strtoupper(trim($nombre)), 'fecha_nacimiento' => $fechaNac, 'id_club_origen' => $idClubOrigen, 'id_club_actual' => $idClubActual, 'edad' => \Carbon\Carbon::parse($fechaNac)->age, ]; if (!$jugador) { // Generar ID con cache local para evitar colisiones en el mismo loop $prefix = $idClubOrigen . date('y', strtotime($fechaNac)); if (!isset($localSequences[$prefix])) { // Obtener la base inicial de la BDD $localSequences[$prefix] = $this->obtenerSiguienteSecuencia($idClubOrigen, $anioNac, $prefix); } else { $localSequences[$prefix]++; } $data['id_jugador'] = sprintf('%s%02d', $prefix, $localSequences[$prefix]); $data['activo'] = 0; $jugador = Jugador::create($data); $successCount++; } else { // El usuario prefiere NO pisar datos si el jugador ya existe $omittedCount++; } // --- MATCHING DE EQUIPO --- if ($currentCategory && $jugador) { $equipo = Equipo::where('id_club', $idClubActual) ->where('categoria', 'LIKE', $currentCategory) ->first(); if ($equipo) { // Evitar duplicados en pivot if (!$equipo->jugadores()->where('jugador_equipo.id_jugador', $jugador->id_jugador)->exists()) { $equipo->jugadores()->attach($jugador->id_jugador, ['fecha_alta' => now()]); $teamAssignedCount++; } } } } catch (\Exception $e) { $errorCount++; $errors[] = "Error en fila DNI $dni: " . $e->getMessage(); } } fclose($handle); $msg = "Importación finalizada ({$formatType}). $successCount nuevos creados, $omittedCount ya registrados (no modificados)."; if ($teamAssignedCount > 0) $msg .= " $teamAssignedCount asignaciones a equipos realizadas."; if ($errorCount > 0) { return redirect()->route('admin.jugadores.index')->with('admin_msg', $msg)->with('admin_error', implode(" | ", array_slice($errors, 0, 5))); } return redirect()->route('admin.jugadores.index')->with('admin_msg', $msg); } public function jugadoresExport(Request $request) { $this->checkGeneralAdmin($request); $headers = [ "Content-type" => "text/csv; charset=UTF-8", "Content-Disposition" => "attachment; filename=jugadores_" . date('Ymd_His') . ".csv", "Pragma" => "no-cache", "Cache-Control" => "must-revalidate, post-check=0, pre-check=0", "Expires" => "0" ]; $callback = function() { $file = fopen('php://output', 'w'); // Añadir BOM para Excel (UTF-8) fprintf($file, chr(0xEF).chr(0xBB).chr(0xBF)); // Cabeceras fputcsv($file, [ 'DNI', 'Apellido', 'Nombre', 'Fecha Nacimiento', 'ID Club Origen', 'ID Club Actual', 'Categoria', 'Activo' ], ";"); $query = Jugador::orderBy('apellido'); if (session('admin_role') == 2) { $query->where('id_club_actual', session('admin_id_club')); } $jugadores = $query->get(); foreach ($jugadores as $j) { fputcsv($file, [ $j->documento, $j->apellido, $j->nombre, $j->fecha_nacimiento ? $j->fecha_nacimiento->format('d/m/Y') : '', $j->id_club_origen ?? 99, $j->id_club_actual ?? 99, $j->categoria_calculada, $j->activo ? 'SI' : 'NO' ], ";"); } fclose($file); }; return response()->stream($callback, 200, $headers); } public function jugadoresEdit(Request $request, $id) { $this->checkGeneralAdmin($request); $jugador = Jugador::findOrFail($id); if (session('admin_role') == 2 && $jugador->id_club_actual != session('admin_id_club')) { abort(403, 'No puedes editar un jugador que no pertenece a tu club.'); } $clubes = session('admin_role') == 1 ? Club::orderBy('nombre')->get() : []; return view('admin.jugadores.form', compact('jugador', 'clubes')); } public function jugadoresUpdate(Request $request, $id) { $this->checkGeneralAdmin($request); $jugador = Jugador::findOrFail($id); if (session('admin_role') == 2 && $jugador->id_club_actual != session('admin_id_club')) { abort(403, 'No puedes editar un jugador que no pertenece a tu club.'); } $data = $request->validate([ 'documento' => 'required|string|max:20|unique:jugadores,documento,' . $id . ',id_jugador', 'nombre' => 'required|string|max:100', 'apellido' => 'required|string|max:100', 'fecha_nacimiento' => 'required|date', 'id_club_actual' => 'nullable|integer|exists:clubes,id_club', 'id_club_origen' => 'nullable|integer|exists:clubes,id_club', ]); if (session('admin_role') == 2) { $data['id_club_actual'] = session('admin_id_club'); // Forzamos a no cambiarlo } if (isset($data['fecha_nacimiento'])) { $data['edad'] = \Carbon\Carbon::parse($data['fecha_nacimiento'])->age; } $jugador->update($data); return redirect()->route('admin.jugadores.index')->with('admin_msg', 'Jugador actualizado correctamente.'); } public function jugadoresDestroy(Request $request, $id) { $this->checkGeneralAdmin($request); $jugador = Jugador::findOrFail($id); if (session('admin_role') == 2 && $jugador->id_club_actual != session('admin_id_club')) { abort(403, 'No tienes permiso para eliminar este jugador.'); } $jugador->delete(); return redirect()->route('admin.jugadores.index')->with('admin_msg', 'Jugador eliminado correctamente.'); } // ══════════════════════════════════ // EVENTOS // ══════════════════════════════════ public function escanearQr(Request $request) { $this->checkGeneralAdmin($request); $query = Evento::orderByRaw(" CAST(SUBSTRING_INDEX(nombre_evento, 'PARTIDO ', -1) AS UNSIGNED) ASC, nombre_evento ASC, fecha_evento ASC, hora_inicio ASC "); $ahora = \Carbon\Carbon::now(); $query->where(function($q) use ($ahora) { $q->where(function($sub) { $sub->whereNull('marcador_local') ->orWhereNull('marcador_visitante'); }) ->orWhere('fecha_evento', '>', $ahora->toDateString()) ->orWhere(function($q2) use ($ahora) { $q2->where('fecha_evento', '=', $ahora->toDateString()) ->where('hora_fin', '>', $ahora->toTimeString()); }); }); if (session('admin_role') == 2) { $idClub = session('admin_id_club'); $query->where(function ($q) use ($idClub) { $q->whereHas('equipoLocal', function ($q2) use ($idClub) { $q2->where('id_club', $idClub); })->orWhereHas('equipoVisitante', function ($q2) use ($idClub) { $q2->where('id_club', $idClub); }); }); } $eventos = $query->get(); return view('admin.escanear_qr', compact('eventos')); } public function eventosIndex(Request $request) { $this->checkGeneralAdmin($request); $estado = $request->get('estado', 'todos'); $query = Evento::with(['equipoLocal.club', 'equipoVisitante.club'])->orderBy('fecha_evento', 'desc'); $tz = 'America/Argentina/Buenos_Aires'; $ahora = \Carbon\Carbon::now($tz); // Filtro por estado if ($estado == 'finalizados') { $query->whereNotNull('marcador_local') ->whereNotNull('marcador_visitante') ->where(function($q) use ($ahora) { $q->where('fecha_evento', '<', $ahora->toDateString()) ->orWhere(function($q2) use ($ahora) { $q2->where('fecha_evento', '=', $ahora->toDateString()) ->where('hora_fin', '<=', $ahora->toTimeString()); }); }); } elseif ($estado == 'pendientes') { $query->where(function($q) use ($ahora) { $q->whereNull('marcador_local') ->orWhereNull('marcador_visitante') ->orWhere('fecha_evento', '>', $ahora->toDateString()) ->orWhere(function($q2) use ($ahora) { $q2->where('fecha_evento', '=', $ahora->toDateString()) ->where('hora_fin', '>', $ahora->toTimeString()); }); }); } if (session('admin_role') == 2) { $idClub = session('admin_id_club'); $query->where(function($q) use ($idClub) { $q->whereHas('equipoLocal', function ($q2) use ($idClub) { $q2->where('id_club', $idClub); })->orWhereHas('equipoVisitante', function ($q2) use ($idClub) { $q2->where('id_club', $idClub); }); }); } $eventos = $query->orderBy('id_evento', 'desc')->get(); return view('admin.eventos.index', compact('eventos', 'estado')); } public function eventosCreate(Request $request) { $this->checkSuperAdmin($request); $equipos = Equipo::with('club')->get(); $torneos = \App\Models\Torneo::orderBy('nombre')->get(); $torneoEquipos = DB::table('torneo_equipo')->get(); return view('admin.eventos.form', ['evento' => null, 'equipos' => $equipos, 'torneos' => $torneos, 'torneoEquipos' => $torneoEquipos]); } public function eventosStore(Request $request) { $this->checkSuperAdmin($request); $data = $request->validate([ 'nombre_evento' => 'nullable|string|max:200', 'id_torneo' => 'nullable|integer|exists:torneos,id', 'fecha_evento' => 'required|date', 'hora_inicio' => 'required', 'hora_fin' => 'required', 'sede' => 'required|string|max:200', 'id_equipo_local' => 'required|integer|exists:equipos,id_equipo', 'id_equipo_visitante' => 'required|integer|exists:equipos,id_equipo', 'precio' => 'nullable|numeric|min:0', 'marcador_local' => 'nullable|integer|min:0', 'marcador_visitante' => 'nullable|integer|min:0', ]); // Validaciones Deportivas $local = Equipo::findOrFail($data['id_equipo_local']); $visit = Equipo::findOrFail($data['id_equipo_visitante']); if ($local->categoria != $visit->categoria) { return back()->withInput()->withErrors(['id_equipo_visitante' => 'Los equipos deben pertenecer a la misma categoría.']); } if (!empty($data['id_torneo'])) { $torneo = \App\Models\Torneo::findOrFail($data['id_torneo']); $localInTorneo = $torneo->equipos()->where('equipos.id_equipo', $data['id_equipo_local'])->first(); $visitInTorneo = $torneo->equipos()->where('equipos.id_equipo', $data['id_equipo_visitante'])->first(); if (!$localInTorneo || !$visitInTorneo) { return back()->withInput()->withErrors(['id_torneo' => 'Uno o ambos equipos no están inscritos en este torneo.']); } if ($localInTorneo->pivot->grupo != $visitInTorneo->pivot->grupo) { return back()->withInput()->withErrors(['id_equipo_visitante' => 'Los equipos deben pertenecer al mismo grupo dentro del torneo.']); } } // Autogenerar ID $data['id_evento'] = bin2hex(random_bytes(4)); // 8 caracteres if (empty($data['nombre_evento'])) { $grupoName = 'General'; if (!empty($data['id_torneo'])) { $rel = DB::table('torneo_equipo') ->where('id_torneo', $data['id_torneo']) ->where('id_equipo', $data['id_equipo_local']) ->first(); if ($rel) $grupoName = $rel->grupo ?? 'General'; } $data['nombre_evento'] = ($local->club->nombre ?? 'Local') . ' vs ' . ($visit->club->nombre ?? 'Visitante') . " ({$grupoName})"; } Evento::create($data); return redirect()->route('admin.eventos.index')->with('admin_msg', 'Evento creado correctamente.'); } public function eventosEdit(Request $request, $id) { $this->checkGeneralAdmin($request); $evento = Evento::findOrFail($id); if (session('admin_role') == 2) { $idClub = session('admin_id_club'); $isLocal = $evento->equipoLocal && $evento->equipoLocal->id_club == $idClub; $isVisitante = $evento->equipoVisitante && $evento->equipoVisitante->id_club == $idClub; if (!$isLocal && !$isVisitante) { abort(403, 'No tienes permiso para editar este evento.'); } } $equipos = Equipo::with('club')->get(); $torneos = \App\Models\Torneo::orderBy('nombre')->get(); $torneoEquipos = DB::table('torneo_equipo')->get(); return view('admin.eventos.form', compact('evento', 'equipos', 'torneos', 'torneoEquipos')); } public function eventosUpdate(Request $request, $id) { $this->checkGeneralAdmin($request); $evento = Evento::findOrFail($id); if (session('admin_role') == 2) { $idClub = session('admin_id_club'); $isLocal = $evento->equipoLocal && $evento->equipoLocal->id_club == $idClub; $isVisitante = $evento->equipoVisitante && $evento->equipoVisitante->id_club == $idClub; if (!$isLocal && !$isVisitante) { abort(403, 'No tienes permiso para editar este evento.'); } // Club admins only edit limite_qr_jugador $data = $request->validate([ 'limite_qr_jugador' => 'required|integer|min:0', ]); $evento->update(['limite_qr_jugador' => $data['limite_qr_jugador']]); return redirect()->route('admin.eventos.index')->with('admin_msg', 'Pase de QRs para el evento actualizado.'); } // Si es edición, restringimos los campos permitidos según solicitud del usuario if ($id) { $data = $request->validate([ 'fecha_evento' => 'required|date', 'hora_inicio' => 'required', 'hora_fin' => 'required', 'limite_qr_jugador' => 'nullable|integer|min:0', 'marcador_local' => 'nullable|integer|min:0', 'marcador_visitante' => 'nullable|integer|min:0', 'nombre_evento' => 'nullable|string|max:200', ]); // Conservar valores que no deberían cambiar para que la validación posterior (deporte) no falle $data['id_torneo'] = $evento->id_torneo; $data['id_equipo_local'] = $evento->id_equipo_local; $data['id_equipo_visitante'] = $evento->id_equipo_visitante; // Si el nombre viene vacío en edición, también lo autogeneramos? // El usuario pidió que se setee automáticamente al no poner nada. if (empty($data['nombre_evento'])) { $local = Equipo::findOrFail($data['id_equipo_local']); $visit = Equipo::findOrFail($data['id_equipo_visitante']); $grupoName = 'General'; if (!empty($data['id_torneo'])) { $rel = DB::table('torneo_equipo') ->where('id_torneo', $data['id_torneo']) ->where('id_equipo', $data['id_equipo_local']) ->first(); if ($rel) $grupoName = $rel->grupo ?? 'General'; } $data['nombre_evento'] = ($local->club->nombre ?? 'Local') . ' vs ' . ($visit->club->nombre ?? 'Visitante') . " ({$grupoName})"; } $data['sede'] = $evento->sede; $data['precio'] = $evento->precio; } else { $data = $request->validate([ 'nombre_evento' => 'nullable|string|max:200', 'id_torneo' => 'nullable|integer|exists:torneos,id', 'fecha_evento' => 'required|date', 'hora_inicio' => 'required', 'hora_fin' => 'required', 'sede' => 'nullable|string|max:200', 'id_equipo_local' => 'nullable|integer|exists:equipos,id_equipo', 'id_equipo_visitante' => 'nullable|integer|exists:equipos,id_equipo', 'precio' => 'nullable|numeric|min:0', 'limite_qr_jugador' => 'nullable|integer|min:0', 'marcador_local' => 'nullable|integer|min:0', 'marcador_visitante' => 'nullable|integer|min:0', ]); } // Validaciones Deportivas if ($data['id_equipo_local'] && $data['id_equipo_visitante']) { $local = Equipo::findOrFail($data['id_equipo_local']); $visit = Equipo::findOrFail($data['id_equipo_visitante']); if ($local->categoria != $visit->categoria) { return back()->withInput()->withErrors(['id_equipo_visitante' => 'Los equipos deben pertenecer a la misma categoría.']); } if (!empty($data['id_torneo'])) { $torneo = \App\Models\Torneo::findOrFail($data['id_torneo']); $localInTorneo = $torneo->equipos()->where('equipos.id_equipo', $data['id_equipo_local'])->first(); $visitInTorneo = $torneo->equipos()->where('equipos.id_equipo', $data['id_equipo_visitante'])->first(); if (!$localInTorneo || !$visitInTorneo) { return back()->withInput()->withErrors(['id_torneo' => 'Uno o ambos equipos no están inscritos en este torneo.']); } if ($localInTorneo->pivot->grupo != $visitInTorneo->pivot->grupo) { return back()->withInput()->withErrors(['id_equipo_visitante' => 'Los equipos deben pertenecer al mismo grupo dentro del torneo.']); } } // Autogenerar Nombre si es nulo if (empty($data['nombre_evento'])) { $data['nombre_evento'] = ($local->club->nombre ?? 'Local') . ' vs ' . ($visit->club->nombre ?? 'Visitante'); } } $evento->update($data); return redirect()->route('admin.eventos.index')->with('admin_msg', 'Evento actualizado correctamente.'); } public function eventosDestroy(Request $request, $id) { $this->checkSuperAdmin($request); $evento = Evento::findOrFail($id); // Al eliminar un evento, también eliminamos sus QRs para que no queden "huérfanos" $evento->qrCodes()->delete(); $evento->delete(); return redirect()->route('admin.eventos.index')->with('admin_msg', 'Evento eliminado correctamente.'); } // ══════════════════════════════════ // PROMOCIONES / LUGARES // ══════════════════════════════════ public function promocionesIndex(Request $request) { $this->checkSuperAdmin($request); $promociones = Promocion::withCount('promoQrs')->orderBy('id', 'desc')->get(); return view('admin.promociones.index', compact('promociones')); } public function promocionesCreate(Request $request) { $this->checkSuperAdmin($request); return view('admin.promociones.form', ['promocion' => null]); } public function promocionesStore(Request $request) { $this->checkSuperAdmin($request); $data = $request->validate([ 'nombre' => 'required|string|max:100', 'direccion' => 'required|string|max:150', 'lat' => 'nullable|numeric', 'lng' => 'nullable|numeric', 'contacto' => 'nullable|string|max:100', 'descripcion' => 'nullable', 'descripcion_lugar' => 'nullable', 'categoria' => 'nullable|string|max:50', 'imagen_file' => 'nullable|image|max:2048', ]); if ($request->hasFile('imagen_file')) { $path = app(ImageOptimizer::class)->storeAndOptimize($request->file('imagen_file'), 'promos'); $data['imagen'] = $path; } unset($data['imagen_file']); Promocion::create($data); return redirect()->route('admin.promociones.index')->with('admin_msg', 'Promoción creada correctamente.'); } public function promocionesEdit(Request $request, $id) { $this->checkSuperAdmin($request); $promocion = Promocion::findOrFail($id); return view('admin.promociones.form', compact('promocion')); } public function promocionesUpdate(Request $request, $id) { $this->checkSuperAdmin($request); $promocion = Promocion::findOrFail($id); $data = $request->validate([ 'nombre' => 'required|string|max:100', 'direccion' => 'required|string|max:150', 'lat' => 'nullable|numeric', 'lng' => 'nullable|numeric', 'contacto' => 'nullable|string|max:100', 'descripcion' => 'nullable', 'descripcion_lugar' => 'nullable', 'categoria' => 'nullable|string|max:50', 'imagen_file' => 'nullable|image|max:2048', ]); if ($request->hasFile('imagen_file')) { $path = app(ImageOptimizer::class)->storeAndOptimize($request->file('imagen_file'), 'promos'); $data['imagen'] = $path; } unset($data['imagen_file']); $promocion->update($data); return redirect()->route('admin.promociones.index')->with('admin_msg', 'Promoción actualizada correctamente.'); } public function promocionesDestroy(Request $request, $id) { $this->checkSuperAdmin($request); $promocion = Promocion::findOrFail($id); $promocion->delete(); return redirect()->route('admin.promociones.index')->with('admin_msg', 'Promoción eliminada correctamente.'); } // ══════════════════════════════════ // NOTICIAS // ══════════════════════════════════ public function noticiasIndex(Request $request) { $this->checkSuperAdmin($request); $noticias = Noticia::orderBy('fecha', 'desc')->orderBy('id', 'desc')->get(); return view('admin.noticias.index', compact('noticias')); } public function noticiasCreate(Request $request) { $this->checkSuperAdmin($request); $torneos = \App\Models\Torneo::orderBy('nombre')->get(); return view('admin.noticias.form', ['noticia' => null, 'torneos' => $torneos]); } public function noticiasStore(Request $request) { $this->checkSuperAdmin($request); $data = $request->validate([ 'titulo' => 'required|string|max:200', 'contenido' => 'required', 'imagen_file' => 'nullable|image|max:5120', 'categoria' => 'nullable|string|max:50', 'id_torneo' => 'nullable|integer|exists:torneos,id', ]); if ($request->hasFile('imagen_file')) { $path = app(ImageOptimizer::class)->storeAndOptimize($request->file('imagen_file'), 'noticias'); $data['imagen'] = 'storage/' . $path; } unset($data['imagen_file']); $data['fecha'] = now(); Noticia::create($data); return redirect()->route('admin.noticias.index')->with('admin_msg', 'Noticia creada correctamente.'); } public function noticiasEdit(Request $request, $id) { $this->checkSuperAdmin($request); $noticia = Noticia::findOrFail($id); $torneos = \App\Models\Torneo::orderBy('nombre')->get(); return view('admin.noticias.form', compact('noticia', 'torneos')); } public function noticiasUpdate(Request $request, $id) { $this->checkSuperAdmin($request); $noticia = Noticia::findOrFail($id); $data = $request->validate([ 'titulo' => 'required|string|max:200', 'contenido' => 'required', 'imagen_file' => 'nullable|image|max:5120', 'categoria' => 'nullable|string|max:50', 'id_torneo' => 'nullable|integer|exists:torneos,id', ]); if ($request->hasFile('imagen_file')) { // Eliminar imagen anterior si existe if ($noticia->imagen && !str_starts_with($noticia->imagen, 'http') && file_exists(public_path($noticia->imagen))) { @unlink(public_path($noticia->imagen)); } $path = app(ImageOptimizer::class)->storeAndOptimize($request->file('imagen_file'), 'noticias'); $data['imagen'] = 'storage/' . $path; } unset($data['imagen_file']); $noticia->update($data); return redirect()->route('admin.noticias.index')->with('admin_msg', 'Noticia actualizada correctamente.'); } public function noticiasDestroy(Request $request, $id) { $this->checkSuperAdmin($request); $noticia = Noticia::findOrFail($id); $noticia->delete(); return redirect()->route('admin.noticias.index')->with('admin_msg', 'Noticia eliminada correctamente.'); } // ══════════════════════════════════ // ESCANEAR / VALIDAR QR // ══════════════════════════════════ public function validarQr(Request $request) { $this->checkGeneralAdmin($request); $id_qr = $request->input('id_qr'); $id_evento = $request->input('id_evento'); if (!$id_qr) { return response()->json(['valid' => false, 'message' => 'QR inválido.']); } $qr = QrCode::with('evento', 'jugador', 'aficionado')->where('id_qr', $id_qr)->first(); if (!$qr) { return response()->json(['valid' => false, 'message' => 'QR no encontrado.']); } // Si se seleccionó un evento, exigir que coincida if ($id_evento && (string)$qr->id_evento !== (string)$id_evento) { return response()->json(['valid' => false, 'message' => 'Este QR corresponde a otro evento.']); } // Verificar vigencia del evento if ($qr->evento) { $ahora = \Carbon\Carbon::now(); $f = ($qr->evento->fecha_evento instanceof \Carbon\Carbon) ? $qr->evento->fecha_evento->format('Y-m-d') : substr($qr->evento->fecha_evento, 0, 10); $h1 = ($qr->evento->hora_inicio instanceof \Carbon\Carbon) ? $qr->evento->hora_inicio->format('H:i:s') : $qr->evento->hora_inicio; $h2 = ($qr->evento->hora_fin instanceof \Carbon\Carbon) ? $qr->evento->hora_fin->format('H:i:s') : $qr->evento->hora_fin; $inicio = \Carbon\Carbon::parse("$f $h1"); $fin = \Carbon\Carbon::parse("$f $h2"); if ($fin->lessThanOrEqualTo($inicio)) { $fin->addDay(); } if ($ahora < $inicio) { return response()->json(['valid' => false, 'message' => '⏳ El evento todavía no comenzó.']); } if ($ahora > $fin) { return response()->json(['valid' => false, 'message' => 'Evento finalizado, QR inválido.']); } } // Verificar escaneos restantes if ((int)$qr->escaneos_restantes <= 0) { return response()->json(['valid' => false, 'message' => '❌ QR ya utilizado.']); } // Decrementar escaneos (concurrency-safe via DB) $affected = \Illuminate\Support\Facades\DB::table('qr_codes') ->where('id_qr', $id_qr) ->where('escaneos_restantes', '>', 0) ->when($id_evento, function ($query) use ($id_evento) { return $query->where('id_evento', $id_evento); }) ->decrement('escaneos_restantes'); if ($affected === 0) { return response()->json(['valid' => false, 'message' => '❌ QR ya utilizado.']); } // Info del titular según tipo de QR $titular = ''; $categoriaNombre = ''; if ($qr->jugador) { $categoriaNombre = $qr->jugador->categoria_calculada; } if ($qr->tipo_qr === 'invitado' && $qr->jugador) { $titular = 'Jugador: ' . $qr->jugador->nombre . ' ' . $qr->jugador->apellido . ' (' . $categoriaNombre . ')'; } elseif ($qr->tipo_qr === 'publico') { if ($qr->aficionado) { $titular = 'Aficionado: ' . $qr->aficionado->nombre . ' ' . $qr->aficionado->apellido; } else { $titular = 'QR de entrada pública (sin referencia)'; } } else { $titular = $qr->jugador ? $qr->jugador->nombre . ' ' . $qr->jugador->apellido . ' (' . $categoriaNombre . ')' : ($qr->aficionado ? $qr->aficionado->nombre . ' ' . $qr->aficionado->apellido : 'Desconocido'); } $mensajeValido = '✅ Acceso válido — Jugador del evento'; if ($qr->tipo_qr === 'libre_50') { $mensajeValido = '✅ Tenés descuento en la entrada, 50% (Jugador Categoría Libre)'; } return response()->json([ 'valid' => true, 'message' => $mensajeValido, 'data' => [ 'evento' => $qr->evento ? $qr->evento->nombre_evento : $qr->id_evento, 'titular' => $titular, 'categoria' => $categoriaNombre, 'tipo' => $qr->tipo_qr, 'restantes' => ($qr->escaneos_restantes > 0 ? $qr->escaneos_restantes - 1 : 0), ], ]); } // ══════════════════════════════════ // CONFIGURACIÓN GENERAL // ══════════════════════════════════ public function settingsIndex(Request $request) { $this->checkSuperAdmin($request); $diasExpiracion = \App\Models\Configuracion::get('dias_expiracion_eventos', 30); $backupFreq = \App\Models\Configuracion::get('backup_frequency', 'daily'); $emailReportes = \App\Models\Configuracion::get('email_reportes', 'asociados@onapb.com'); $lastRun = \App\Models\Configuracion::get('last_scheduler_run', 'Nunca detectado'); return view('admin.settings', compact('diasExpiracion', 'backupFreq', 'lastRun', 'emailReportes')); } public function settingsUpdate(Request $request) { $this->checkSuperAdmin($request); $data = $request->validate([ 'dias_expiracion_eventos' => 'required|integer|min:1', 'backup_frequency' => 'required|string|in:daily,weekly,monthly', 'email_reportes' => 'required|email', ]); \App\Models\Configuracion::set('dias_expiracion_eventos', $data['dias_expiracion_eventos'], 'Días de antigüedad para borrar eventos y QRs'); \App\Models\Configuracion::set('backup_frequency', $data['backup_frequency'], 'Frecuencia de backups automáticos (daily, weekly, monthly)'); \App\Models\Configuracion::set('email_reportes', $data['email_reportes'], 'Email principal para recibir reportes del sistema'); return back()->with('admin_msg', 'Configuración actualizada correctamente.'); } public function runManualTask(Request $request) { $this->checkSuperAdmin($request); $command = $request->input('command'); try { switch ($command) { case 'cleanup': \Illuminate\Support\Facades\Artisan::call('app:cleanup-old-events'); $msg = '✅ Tarea de limpieza ejecutada.'; break; case 'backup': // Usamos backup:run para forzar un backup completo \Illuminate\Support\Facades\Artisan::call('backup:run'); $msg = '✅ Proceso de backup iniciado.'; break; case 'report': \Illuminate\Support\Facades\Artisan::call('reportes:semanal'); $msg = '✅ Informe semanal enviado.'; break; default: throw new \Exception('Comando no reconocido.'); } return back()->with('admin_msg', $msg . ' (Salida: ' . \Illuminate\Support\Facades\Artisan::output() . ')'); } catch (\Exception $e) { \Illuminate\Support\Facades\Log::error("Error ejecutando comando manual: " . $e->getMessage()); return back()->with('admin_error', 'Error al ejecutar tarea: ' . $e->getMessage()); } } // ══════════════════════════════════ // SPONSORS // ══════════════════════════════════ public function sponsorsIndex(Request $request) { $this->checkSuperAdmin($request); $sponsors = Sponsor::orderBy('orden')->latest()->get(); return view('admin.sponsors.index', compact('sponsors')); } public function sponsorsCreate(Request $request) { $this->checkSuperAdmin($request); return view('admin.sponsors.form'); } public function sponsorsStore(Request $request) { $this->checkSuperAdmin($request); $data = $request->validate([ 'nombre' => 'required|string|max:100', 'imagen' => 'required|image|max:5120', 'url' => 'nullable|url|max:255', 'activo' => 'nullable|boolean', 'orden' => 'nullable|integer', ]); if ($request->hasFile('imagen')) { $path = app(ImageOptimizer::class)->storeAndOptimize($request->file('imagen'), 'sponsors'); $data['imagen'] = 'storage/' . $path; } $data['activo'] = $request->has('activo'); $data['orden'] = $data['orden'] ?? 0; Sponsor::create($data); return redirect()->route('admin.sponsors.index')->with('admin_msg', 'Sponsor creado correctamente.'); } public function sponsorsEdit(Request $request, $id) { $this->checkSuperAdmin($request); $sponsor = Sponsor::findOrFail($id); return view('admin.sponsors.form', compact('sponsor')); } public function sponsorsUpdate(Request $request, $id) { $this->checkSuperAdmin($request); $sponsor = Sponsor::findOrFail($id); $data = $request->validate([ 'nombre' => 'required|string|max:100', 'imagen' => 'nullable|image|max:5120', 'url' => 'nullable|url|max:255', 'activo' => 'nullable|boolean', 'orden' => 'nullable|integer', ]); if ($request->hasFile('imagen')) { // Eliminar imagen anterior si existe if ($sponsor->imagen && file_exists(public_path($sponsor->imagen))) { @unlink(public_path($sponsor->imagen)); } $path = app(ImageOptimizer::class)->storeAndOptimize($request->file('imagen'), 'sponsors'); $data['imagen'] = 'storage/' . $path; } $data['activo'] = $request->has('activo'); $data['orden'] = $data['orden'] ?? 0; $sponsor->update($data); return redirect()->route('admin.sponsors.index')->with('admin_msg', 'Sponsor actualizado correctamente.'); } public function sponsorsDestroy(Request $request, $id) { $this->checkSuperAdmin($request); $sponsor = Sponsor::findOrFail($id); if ($sponsor->imagen && file_exists(public_path($sponsor->imagen))) { @unlink(public_path($sponsor->imagen)); } $sponsor->delete(); return redirect()->route('admin.sponsors.index')->with('admin_msg', 'Sponsor eliminado correctamente.'); } // ══════════════════════════════════ // TORNEOS // ══════════════════════════════════ public function torneosIndex(Request $request) { $this->checkSuperAdmin($request); $torneos = \App\Models\Torneo::withCount('equipos')->latest()->get(); return view('admin.torneos.index', compact('torneos')); } public function torneosCreate(Request $request) { $this->checkSuperAdmin($request); return view('admin.torneos.form', ['torneo' => null]); } public function torneosStore(Request $request) { $this->checkSuperAdmin($request); $data = $request->validate([ 'nombre' => 'required|string|max:100', 'fecha_inicio' => 'nullable|date', 'fecha_fin' => 'nullable|date', ]); \App\Models\Torneo::create($data); return redirect()->route('admin.torneos.index')->with('admin_msg', 'Torneo creado correctamente.'); } public function torneosEdit(Request $request, $id) { $this->checkSuperAdmin($request); $torneo = \App\Models\Torneo::with('equipos.club')->findOrFail($id); $clubes = Club::with('equipos')->orderBy('nombre')->get(); return view('admin.torneos.form', compact('torneo', 'clubes')); } public function torneosUpdate(Request $request, $id) { $this->checkSuperAdmin($request); $torneo = \App\Models\Torneo::findOrFail($id); $data = $request->validate([ 'nombre' => 'required|string|max:100', 'fecha_inicio' => 'nullable|date', 'fecha_fin' => 'nullable|date', ]); $torneo->update($data); return redirect()->route('admin.torneos.index')->with('admin_msg', 'Torneo actualizado correctamente.'); } public function torneosDestroy(Request $request, $id) { $this->checkSuperAdmin($request); $torneo = \App\Models\Torneo::findOrFail($id); $torneo->delete(); return redirect()->route('admin.torneos.index')->with('admin_msg', 'Torneo eliminado correctamente.'); } public function torneoAddEquipo(Request $request, $id) { $this->checkSuperAdmin($request); $torneo = \App\Models\Torneo::findOrFail($id); $data = $request->validate([ 'id_equipo' => 'required|integer|exists:equipos,id_equipo', 'grupo' => 'nullable|string|max:50', ]); if ($torneo->equipos()->where('torneo_equipo.id_equipo', $data['id_equipo'])->exists()) { return back()->with('admin_error', 'El equipo ya está asignado a este torneo.'); } $torneo->equipos()->attach($data['id_equipo'], ['grupo' => $data['grupo']]); return back()->with('admin_msg', 'Equipo asignado al torneo correctamente.'); } public function torneoRemoveEquipo($id, $id_equipo) { $this->checkSuperAdmin(request()); $torneo = \App\Models\Torneo::findOrFail($id); $torneo->equipos()->detach($id_equipo); return back()->with('admin_msg', 'Equipo removido del torneo.'); } public function eventosStats($id) { $this->checkGeneralAdmin(request()); $evento = Evento::with(['equipoLocal.jugadores', 'equipoVisitante.jugadores'])->findOrFail($id); // Restricción para Administradores de Club: Solo partidos donde participa su club if (session('admin_role') == 2) { $idClub = session('admin_id_club'); if ($evento->equipoLocal->id_club != $idClub && $evento->equipoVisitante->id_club != $idClub) { abort(403, 'No tienes permiso para gestionar estadísticas de este partido.'); } } $stats = \App\Models\EventoJugador::where('id_evento', $id)->get()->keyBy('id_jugador'); return view('admin.eventos.stats', compact('evento', 'stats')); } public function eventosStatsStore(Request $request, $id) { $this->checkGeneralAdmin($request); // Validación de seguridad para Edición if (session('admin_role') == 2) { $evento = Evento::findOrFail($id); $idClub = session('admin_id_club'); if ($evento->equipoLocal->id_club != $idClub && $evento->equipoVisitante->id_club != $idClub) { abort(403, 'No tienes permiso para editar estadísticas de este partido.'); } } $request->validate([ 'stats' => 'required|array', 'stats.*.puntos' => 'required|integer|min:0', 'stats.*.faltas' => 'required|integer|min:0|max:5', ]); foreach ($request->stats as $id_jugador => $vals) { // Si es Admin de Club, solo puede guardar sus propios jugadores if (session('admin_role') == 2) { $jugador = Jugador::find($id_jugador); if (!$jugador || $jugador->id_club_actual != session('admin_id_club')) { continue; } } \App\Models\EventoJugador::updateOrCreate( ['id_evento' => $id, 'id_jugador' => $id_jugador], ['puntos' => $vals['puntos'], 'faltas' => $vals['faltas']] ); } return redirect()->route('admin.eventos.index')->with('admin_msg', 'Estadísticas guardadas correctamente.'); } }