Files
OnAPB-Carrere_Demartin/app/Http/Controllers/AdminController.php
T
Laucha1312 8fc619f9e7 Ahora si(?
2026-06-04 15:01:53 -03:00

1668 lines
68 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Services\ImageOptimizer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use App\Models\Club;
use App\Models\Equipo;
use App\Models\Jugador;
use App\Models\Evento;
use App\Models\Promocion;
use App\Models\Sponsor;
use App\Models\Noticia;
use App\Models\QrCode;
class AdminController extends Controller
{
// ──────── Middleware check ────────
private function checkSuperAdmin(Request $request)
{
if (!session('admin_logged_in') || session('admin_role') != 1) {
abort(403, 'Acceso denegado. Solo Súper Administradores.');
}
}
private function checkGeneralAdmin(Request $request)
{
if (!session('admin_logged_in') || !in_array(session('admin_role'), [1, 2])) {
abort(403, 'Acceso denegado');
}
}
// ══════════════════════════════════
// DASHBOARD
// ══════════════════════════════════
public function dashboard(Request $request)
{
$this->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.');
}
}