Ahora si :)))))))))))))))))

This commit is contained in:
Laucha1312
2026-06-04 15:10:18 -03:00
parent 47408d49fc
commit 1a8c01ae45
167 changed files with 15870 additions and 0 deletions
@@ -0,0 +1,115 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\AdminUser;
use App\Models\Club;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rule;
class AdminUserController extends Controller
{
private function checkSuperAdmin(Request $request)
{
if (!session('admin_logged_in') || session('admin_role') != 1) {
abort(403, 'Acceso denegado. Solo Súper Administradores.');
}
}
public function index(Request $request)
{
$this->checkSuperAdmin($request);
$usuarios = AdminUser::with('club')->orderBy('id', 'desc')->paginate(20);
return view('admin.usuarios.index', compact('usuarios'));
}
public function create(Request $request)
{
$this->checkSuperAdmin($request);
$usuario = null;
$clubes = Club::orderBy('nombre')->get();
return view('admin.usuarios.form', compact('usuario', 'clubes'));
}
public function store(Request $request)
{
$this->checkSuperAdmin($request);
$data = $request->validate([
'username' => 'required|string|max:50|unique:admin_users',
'password' => 'required|string|min:6',
'role' => 'required|integer|in:1,2',
'id_club' => 'nullable|integer|exists:clubes,id_club'
]);
if ($data['role'] == 2 && empty($data['id_club'])) {
return back()->withErrors(['id_club' => 'Si el rol es Admin de Club, se requiere un club asociado.'])->withInput();
}
if ($data['role'] == 1) {
$data['id_club'] = null; // Superadmins no pertenecen a un club específico en este contexto
}
$data['password'] = Hash::make($data['password']);
AdminUser::create($data);
return redirect()->route('admin.usuarios.index')->with('admin_msg', 'Administrador creado exitosamente.');
}
public function edit(Request $request, $id)
{
$this->checkSuperAdmin($request);
$usuario = AdminUser::findOrFail($id);
$clubes = Club::orderBy('nombre')->get();
return view('admin.usuarios.form', compact('usuario', 'clubes'));
}
public function update(Request $request, $id)
{
$this->checkSuperAdmin($request);
$usuario = AdminUser::findOrFail($id);
$data = $request->validate([
'username' => ['required', 'string', 'max:50', Rule::unique('admin_users')->ignore($usuario->id)],
'password' => 'nullable|string|min:6',
'role' => 'required|integer|in:1,2',
'id_club' => 'nullable|integer|exists:clubes,id_club'
]);
if ($data['role'] == 2 && empty($data['id_club'])) {
return back()->withErrors(['id_club' => 'Si el rol es Admin de Club, se requiere un club asociado.'])->withInput();
}
if ($data['role'] == 1) {
$data['id_club'] = null;
}
if (!empty($data['password'])) {
$data['password'] = Hash::make($data['password']);
} else {
unset($data['password']);
}
$usuario->update($data);
return redirect()->route('admin.usuarios.index')->with('admin_msg', 'Administrador actualizado exitosamente.');
}
public function destroy(Request $request, $id)
{
$this->checkSuperAdmin($request);
$usuario = AdminUser::findOrFail($id);
if ($usuario->id == session('admin_id')) {
return back()->with('admin_error', 'No puedes eliminar tu propio usuario.');
}
$usuario->delete();
return redirect()->route('admin.usuarios.index')->with('admin_msg', 'Administrador eliminado.');
}
}
@@ -0,0 +1,113 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\CarouselItem;
use App\Services\ImageOptimizer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
class CarouselItemController extends Controller
{
private function checkGeneralAdmin(Request $request): void
{
if (!session('admin_logged_in') || !in_array(session('admin_role'), [1, 2])) {
abort(403, 'Acceso denegado');
}
}
public function index(Request $request)
{
$this->checkGeneralAdmin($request);
$items = CarouselItem::orderBy('orden', 'asc')->latest()->get();
return view('admin.carousel.index', compact('items'));
}
public function create(Request $request)
{
$this->checkGeneralAdmin($request);
return view('admin.carousel.create');
}
public function store(Request $request)
{
$this->checkGeneralAdmin($request);
$request->validate([
'titulo' => 'nullable|string|max:255',
'subtitulo' => 'nullable|string|max:255',
'boton_texto' => 'nullable|string|max:255',
'boton_enlace' => 'nullable|string|max:255',
'imagen' => 'required|image|mimes:jpeg,png,jpg,webp|max:5120',
'orden' => 'nullable|integer',
'activo' => 'nullable|boolean',
]);
$data = $request->except('imagen');
$data['activo'] = $request->has('activo');
if ($request->hasFile('imagen')) {
$path = app(ImageOptimizer::class)->storeAndOptimize($request->file('imagen'), 'carousel');
$data['imagen'] = 'storage/' . $path;
}
CarouselItem::create($data);
return redirect()->route('admin.carousel.index')->with('admin_msg', 'Slide creado exitosamente.');
}
public function edit(Request $request, CarouselItem $carouselItem)
{
$this->checkGeneralAdmin($request);
return view('admin.carousel.edit', compact('carouselItem'));
}
public function update(Request $request, CarouselItem $carouselItem)
{
$this->checkGeneralAdmin($request);
$request->validate([
'titulo' => 'nullable|string|max:255',
'subtitulo' => 'nullable|string|max:255',
'boton_texto' => 'nullable|string|max:255',
'boton_enlace' => 'nullable|string|max:255',
'imagen' => 'nullable|image|mimes:jpeg,png,jpg,webp|max:5120',
'orden' => 'nullable|integer',
'activo' => 'nullable|boolean',
]);
$data = $request->except('imagen');
$data['activo'] = $request->has('activo');
if ($request->hasFile('imagen')) {
// Eliminar imagen anterior si existe
if ($carouselItem->imagen) {
$oldPath = public_path($carouselItem->imagen);
if (File::exists($oldPath)) {
File::delete($oldPath);
}
}
$path = app(ImageOptimizer::class)->storeAndOptimize($request->file('imagen'), 'carousel');
$data['imagen'] = 'storage/' . $path;
}
$carouselItem->update($data);
return redirect()->route('admin.carousel.index')->with('admin_msg', 'Slide actualizado exitosamente.');
}
public function destroy(Request $request, CarouselItem $carouselItem)
{
$this->checkGeneralAdmin($request);
if ($carouselItem->imagen) {
$imagePath = public_path($carouselItem->imagen);
if (File::exists($imagePath)) {
File::delete($imagePath);
}
}
$carouselItem->delete();
return redirect()->route('admin.carousel.index')->with('admin_msg', 'Slide eliminado exitosamente.');
}
}
@@ -0,0 +1,74 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Categoria;
class CategoriaController extends Controller
{
private function checkSuperAdmin(Request $request)
{
if (!session('admin_logged_in') || session('admin_role') != 1) {
abort(403, 'Acceso denegado. Solo Súper Administradores.');
}
}
public function index(Request $request)
{
$this->checkSuperAdmin($request);
$categorias = Categoria::latest()->get();
return view('admin.categorias.index', compact('categorias'));
}
public function create(Request $request)
{
$this->checkSuperAdmin($request);
return view('admin.categorias.form', ['categoria' => null]);
}
public function store(Request $request)
{
$this->checkSuperAdmin($request);
$data = $request->validate([
'nombre' => 'required|string|max:50',
'edad_min' => 'required|integer',
'edad_max' => 'required|integer|gte:edad_min',
'genero' => 'nullable|string|max:20',
]);
$data['es_libre'] = $request->has('es_libre');
Categoria::create($data);
return redirect()->route('admin.categorias.index')->with('admin_msg', 'Categoría creada.');
}
public function edit(Request $request, $id)
{
$this->checkSuperAdmin($request);
$categoria = Categoria::findOrFail($id);
return view('admin.categorias.form', compact('categoria'));
}
public function update(Request $request, $id)
{
$this->checkSuperAdmin($request);
$categoria = Categoria::findOrFail($id);
$data = $request->validate([
'nombre' => 'required|string|max:50',
'edad_min' => 'required|integer',
'edad_max' => 'required|integer|gte:edad_min',
'genero' => 'nullable|string|max:20',
]);
$data['es_libre'] = $request->has('es_libre');
$categoria->update($data);
return redirect()->route('admin.categorias.index')->with('admin_msg', 'Categoría actualizada.');
}
public function destroy(Request $request, $id)
{
$this->checkSuperAdmin($request);
$categoria = Categoria::findOrFail($id);
$categoria->delete();
return redirect()->route('admin.categorias.index')->with('admin_msg', 'Categoría eliminada.');
}
}
@@ -0,0 +1,380 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Torneo;
use App\Services\FixtureService;
class FixtureController extends Controller
{
private FixtureService $fixtureService;
public function __construct(FixtureService $fixtureService)
{
$this->fixtureService = $fixtureService;
}
private function checkSuperAdmin()
{
if (!session('admin_logged_in') || session('admin_role') != 1) {
abort(403, 'Solo Súper Administradores pueden generar fixtures.');
}
}
/**
* POST /admin/torneos/{id}/generar-fixture — Preview del fixture
*/
public function preview(Request $request, int $id)
{
$this->checkSuperAdmin();
$torneo = Torneo::with('equipos.club')->findOrFail($id);
$request->validate([
'fecha_inicio' => 'required|date|after_or_equal:today',
'dias_entre_jornadas' => 'required|integer|min:1|max:60',
'sede_default' => 'nullable|string|max:200',
'doble_rueda' => 'nullable|boolean',
]);
try {
$partidos = $this->fixtureService->generarRoundRobin(
torneo: $torneo,
fechaInicio: $request->input('fecha_inicio'),
diasEntreJornadas: (int) $request->input('dias_entre_jornadas', 7),
sedeDefault: $request->input('sede_default', ''),
dobleRueda: (bool) $request->input('doble_rueda', false),
);
// Enriquecer con nombres para la vista
$partidosEnriquecidos = collect($partidos)->map(function ($p) {
$local = \App\Models\Equipo::with('club')->find($p['id_equipo_local']);
$visitante = \App\Models\Equipo::with('club')->find($p['id_equipo_visitante']);
return array_merge($p, [
'nombre_local' => ($local->club->nombre ?? '?') . ' (' . ($local->categoria ?? '') . ')',
'nombre_visitante' => ($visitante->club->nombre ?? '?') . ' (' . ($visitante->categoria ?? '') . ')',
]);
})->toArray();
} catch (\InvalidArgumentException $e) {
return back()->with('admin_error', $e->getMessage());
}
$fixtureParams = [
'fecha_inicio' => $request->input('fecha_inicio'),
'dias_entre_jornadas' => $request->input('dias_entre_jornadas', 7),
'sede_default' => $request->input('sede_default', ''),
'doble_rueda' => (bool) $request->input('doble_rueda', false),
];
return view('admin.torneos.fixture_preview', compact('torneo', 'partidosEnriquecidos', 'fixtureParams'));
}
/**
* POST /admin/torneos/{id}/confirmar-fixture — Persiste el fixture
*/
public function confirmar(Request $request, int $id)
{
$this->checkSuperAdmin();
$torneo = Torneo::with('equipos.club')->findOrFail($id);
$request->validate([
'fecha_inicio' => 'required|date',
'dias_entre_jornadas' => 'required|integer|min:1',
'sede_default' => 'nullable|string|max:200',
'doble_rueda' => 'nullable|boolean',
]);
try {
$partidos = $this->fixtureService->generarRoundRobin(
torneo: $torneo,
fechaInicio: $request->input('fecha_inicio'),
diasEntreJornadas: (int) $request->input('dias_entre_jornadas', 7),
sedeDefault: $request->input('sede_default', ''),
dobleRueda: (bool) $request->input('doble_rueda', false),
);
$creados = $this->fixtureService->persistirFixture($partidos, $torneo);
} catch (\InvalidArgumentException $e) {
return back()->with('admin_error', $e->getMessage());
}
return redirect()->route('admin.torneos.edit', $id)
->with('admin_msg', "✅ Fixture generado correctamente: {$creados} partidos creados.");
}
public function importForm(int $id)
{
$this->checkSuperAdmin();
$torneo = Torneo::findOrFail($id);
return view('admin.torneos.importar', compact('torneo'));
}
public function importStore(Request $request, int $id)
{
$this->checkSuperAdmin();
$torneo = Torneo::with(['equipos.club'])->findOrFail($id);
$request->validate([
'texto_importar' => 'required|string',
]);
$lineas = explode("\n", $request->input('texto_importar'));
$creados = 0;
$errores = [];
foreach ($lineas as $index => $linea) {
$linea = trim($linea);
if (empty($linea)) continue;
try {
// Formato esperado: Fecha, ClubL, CatL, ClubV, CatV, MarcadorL, MarcadorV, Sede, Grupo
$partes = str_getcsv($linea);
if (count($partes) < 7) {
throw new \Exception("Formato insuficiente. Se esperan al menos 7 columnas.");
}
$fechaRaw = trim($partes[0]);
$fecha = \Carbon\Carbon::parse(str_replace('/', '-', $fechaRaw))->format('Y-m-d');
$nombreClubL = trim($partes[1]);
$catL = trim($partes[2]);
$nombreClubV = trim($partes[3]);
$catV = trim($partes[4]);
$marcadorL = trim($partes[5]);
$marcadorV = trim($partes[6]);
$sede = $partes[7] ?? null;
$grupo = $partes[8] ?? null;
// Buscar equipos con validación de grupo (prioridad) y categoría
$equipoL = $this->buscarEquipo($nombreClubL, $catL, $torneo, $grupo);
$equipoV = $this->buscarEquipo($nombreClubV, $catV, $torneo, $grupo);
if (!$equipoL || !$equipoV) {
$missing = !$equipoL ? "'{$nombreClubL} ({$catL})'" : "'{$nombreClubV} ({$catV})'";
throw new \Exception("No se encontró el equipo {$missing} en el grupo '" . ($grupo ?? 'N/A') . "' ni por categoría.");
}
\App\Models\Evento::create([
'id_evento' => uniqid('ev_imp_'),
'id_torneo' => $id,
'fase' => \App\Models\Evento::FASE_REGULAR,
'fecha_evento' => $fecha,
'id_equipo_local' => $equipoL->id_equipo,
'id_equipo_visitante' => $equipoV->id_equipo,
'marcador_local' => $marcadorL !== '' ? (int)$marcadorL : null,
'marcador_visitante' => $marcadorV !== '' ? (int)$marcadorV : null,
'nombre_evento' => $equipoL->club->nombre . ' vs ' . $equipoV->club->nombre,
'hora_inicio' => '19:00:00',
'hora_fin' => '21:00:00',
'sede' => $sede,
'precio' => 0,
]);
$creados++;
} catch (\Exception $e) {
$errores[] = "Línea " . ($index + 1) . ": " . $e->getMessage();
continue;
}
}
$msg = "Se importaron {$creados} partidos exitosamente.";
if (count($errores) > 0) {
return back()->with('admin_msg', $msg)->with('admin_error', "Se detectaron errores en " . count($errores) . " líneas:<br>" . implode("<br>", $errores));
}
return redirect()->route('admin.torneos.edit', $id)->with('admin_msg', $msg);
}
private function buscarEquipo(string $clubName, string $category, Torneo $torneo, ?string $grupo = null)
{
$clubNameNorm = $this->normalizeString($clubName);
$categoryNorm = $this->normalizeString($category);
$grupoNorm = $grupo ? $this->normalizeString($grupo) : null;
// PASO 1: Prioridad absoluta al GRUPO (vía pivot)
if ($grupoNorm) {
$match = $torneo->equipos->first(function($e) use ($clubNameNorm, $grupoNorm) {
$eClubName = $this->normalizeString($e->club->nombre ?? '');
$eGrupo = $this->normalizeString($e->pivot->grupo ?? '');
return ($eGrupo === $grupoNorm) &&
(strpos($eClubName, $clubNameNorm) !== false || strpos($clubNameNorm, $eClubName) !== false);
});
if ($match) return $match;
}
// PASO 2: Fallback a búsqueda por CATEGORÍA si el grupo no coincide o no se especificó
return $torneo->equipos->first(function($e) use ($clubNameNorm, $categoryNorm) {
$eClubName = $this->normalizeString($e->club->nombre ?? '');
$eCategory = $this->normalizeString($e->categoria ?? '');
$matchClub = (strpos($eClubName, $clubNameNorm) !== false || strpos($clubNameNorm, $eClubName) !== false);
$matchCat = (strpos($eCategory, $categoryNorm) !== false || strpos($categoryNorm, $eCategory) !== false);
return $matchClub && $matchCat;
});
}
private function normalizeString(?string $str): string
{
if (!$str) return '';
$str = mb_strtolower(trim($str), 'UTF-8');
$str = str_replace(
['á', 'é', 'í', 'ó', 'ú', 'ü', 'ñ'],
['a', 'e', 'i', 'o', 'u', 'u', 'n'],
$str
);
// Eliminar caracteres no alfanuméricos para un matching más flexible
return preg_replace('/[^a-z0-9]/', '', $str);
}
public function generarPlayoffs(Request $request, int $id)
{
$this->checkSuperAdmin();
$torneo = Torneo::findOrFail($id);
$grupo = $request->input('grupo');
$formato = (int) $request->input('formato', 1); // 1, 3, 5
if (!$grupo) {
return back()->with('admin_error', 'Debes seleccionar un grupo para generar los playoffs.');
}
$ts = new \App\Services\TournamentService();
$standings = $ts->getStandings($id, true);
if (!isset($standings[$grupo])) {
return back()->with('admin_error', "No hay equipos en el grupo {$grupo}.");
}
$top8 = array_slice($standings[$grupo], 0, 8);
if (count($top8) < 2) {
return back()->with('admin_error', "Se necesitan al menos 2 equipos para generar playoffs.");
}
// Determinar emparejamientos (1 vs 8, 4 vs 5, 2 vs 7, 3 vs 6)
// Usamos el seeding oficial: 1v8, 4v5, 2v7, 3v6
$parejas = [
['local' => $top8[0]['id'], 'visit' => $top8[7]['id'] ?? null, 'nro' => 1],
['local' => $top8[3]['id'] ?? null, 'visit' => $top8[4]['id'] ?? null, 'nro' => 2],
['local' => $top8[1]['id'] ?? null, 'visit' => $top8[6]['id'] ?? null, 'nro' => 3],
['local' => $top8[2]['id'] ?? null, 'visit' => $top8[5]['id'] ?? null, 'nro' => 4],
];
$count = 0;
foreach ($parejas as $p) {
if (!$p['local'] || !$p['visit']) continue;
$local = \App\Models\Equipo::with('club')->find($p['local']);
$visit = \App\Models\Equipo::with('club')->find($p['visit']);
// Crear N partidos según el formato
for ($i = 0; $i < $formato; $i++) {
\App\Models\Evento::create([
'id_evento' => uniqid('ply_'),
'id_torneo' => $id,
'fase' => \App\Models\Evento::FASE_CUARTOS,
'numero_partido_bracket' => $p['nro'],
'id_equipo_local' => $p['local'],
'id_equipo_visitante' => $p['visit'],
'nombre_evento' => "4tos (J".($i+1)."): " . $local->club->nombre . " vs " . $visit->club->nombre . " ({$grupo})",
'fecha_evento' => now()->addDays(7 + ($i * 3))->format('Y-m-d'),
'hora_inicio' => '20:00:00',
'hora_fin' => '22:00:00',
'precio' => 0,
]);
$count++;
}
}
return redirect()->route('admin.torneos.playoffs.manage', $id)
->with('admin_msg', "Playoffs generados: {$count} partidos creados para el grupo {$grupo}.");
}
public function managePlayoffs(int $id)
{
$this->checkSuperAdmin();
$torneo = Torneo::findOrFail($id);
$ts = new \App\Services\TournamentService();
$bracket = $ts->getPlayoffBrackets($id);
$grupos = \DB::table('torneo_equipo')->where('id_torneo', $id)->distinct()->pluck('grupo')->filter();
return view('admin.torneos.playoff_manage', compact('torneo', 'bracket', 'grupos'));
}
public function avanzarGanador(Request $request, int $id)
{
$this->checkSuperAdmin();
$faseActual = (int) $request->input('fase');
$nroBracket = (int) $request->input('nro_bracket');
$idGanador = (int) $request->input('id_ganador');
$torneo = Torneo::findOrFail($id);
$ganador = \App\Models\Equipo::with('club')->findOrFail($idGanador);
// Mapeo de avance
// Cuartos 1 & 2 -> Semis 1
// Cuartos 3 & 4 -> Semis 2
// Semis 1 & 2 -> Final 1
$mapping = [
\App\Models\Evento::FASE_CUARTOS => [
1 => ['next_fase' => \App\Models\Evento::FASE_SEMIS, 'next_nro' => 1, 'side' => 'local'],
2 => ['next_fase' => \App\Models\Evento::FASE_SEMIS, 'next_nro' => 1, 'side' => 'visitante'],
3 => ['next_fase' => \App\Models\Evento::FASE_SEMIS, 'next_nro' => 2, 'side' => 'local'],
4 => ['next_fase' => \App\Models\Evento::FASE_SEMIS, 'next_nro' => 2, 'side' => 'visitante'],
],
\App\Models\Evento::FASE_SEMIS => [
1 => ['next_fase' => \App\Models\Evento::FASE_FINAL, 'next_nro' => 1, 'side' => 'local'],
2 => ['next_fase' => \App\Models\Evento::FASE_FINAL, 'next_nro' => 1, 'side' => 'visitante'],
]
];
if (!isset($mapping[$faseActual][$nroBracket])) {
return back()->with('admin_error', 'No se puede avanzar desde esta fase.');
}
$next = $mapping[$faseActual][$nroBracket];
// Buscar si ya existe el partido en la siguiente fase
$eventoNext = \App\Models\Evento::where('id_torneo', $id)
->where('fase', $next['next_fase'])
->where('numero_partido_bracket', $next['next_nro'])
->first();
if ($eventoNext) {
if ($next['side'] == 'local') {
$eventoNext->id_equipo_local = $idGanador;
} else {
$eventoNext->id_equipo_visitante = $idGanador;
}
$eventoNext->nombre_evento = ($eventoNext->equipoLocal->club->nombre ?? 'TBD') . ' vs ' . ($eventoNext->equipoVisitante->club->nombre ?? 'TBD');
$eventoNext->save();
} else {
// Crear el primer partido de la serie (por defecto 1 partido, el admin puede agregar más)
\App\Models\Evento::create([
'id_evento' => uniqid('ply_adv_'),
'id_torneo' => $id,
'fase' => $next['next_fase'],
'numero_partido_bracket' => $next['next_nro'],
'id_equipo_local' => $next['side'] == 'local' ? $idGanador : null,
'id_equipo_visitante' => $next['side'] == 'visitante' ? $idGanador : null,
'nombre_evento' => $next['side'] == 'local' ? ($ganador->club->nombre . " vs TBD") : ("TBD vs " . $ganador->club->nombre),
'fecha_evento' => now()->addDays(14)->format('Y-m-d'),
'hora_inicio' => '20:00:00',
'hora_fin' => '22:00:00',
'precio' => 0,
]);
}
return back()->with('admin_msg', "✅ Equipo {$ganador->club->nombre} avanzado a la siguiente ronda.");
}
}
@@ -0,0 +1,129 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Pase;
use App\Models\Jugador;
use App\Models\Club;
class PaseController extends Controller
{
private function checkGeneralAdmin(Request $request)
{
if (!session('admin_logged_in') || !in_array(session('admin_role'), [1, 2])) {
abort(403, 'Acceso denegado');
}
}
private function checkSuperAdmin(Request $request)
{
if (!session('admin_logged_in') || session('admin_role') != 1) {
abort(403, 'Acceso denegado. Solo Súper Administradores.');
}
}
public function index(Request $request)
{
$this->checkGeneralAdmin($request);
$query = Pase::with(['jugador', 'clubOrigen', 'clubDestino']);
if (session('admin_role') == 2) {
$idClub = session('admin_id_club');
$query->where(function ($q) use ($idClub) {
$q->where('id_club_origen', $idClub)
->orWhere('id_club_destino', $idClub);
});
}
$pases = $query->orderBy('created_at', 'desc')->paginate(20);
return view('admin.pases.index', compact('pases'));
}
public function create(Request $request)
{
$this->checkGeneralAdmin($request);
return view('admin.pases.create');
}
public function store(Request $request)
{
$this->checkGeneralAdmin($request);
$request->validate([
'documento' => 'required|string|exists:jugadores,documento'
], [
'documento.exists' => 'No se encontró un jugador con ese DNI.'
]);
$jugador = Jugador::where('documento', $request->documento)->first();
// Si el jugador ya pertenece al club destino, error
$idClubDestino = session('admin_role') == 2 ? session('admin_id_club') : $request->input('id_club_destino');
if (!$idClubDestino) {
return back()->withErrors(['id_club_destino' => 'Debe especificar el club destino.']);
}
if ($jugador->id_club_actual == $idClubDestino) {
return back()->withErrors(['documento' => 'Este jugador ya pertenece a tu club.']);
}
// Crear la petición de pase
Pase::create([
'id_jugador' => $jugador->id_jugador,
'id_club_origen' => $jugador->id_club_actual,
'id_club_destino' => $idClubDestino,
'estado' => 'Pendiente'
]);
return redirect()->route('admin.pases.index')->with('admin_msg', 'Solicitud de pase creada correctamente y está pendiente de aprobación.');
}
public function aprobar(Request $request, $id)
{
$this->checkSuperAdmin($request);
$pase = Pase::findOrFail($id);
if ($pase->estado !== 'Pendiente') {
return back()->withErrors(['pase' => 'Este pase ya fue procesado.']);
}
$pase->update(['estado' => 'Aprobado']);
// Actualizar jugador
if ($pase->jugador) {
// Desvincular de equipos del club de origen
$equiposOldClubIds = $pase->jugador->equipos()
->where('id_club', $pase->id_club_origen)
->pluck('equipos.id_equipo');
if ($equiposOldClubIds->count() > 0) {
$pase->jugador->equipos()->detach($equiposOldClubIds);
}
$pase->jugador->update([
'id_club_origen' => $pase->id_club_origen,
'id_club_actual' => $pase->id_club_destino
]);
}
return redirect()->route('admin.pases.index')->with('admin_msg', 'Pase aprobado correctamente y jugador desvinculado de equipos anteriores.');
}
public function rechazar(Request $request, $id)
{
$this->checkSuperAdmin($request);
$pase = Pase::findOrFail($id);
if ($pase->estado !== 'Pendiente') {
return back()->withErrors(['pase' => 'Este pase ya fue procesado.']);
}
$pase->update(['estado' => 'Rechazado']);
return redirect()->route('admin.pases.index')->with('admin_msg', 'Pase rechazado.');
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,55 @@
<?php
namespace App\Http\Controllers;
use App\Models\AdminUser;
use Illuminate\Http\Request;
class AdminUserController extends Controller
{
public function index()
{
$users = AdminUser::all();
return response()->json($users);
}
public function show($id)
{
$user = AdminUser::findOrFail($id);
return response()->json($user);
}
public function store(Request $request)
{
$data = $request->validate([
'username' => 'required|string|max:50|unique:admin_users',
'password' => 'required|string',
'role' => 'nullable|integer',
]);
$data['password'] = bcrypt($data['password']);
$user = AdminUser::create($data);
return response()->json($user, 201);
}
public function update(Request $request, $id)
{
$user = AdminUser::findOrFail($id);
$data = $request->validate([
'username' => 'sometimes|string|max:50|unique:admin_users,username,' . $id,
'password' => 'sometimes|string',
'role' => 'sometimes|integer',
]);
if (isset($data['password'])) {
$data['password'] = bcrypt($data['password']);
}
$user->update($data);
return response()->json($user);
}
public function destroy($id)
{
$user = AdminUser::findOrFail($id);
$user->delete();
return response()->json(null, 204);
}
}
@@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers;
use App\Models\Aficionado;
use Illuminate\Http\Request;
class AficionadoController extends Controller
{
public function index()
{
$aficionados = Aficionado::all();
return response()->json($aficionados);
}
public function show($id)
{
$aficionado = Aficionado::findOrFail($id);
return response()->json($aficionado);
}
public function store(Request $request)
{
$data = $request->validate([
'nombre' => 'required|string|max:100',
'apellido' => 'required|string|max:100',
'dni' => 'required|string|max:20|unique:aficionados',
'fecha_nacimiento' => 'nullable|date',
'email' => 'nullable|email|max:150',
'telefono' => 'nullable|string|max:50',
'localidad' => 'nullable|string|max:100',
'password' => 'nullable|string',
]);
if (isset($data['password'])) {
$data['password'] = bcrypt($data['password']);
}
$aficionado = Aficionado::create($data);
return response()->json($aficionado, 201);
}
public function update(Request $request, $id)
{
$aficionado = Aficionado::findOrFail($id);
$data = $request->validate([
'nombre' => 'sometimes|string|max:100',
'apellido' => 'sometimes|string|max:100',
'dni' => 'sometimes|string|max:20|unique:aficionados,dni,' . $id . ',id_aficionado',
'fecha_nacimiento' => 'nullable|date',
'email' => 'nullable|email|max:150',
'telefono' => 'nullable|string|max:50',
'localidad' => 'nullable|string|max:100',
'password' => 'nullable|string',
]);
if (isset($data['password'])) {
$data['password'] = bcrypt($data['password']);
}
$aficionado->update($data);
return response()->json($aficionado);
}
public function destroy($id)
{
$aficionado = Aficionado::findOrFail($id);
$aficionado->delete();
return response()->json(null, 204);
}
}
+411
View File
@@ -0,0 +1,411 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use App\Models\Jugador;
use App\Models\Aficionado;
use App\Models\AdminUser;
use App\Models\Club;
use App\Mail\WelcomeMail;
use App\Mail\ResetPasswordMail;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http;
class AuthController extends Controller
{
/**
* Valida el token de Cloudflare Turnstile
*/
private function verifyTurnstile($token)
{
if (in_array(config('app.env'), ['local', 'testing']) && $token === '1x00000000000000000000AA') {
return true;
}
if (!$token) return false;
$response = Http::asForm()->post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
'secret' => config('services.turnstile.secret_key'),
'response' => $token,
'remoteip' => request()->ip(),
]);
return $response->successful() && $response->json('success');
}
public function login(Request $request)
{
$tipo = $request->input('tipo');
if ($tipo === 'admin') {
return $this->loginAdmin($request);
}
return $this->loginPlayer($request);
}
public function loginPlayer(Request $request)
{
if (!$this->verifyTurnstile($request->input('cf-turnstile-response'))) {
return back()->with('login_error', 'Error de verificación de seguridad (Turnstile).')->with('login_tab', 'player');
}
$dni = $request->input('dni');
$password = $request->input('password');
$jugador = Jugador::where('documento', $dni)->where('activo', true)->first();
if ($jugador && $jugador->password && Hash::check($password, $jugador->password)) {
$request->session()->put('user_logged_in', true);
$request->session()->put('user_tipo', 'jugador');
$request->session()->put('user_id', $jugador->id_jugador);
$request->session()->put('user_name', $jugador->nombre . ' ' . $jugador->apellido);
$request->session()->put('user_documento', $jugador->documento);
$request->session()->put('user_ultimo_acceso', time());
return redirect()->intended('/');
}
$aficionado = Aficionado::where('dni', $dni)->first();
if ($aficionado && $aficionado->password && Hash::check($password, $aficionado->password)) {
$request->session()->put('user_logged_in', true);
$request->session()->put('user_tipo', 'aficionado');
$request->session()->put('user_id', $aficionado->id_aficionado);
$request->session()->put('user_name', $aficionado->nombre . ' ' . $aficionado->apellido);
$request->session()->put('user_documento', $aficionado->dni);
$request->session()->put('user_ultimo_acceso', time());
return redirect()->intended('/');
}
return back()->with('login_error', 'DNI o contraseña incorrectos')->with('login_tab', 'player');
}
public function loginAdmin(Request $request)
{
if (!$this->verifyTurnstile($request->input('cf-turnstile-response'))) {
return back()->with('login_error', 'Error de verificación de seguridad (Turnstile).')->with('login_tab', 'admin');
}
$username = $request->input('username');
$password = $request->input('password');
$admin = AdminUser::whereRaw('BINARY `username` = ?', [$username])->first();
if ($admin && Hash::check($password, $admin->password)) {
$request->session()->put('admin_logged_in', true);
$request->session()->put('admin_id', $admin->id);
$request->session()->put('admin_username', $admin->username);
$request->session()->put('admin_role', $admin->role);
$request->session()->put('admin_id_club', $admin->id_club);
if ($admin->id_club && $admin->club) {
$request->session()->put('admin_club_nombre', $admin->club->nombre);
}
$request->session()->put('ultimo_acceso', time());
return redirect()->intended('/');
}
return back()->with('login_error', 'Usuario o contraseña incorrectos')->with('login_tab', 'admin');
}
public function logout(Request $request)
{
$isAdmin = $request->session()->get('admin_logged_in');
if ($isAdmin) {
$request->session()->forget(['admin_logged_in', 'admin_id', 'admin_username', 'admin_role', 'ultimo_acceso']);
$msg = 'Sesión de administrador cerrada correctamente.';
} else {
$request->session()->forget(['user_logged_in', 'user_tipo', 'user_id', 'user_name', 'user_documento', 'user_ultimo_acceso']);
$msg = 'Sesión cerrada correctamente.';
}
return redirect('/?logout_msg=' . urlencode($msg));
}
public function showLoginForm()
{
return view('welcome');
}
public function recuperar(Request $request)
{
if (!$this->verifyTurnstile($request->input('cf-turnstile-response'))) {
return back()->with('mensaje', '⚠️ Error de verificación de seguridad (Captcha).');
}
$dni = trim($request->input('dni'));
$email = trim($request->input('email'));
if (empty($dni) || empty($email)) {
return back()->with('mensaje', 'Debes ingresar tu DNI y correo electrónico.');
}
$jugador = Jugador::where('documento', $dni)->where('email', $email)->first();
$aficionado = Aficionado::where('dni', $dni)->where('email', $email)->first();
$usuario = $jugador ?: $aficionado;
if (!$usuario) {
return back()->with('mensaje', 'No se encontró un usuario con ese DNI y correo.');
}
$token = bin2hex(random_bytes(16));
$expires = now()->addHour();
if ($jugador) {
$jugador->update([
'reset_token' => $token,
'reset_expira' => $expires
]);
} else {
$aficionado->update([
'reset_token' => $token,
'reset_expira' => $expires
]);
}
try {
Mail::to($usuario->email)->send(new ResetPasswordMail($usuario, $token));
} catch (\Exception $e) {
Log::error("Error enviando mail de recuperación: " . $e->getMessage());
}
return back()->with('mensaje', '📩 Te enviamos un correo con las instrucciones para recuperar tu contraseña.');
}
public function resetPasswordForm($token)
{
// Verificar que el token exista y no esté expirado
$jugador = Jugador::where('reset_token', $token)->where('reset_expira', '>', now())->first();
$aficionado = Aficionado::where('reset_token', $token)->where('reset_expira', '>', now())->first();
if (!$jugador && !$aficionado) {
return redirect()->route('recuperar')->with('mensaje', '❌ El enlace es inválido o ya expiró. Solicitá uno nuevo.');
}
return view('auth.reset_password', compact('token'));
}
public function resetPassword(Request $request)
{
$request->validate([
'token' => 'required|string',
'password' => 'required|confirmed|min:6',
]);
$token = $request->input('token');
$jugador = Jugador::where('reset_token', $token)->where('reset_expira', '>', now())->first();
$aficionado = Aficionado::where('reset_token', $token)->where('reset_expira', '>', now())->first();
$usuario = $jugador ?: $aficionado;
if (!$usuario) {
return redirect()->route('recuperar')->with('mensaje', '❌ El enlace es inválido o ya expiró. Solicitá uno nuevo.');
}
$usuario->update([
'password' => bcrypt($request->input('password')),
'reset_token' => null,
'reset_expira' => null,
]);
return redirect('/')->with('login_success', '✅ Contraseña cambiada correctamente. Ya podés iniciar sesión.');
}
public function registroAficionado(Request $request)
{
if (!$this->verifyTurnstile($request->input('cf-turnstile-response'))) {
return back()->with('registro_msg', '⚠️ Error de verificación de seguridad (Captcha).')->withInput();
}
$data = $request->validate([
'nombre' => 'required|string|max:100',
'apellido' => 'required|string|max:100',
'dni' => 'required|string|unique:aficionados,dni',
'email' => 'required|email|unique:aficionados,email',
'fecha_nacimiento' => 'nullable|date',
'telefono' => 'nullable|string|max:50',
'localidad' => 'nullable|string|max:100',
'password' => 'required|confirmed|min:6',
]);
$data['password'] = bcrypt($data['password']);
$data['fecha_registro'] = now();
$aficionado = Aficionado::create($data);
try {
Mail::to($aficionado->email)->send(new WelcomeMail($aficionado, 'aficionado'));
} catch (\Exception $e) {
Log::error("Error enviando mail de bienvenida a Aficionado: " . $e->getMessage());
}
return redirect()->route('asociate')->with('mensaje', '✅ Te registraste correctamente. Ya podés iniciar sesión.');
}
public function buscarJugador(Request $request)
{
$request->validate([
'nombre' => 'required|string',
'apellido' => 'required|string',
'dni' => 'required|string',
'acepto' => 'required',
]);
$dni = preg_replace('/[^0-9]/', '', $request->input('dni'));
$nombre = strtoupper(trim($request->input('nombre')));
$apellido = strtoupper(trim($request->input('apellido')));
// Buscar jugador por DNI
$jugador = Jugador::where('documento', $dni)->first();
if (!$jugador) {
// Verificar si ya está registrado como aficionado
$aficionado = Aficionado::where('dni', $dni)->first();
if ($aficionado) {
return redirect()->route('asociate')->with('mensaje', '⚠️ Ya estás registrado como aficionado.');
}
// No existe - se debe registrar como aficionado
return redirect()->route('asociate')->with('mensaje', '⚠️ No encontramos tu registro como jugador. Podés registrarte como aficionado.');
}
// --- Lógica Smart Match ---
$nombreEnBD = $this->normalizeString($jugador->nombre ?? '');
$apellidoEnBD = $this->normalizeString($jugador->apellido ?? '');
$terminosBD = explode(' ', $nombreEnBD . ' ' . $apellidoEnBD);
$palabrasNombreIn = explode(' ', $this->normalizeString($nombre));
$palabrasApellidoIn = explode(' ', $this->normalizeString($apellido));
$nombreCoincide = false;
foreach ($palabrasNombreIn as $p) {
if ($this->isApproxMatch($p, $terminosBD)) {
$nombreCoincide = true;
break;
}
}
$apellidoCoincide = false;
foreach ($palabrasApellidoIn as $p) {
if ($this->isApproxMatch($p, $terminosBD)) {
$apellidoCoincide = true;
break;
}
}
if (!$nombreCoincide || !$apellidoCoincide) {
return redirect()->route('asociate')->with('mensaje', '⚠️ El DNI ingresado no coincide con el Nombre y Apellido proporcionados. Por favor, verifica tus datos.');
}
// --- Fin Smart Match ---
if ($jugador->activo) {
return redirect()->route('asociate')->with('registro_msg', 'Este jugador ya está registrado en el sistema.');
}
// Jugador encontrado e inactivo - mostrar formulario para completar
$club = null;
if ($jugador->id_club_actual) {
$clubObj = \App\Models\Club::find($jugador->id_club_actual);
$club = $clubObj ? $clubObj->nombre : null;
}
$jugador_encontrado = [
'documento' => $jugador->documento,
'nombre' => $jugador->nombre,
'apellido' => $jugador->apellido,
'fecha_nacimiento' => $jugador->fecha_nacimiento,
'club' => $club,
'categoria' => $jugador->categoria,
];
return view('auth.asociate', compact('jugador_encontrado'))->with('tab', 'jugador');
}
public function completarRegistroJugador(Request $request)
{
if (!$this->verifyTurnstile($request->input('cf-turnstile-response'))) {
return back()->with('registro_msg', '⚠️ Error de verificación de seguridad (Captcha).')->withInput();
}
$request->validate([
'dni' => 'required|string',
'email' => 'required|email',
'telefono' => 'nullable|string',
'password' => 'required|confirmed|min:6',
]);
$dni = preg_replace('/[^0-9]/', '', $request->input('dni'));
$jugador = Jugador::where('documento', $dni)->first();
if (!$jugador) {
return redirect()->route('asociate')->with('registro_msg', 'Jugador no encontrado.');
}
if ($jugador->activo) {
return redirect()->route('asociate')->with('registro_msg', 'Este jugador ya está registrado.');
}
$jugador->update([
'email' => $request->input('email'),
'telefono' => $request->input('telefono'),
'password' => bcrypt($request->input('password')),
'activo' => 1,
'fecha_registro' => now(),
]);
try {
Mail::to($jugador->email)->send(new WelcomeMail($jugador, 'jugador'));
} catch (\Exception $e) {
Log::error("Error enviando mail de bienvenida a Jugador: " . $e->getMessage());
}
return redirect()->route('asociate')->with('mensaje', '✅ Registro completado exitosamente. Ya podés iniciar sesión.');
}
/**
* Normaliza un string para comparaciones (mayúsculas, sin acentos, sin espacios extras)
*/
private function normalizeString($str)
{
$unwanted_array = ['Š'=>'S', 'š'=>'s', 'Ž'=>'Z', 'ž'=>'z', 'À'=>'A', 'Á'=>'A', 'Â'=>'A', 'Ã'=>'A', 'Ä'=>'A', 'Å'=>'A', 'Æ'=>'A', 'Ç'=>'C', 'È'=>'E', 'É'=>'E',
'Ê'=>'E', 'Ë'=>'E', 'Ì'=>'I', 'Í'=>'I', 'Î'=>'I', 'Ï'=>'I', 'Ñ'=>'N', 'Ò'=>'O', 'Ó'=>'O', 'Ô'=>'O', 'Õ'=>'O', 'Ö'=>'O', 'Ø'=>'O', 'Ù'=>'U',
'Ú'=>'U', 'Û'=>'U', 'Ü'=>'U', 'Ý'=>'Y', 'Þ'=>'B', 'ß'=>'Ss', 'à'=>'a', 'á'=>'a', 'â'=>'a', 'ã'=>'a', 'ä'=>'a', 'å'=>'a', 'æ'=>'a', 'ç'=>'c',
'è'=>'e', 'é'=>'e', 'ê'=>'e', 'ë'=>'e', 'ì'=>'i', 'í'=>'i', 'î'=>'i', 'ï'=>'i', 'ð'=>'o', 'ñ'=>'n', 'ò'=>'o', 'ó'=>'o', 'ô'=>'o', 'õ'=>'o',
'ö'=>'o', 'ø'=>'o', 'ù'=>'u', 'ú'=>'u', 'û'=>'u', 'ý'=>'y', 'þ'=>'b', 'ÿ'=>'y'];
$str = strtr($str, $unwanted_array);
return strtoupper(trim(preg_replace('/\s+/', ' ', $str)));
}
/**
* Comprueba si una palabra coincide aproximadamente con alguna de la lista
*/
private function isApproxMatch($word, $list)
{
if (strlen($word) < 3) return false;
foreach ($list as $item) {
if (strlen($item) < 3) continue;
// Coincidencia exacta o contenida
if ($word === $item || strpos($item, $word) !== false || strpos($word, $item) !== false) {
return true;
}
// Levenshtein para errores de tipeo (máximo 1 de distancia)
if (levenshtein($word, $item) <= 1) {
return true;
}
}
return false;
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace App\Http\Controllers;
use App\Models\Club;
use Illuminate\Http\Request;
class ClubController extends Controller
{
public function index()
{
$clubes = Club::with('equipos')->get();
return response()->json($clubes);
}
public function show($id)
{
$club = Club::with('equipos', 'jugadores')->findOrFail($id);
return response()->json($club);
}
public function store(Request $request)
{
$data = $request->validate([
'nombre' => 'required|string|max:100',
]);
$club = Club::create($data);
return response()->json($club, 201);
}
public function update(Request $request, $id)
{
$club = Club::findOrFail($id);
$data = $request->validate([
'nombre' => 'sometimes|string|max:100',
]);
$club->update($data);
return response()->json($club);
}
public function destroy($id)
{
$club = Club::findOrFail($id);
$club->delete();
return response()->json(null, 204);
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}
@@ -0,0 +1,221 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use Barryvdh\DomPDF\Facade\Pdf;
class DocumentacionController extends Controller
{
public function index()
{
$path = base_path('misc/MANUAL_USUARIO.md');
$content = "No se encontró el manual.";
if (File::exists($path)) {
$md = File::get($path);
// Detectar rol de la sesión
$role = $this->getUserRole();
// Segmentar contenido
$filteredMd = $this->segmentMarkdown($md, $role);
$parsedown = new \Parsedown();
$content = $parsedown->text($filteredMd);
}
return view('documentacion.index', compact('content'));
}
/**
* Detecta el rol del usuario basado en las variables de sesión
*/
private function getUserRole()
{
if (session()->get('admin_logged_in')) {
$adminRole = session()->get('admin_role');
return ($adminRole == 1) ? 'superadmin' : 'admin_club';
}
if (session()->get('user_logged_in')) {
return session()->get('user_tipo'); // 'jugador' o 'aficionado'
}
return 'visitante';
}
/**
* Segmenta y filtra el Markdown según el rol
*/
private function segmentMarkdown($md, $role)
{
$markers = [
'cap1' => '<a name="cap1"></a>',
'cap2' => '<a name="cap2"></a>',
'cap3' => '<a name="cap3"></a>',
'cap4' => '<a name="cap4"></a>',
'cap5' => '<a name="cap5"></a>',
'faq' => '## ❓ Preguntas Frecuentes'
];
// Encontrar posiciones de los marcadores
$positions = [];
foreach ($markers as $key => $marker) {
$pos = strpos($md, $marker);
if ($pos !== false) {
$positions[$key] = $pos;
}
}
asort($positions);
$keys = array_keys($positions);
$sections = [];
// Intro (antes del primer capítulo)
$sections['intro'] = substr($md, 0, $positions[$keys[0]]);
// Capítulos y FAQ
for ($i = 0; $i < count($keys); $i++) {
$start = $positions[$keys[$i]];
$end = isset($keys[$i+1]) ? $positions[$keys[$i+1]] : strlen($md);
$sections[$keys[$i]] = substr($md, $start, $end - $start);
}
// Determinar qué capítulos mostrar
$allowedChapters = [1]; // Todos ven el Cap 1
$showSections = ['intro', 'cap1', 'faq'];
switch ($role) {
case 'superadmin':
$allowedChapters = [1, 2, 3, 4, 5];
$showSections = ['intro', 'cap1', 'cap2', 'cap3', 'cap4', 'cap5', 'faq'];
break;
case 'admin_club':
$allowedChapters = [1, 4];
$showSections = ['intro', 'cap1', 'cap4', 'faq'];
break;
case 'jugador':
$allowedChapters = [1, 2];
$showSections = ['intro', 'cap1', 'cap2', 'faq'];
break;
case 'aficionado':
$allowedChapters = [1, 3];
$showSections = ['intro', 'cap1', 'cap3', 'faq'];
break;
default: // visitante
$allowedChapters = [1];
$showSections = ['intro', 'cap1', 'faq'];
break;
}
// Filtrar Tabla de Contenidos en la Intro
$sections['intro'] = $this->filterTOC($sections['intro'], $allowedChapters);
// Unir secciones seleccionadas
$finalMd = "";
foreach ($showSections as $s) {
if (isset($sections[$s])) {
$finalMd .= $sections[$s] . "\n\n---\n\n";
}
}
return $finalMd;
}
/**
* Filtra la tabla de contenidos para mostrar solo los capítulos permitidos
*/
private function filterTOC($intro, $allowedChapters)
{
$lines = explode("\n", $intro);
$filteredLines = [];
$inTable = false;
foreach ($lines as $line) {
if (strpos($line, '| Capítulo | Perfil |') !== false) {
$inTable = true;
$filteredLines[] = $line;
continue;
}
if ($inTable) {
if (trim($line) === '' || (strpos($line, '|') === false && trim($line) !== '')) {
$inTable = false;
$filteredLines[] = $line;
continue;
}
if (strpos($line, '|---|') !== false) {
$filteredLines[] = $line;
continue;
}
// Filtrar fila de la tabla
$matched = false;
foreach ($allowedChapters as $cap) {
if (strpos($line, "[Capítulo $cap]") !== false) {
$matched = true;
break;
}
}
if ($matched) {
$filteredLines[] = $line;
}
} else {
$filteredLines[] = $line;
}
}
return implode("\n", $filteredLines);
}
public function download()
{
$path = base_path('misc/MANUAL_USUARIO.md');
if (!File::exists($path)) {
abort(404, 'No se pudo generar el manual porque el archivo base no existe.');
}
$md = File::get($path);
// Detectar rol de la sesión
$role = $this->getUserRole();
// Segmentar contenido
$filteredMd = $this->segmentMarkdown($md, $role);
// Convertir Markdown a HTML
$parsedown = new \Parsedown();
$htmlContent = $parsedown->text($filteredMd);
// Preparar Logo en Base64 para el PDF
$logoBase64 = null;
$logoPath = public_path('logo.png');
if (File::exists($logoPath)) {
$type = pathinfo($logoPath, PATHINFO_EXTENSION);
$data = File::get($logoPath);
$logoBase64 = 'data:image/' . $type . ';base64,' . base64_encode($data);
}
// Depuración temporal: Descomenta si querés ver qué rol detecta antes de generar el PDF
// dd('Rol detectado: ' . $role);
// Generar PDF con dompdf
$pdf = Pdf::loadView('documentacion.pdf', [
'content' => $htmlContent,
'logo' => $logoBase64
]);
// Ajustar papel y orientación
$pdf->setPaper('A4', 'portrait');
// Nombre de archivo único para evitar caché de navegador
$filename = 'Manual_Segmentado_' . $role . '_' . time() . '.pdf';
return $pdf->download($filename);
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace App\Http\Controllers;
use App\Models\Equipo;
use Illuminate\Http\Request;
class EquipoController extends Controller
{
public function index()
{
$equipos = Equipo::with('club', 'jugadores')->get();
return response()->json($equipos);
}
public function show($id)
{
$equipo = Equipo::with('club', 'jugadores')->findOrFail($id);
return response()->json($equipo);
}
public function publicShow($id)
{
$equipo = Equipo::with(['club', 'jugadores'])->findOrFail($id);
return view('equipos.show', compact('equipo'));
}
public function store(Request $request)
{
$data = $request->validate([
'id_club' => 'required|integer|exists:clubes,id_club',
'categoria' => 'required|string|max:20',
'division' => 'nullable|string|max:5',
]);
$equipo = Equipo::create($data);
return response()->json($equipo, 201);
}
public function update(Request $request, $id)
{
$equipo = Equipo::findOrFail($id);
$data = $request->validate([
'id_club' => 'sometimes|integer|exists:clubes,id_club',
'categoria' => 'sometimes|string|max:20',
'division' => 'nullable|string|max:5',
]);
$equipo->update($data);
return response()->json($equipo);
}
public function destroy($id)
{
$equipo = Equipo::findOrFail($id);
$equipo->delete();
return response()->json(null, 204);
}
}
+84
View File
@@ -0,0 +1,84 @@
<?php
namespace App\Http\Controllers;
use App\Models\Evento;
use Carbon\Carbon;
use Illuminate\Http\Request;
class EventoController extends Controller
{
public function index(Request $request)
{
$fechaStr = $request->get('fecha', now()->toDateString());
$fechaSeleccionada = Carbon::parse($fechaStr);
// Generar rango de fechas (Ayer, Hoy, Mañana + otros)
$fechasNav = [];
for ($i = -3; $i <= 3; $i++) {
$d = now()->addDays($i);
$fechasNav[] = [
'label' => $this->getFechaLabel($d),
'fecha' => $d->toDateString(),
'active' => $d->isSameDay($fechaSeleccionada)
];
}
$eventos = Evento::with(['equipoLocal.club', 'equipoVisitante.club'])
->whereDate('fecha_evento', $fechaSeleccionada->toDateString())
->orderBy('hora_inicio', 'asc')
->get()
->map(function ($e) {
$e->estado = $this->calcularEstado($e->fecha_evento, $e->hora_inicio, $e->hora_fin);
return $e;
});
return view('eventos.index', compact('eventos', 'fechasNav', 'fechaSeleccionada'));
}
private function getFechaLabel(Carbon $date)
{
$fecha = $date->format('d/m');
if ($date->isToday()) return "HOY $fecha";
if ($date->isYesterday()) return "AYER $fecha";
if ($date->isTomorrow()) return "MAÑANA $fecha";
return $date->translatedFormat('D d/m');
}
public function show($id)
{
$evento = Evento::with(['equipoLocal.club', 'equipoVisitante.club', 'qrCodes'])
->findOrFail($id);
$evento->estado = $this->calcularEstado($evento->fecha_evento, $evento->hora_inicio, $evento->hora_fin);
$isAdmin = session()->has('admin_logged_in') && session('admin_logged_in');
$isUser = session()->has('user_logged_in') && session('user_logged_in');
return view('eventos.show', compact('evento', 'isAdmin', 'isUser'));
}
private function calcularEstado($fechaEvento, $horaInicio, $horaFin)
{
$tz = new \DateTimeZone(config('app.timezone', 'America/Argentina/Buenos_Aires'));
// Asegurarnos de tener strings limpios (Y-m-d y H:i:s)
$f = ($fechaEvento instanceof \Carbon\Carbon) ? $fechaEvento->format('Y-m-d') : substr($fechaEvento, 0, 10);
$h1 = ($horaInicio instanceof \Carbon\Carbon) ? $horaInicio->format('H:i:s') : $horaInicio;
$h2 = ($horaFin instanceof \Carbon\Carbon) ? $horaFin->format('H:i:s') : $horaFin;
$inicio = new \DateTime("$f $h1", $tz);
$fin = new \DateTime("$f $h2", $tz);
if ($fin <= $inicio) {
$fin->modify('+1 day');
}
$ahora = new \DateTime('now', $tz);
if ($ahora >= $inicio && $ahora <= $fin) return 'Activo';
if ($ahora < $inicio) return 'Próximo';
return 'Finalizado';
}
}
@@ -0,0 +1,81 @@
<?php
namespace App\Http\Controllers;
use App\Models\AgentThread;
use App\Services\GeniusAgentService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class GeniusAgentController extends Controller
{
public function __construct(private GeniusAgentService $service) {}
public function chat(Request $request): JsonResponse
{
set_time_limit(120);
$request->validate([
'message' => ['required', 'string', 'max:1000'],
'thread_id' => ['nullable', 'string', 'uuid'],
]);
$message = $request->string('message')->trim()->toString();
if (session('admin_logged_in')) {
return $this->handleAdmin($message, $request->input('thread_id'));
}
return $this->handlePublic($request, $message);
}
private function handleAdmin(string $message, ?string $threadId): JsonResponse
{
$adminId = (int) session('admin_id');
$isSuperadmin = (int) session('admin_role') === 1;
$thread = AgentThread::findOrCreateForAdmin($threadId, $adminId);
$reply = $this->service->chatAdmin($message, $thread, $isSuperadmin);
return response()->json([
'reply' => $reply,
'thread_id' => $thread->thread_id,
]);
}
private function handlePublic(Request $request, string $message): JsonResponse
{
$maxMessages = (int) config('services.genius.max_messages_per_session', 20);
$windowMin = (int) config('services.genius.session_window_minutes', 60);
$session = $request->session();
$startedAt = $session->get('agent_window_started_at');
$count = (int) $session->get('agent_window_count', 0);
if ($startedAt === null || (time() - (int) $startedAt) > $windowMin * 60) {
$startedAt = time();
$count = 0;
}
if ($maxMessages > 0 && $count >= $maxMessages) {
$remaining = max(1, (int) ceil(($windowMin * 60 - (time() - (int) $startedAt)) / 60));
return response()->json([
'reply' => "Llegaste al límite de {$maxMessages} consultas por sesión. "
. "Volvé a intentar en {$remaining} minuto(s) o contactá directamente a OnAPB.",
'limit_reached' => true,
]);
}
$history = $session->get('agent_messages', []);
$reply = $this->service->chatPublic($message, $history);
$history[] = ['role' => 'user', 'content' => $message];
$history[] = ['role' => 'assistant', 'content' => $reply];
$session->put('agent_messages', $history);
$session->put('agent_window_started_at', $startedAt);
$session->put('agent_window_count', $count + 1);
return response()->json(['reply' => $reply]);
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
namespace App\Http\Controllers;
use App\Models\Noticia;
use App\Models\Torneo;
use App\Models\Evento;
use App\Models\Promocion;
use App\Models\CarouselItem;
use App\Services\TournamentService;
use Carbon\Carbon;
use Illuminate\Http\Request;
class HomeController extends Controller
{
public function index()
{
$eventos = $this->getEventos();
$promociones = Promocion::whereNotNull('descripcion')
->where('descripcion', '!=', '')
->orderBy('id', 'desc')
->get();
$carouselItems = CarouselItem::where('activo', true)
->orderBy('orden', 'asc')
->get();
$noticias = Noticia::orderBy('id', 'desc')
->take(3)
->get();
$torneos = Torneo::orderBy('id', 'desc')
->get();
$tournamentService = new TournamentService();
$standingsData = [];
foreach ($torneos as $t) {
$standingsData[$t->id] = $tournamentService->getStandings($t->id);
}
return view('welcome', compact('eventos', 'promociones', 'carouselItems', 'noticias', 'torneos', 'standingsData'));
}
private function getEventos()
{
$eventos = Evento::with(['equipoLocal.club', 'equipoVisitante.club'])
->orderBy('fecha_evento', 'asc')
->orderBy('hora_inicio', 'asc')
->get()
->map(function ($e) {
$estado = $this->calcularEstado($e->fecha_evento, $e->hora_inicio, $e->hora_fin);
if (in_array($estado, ['Activo', 'Próximo'])) {
$e->estado = $estado;
return $e;
}
return null;
})
->filter()
->values();
return $eventos;
}
private function calcularEstado($fechaEvento, $horaInicio, $horaFin)
{
$tz = new \DateTimeZone(config('app.timezone', 'America/Argentina/Buenos_Aires'));
// Asegurarnos de tener strings limpios (Y-m-d y H:i:s)
$f = ($fechaEvento instanceof \Carbon\Carbon) ? $fechaEvento->format('Y-m-d') : substr($fechaEvento, 0, 10);
$h1 = ($horaInicio instanceof \Carbon\Carbon) ? $horaInicio->format('H:i:s') : $horaInicio;
$h2 = ($horaFin instanceof \Carbon\Carbon) ? $horaFin->format('H:i:s') : $horaFin;
$inicio = new \DateTime("$f $h1", $tz);
$fin = new \DateTime("$f $h2", $tz);
if ($fin <= $inicio) {
$fin->modify('+1 day');
}
$ahora = new \DateTime('now', $tz);
if ($ahora >= $inicio && $ahora <= $fin) return 'Activo';
if ($ahora < $inicio) return 'Próximo';
return 'Finalizado';
}
}
@@ -0,0 +1,76 @@
<?php
namespace App\Http\Controllers;
use App\Models\Jugador;
use Illuminate\Http\Request;
class JugadorController extends Controller
{
public function index()
{
$jugadores = Jugador::with('clubActual', 'clubOrigen', 'equipos')->get();
return response()->json($jugadores);
}
public function show($id)
{
$jugador = Jugador::with('clubActual', 'clubOrigen', 'equipos')->findOrFail($id);
return response()->json($jugador);
}
public function store(Request $request)
{
$data = $request->validate([
'id_jugador' => 'nullable|string|max:6',
'documento' => 'required|string|max:20|unique:jugadores',
'nombre' => 'required|string|max:100',
'apellido' => 'required|string|max:100',
'fecha_nacimiento' => 'nullable|date',
'edad' => 'nullable|integer',
'categoria' => 'nullable|string|max:20',
'id_club_actual' => 'nullable|integer|exists:clubes,id_club',
'id_club_origen' => 'nullable|integer|exists:clubes,id_club',
'activo' => 'nullable|boolean',
'email' => 'nullable|email|max:150',
'telefono' => 'nullable|string|max:50',
'password' => 'nullable|string',
]);
if (isset($data['password'])) {
$data['password'] = bcrypt($data['password']);
}
$jugador = Jugador::create($data);
return response()->json($jugador, 201);
}
public function update(Request $request, $id)
{
$jugador = Jugador::findOrFail($id);
$data = $request->validate([
'documento' => 'sometimes|string|max:20|unique:jugadores,documento,' . $id . ',id_jugador',
'nombre' => 'sometimes|string|max:100',
'apellido' => 'sometimes|string|max:100',
'fecha_nacimiento' => 'nullable|date',
'edad' => 'nullable|integer',
'categoria' => 'nullable|string|max:20',
'id_club_actual' => 'nullable|integer|exists:clubes,id_club',
'id_club_origen' => 'nullable|integer|exists:clubes,id_club',
'activo' => 'nullable|boolean',
'email' => 'nullable|email|max:150',
'telefono' => 'nullable|string|max:50',
'password' => 'nullable|string',
]);
if (isset($data['password'])) {
$data['password'] = bcrypt($data['password']);
}
$jugador->update($data);
return response()->json($jugador);
}
public function destroy($id)
{
$jugador = Jugador::findOrFail($id);
$jugador->delete();
return response()->json(null, 204);
}
}
@@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers;
use App\Models\JugadorEquipo;
use Illuminate\Http\Request;
class JugadorEquipoController extends Controller
{
public function index()
{
$relaciones = JugadorEquipo::with('jugador', 'equipo')->get();
return response()->json($relaciones);
}
public function show($idJugador, $idEquipo)
{
$relacion = JugadorEquipo::where('id_jugador', $idJugador)
->where('id_equipo', $idEquipo)
->with('jugador', 'equipo')
->firstOrFail();
return response()->json($relacion);
}
public function store(Request $request)
{
$data = $request->validate([
'id_jugador' => 'required|string|max:6|exists:jugadores,id_jugador',
'id_equipo' => 'required|integer|exists:equipos,id_equipo',
'fecha_alta' => 'nullable|date',
]);
$relacion = JugadorEquipo::create($data);
return response()->json($relacion, 201);
}
public function update(Request $request, $idJugador, $idEquipo)
{
$relacion = JugadorEquipo::where('id_jugador', $idJugador)
->where('id_equipo', $idEquipo)
->firstOrFail();
$data = $request->validate([
'fecha_alta' => 'nullable|date',
]);
$relacion->update($data);
return response()->json($relacion);
}
public function destroy($idJugador, $idEquipo)
{
$relacion = JugadorEquipo::where('id_jugador', $idJugador)
->where('id_equipo', $idEquipo)
->firstOrFail();
$relacion->delete();
return response()->json(null, 204);
}
public function porJugador($idJugador)
{
$relaciones = JugadorEquipo::where('id_jugador', $idJugador)
->with('equipo')
->get();
return response()->json($relaciones);
}
public function porEquipo($idEquipo)
{
$relaciones = JugadorEquipo::where('id_equipo', $idEquipo)
->with('jugador')
->get();
return response()->json($relaciones);
}
}
@@ -0,0 +1,61 @@
<?php
namespace App\Http\Controllers;
use App\Models\Noticia;
use Illuminate\Http\Request;
class NoticiaController extends Controller
{
public function index(Request $request)
{
$noticias = Noticia::orderBy('fecha', 'desc')->get();
if ($request->expectsJson()) {
return response()->json($noticias);
}
return view('noticias.index', compact('noticias'));
}
public function show($id)
{
$noticia = Noticia::findOrFail($id);
if (request()->expectsJson()) {
return response()->json($noticia);
}
return view('noticias.show', compact('noticia'));
}
public function store(Request $request)
{
$data = $request->validate([
'titulo' => 'required|string|max:200',
'contenido' => 'required',
'imagen' => 'nullable|string|max:200',
]);
$noticia = Noticia::create($data);
return response()->json($noticia, 201);
}
public function update(Request $request, $id)
{
$noticia = Noticia::findOrFail($id);
$data = $request->validate([
'titulo' => 'sometimes|string|max:200',
'contenido' => 'sometimes',
'imagen' => 'nullable|string|max:200',
]);
$noticia->update($data);
return response()->json($noticia);
}
public function destroy($id)
{
$noticia = Noticia::findOrFail($id);
$noticia->delete();
return response()->json(null, 204);
}
}
@@ -0,0 +1,143 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Services\NotificacionService;
class NotificacionController extends Controller
{
private NotificacionService $service;
public function __construct(NotificacionService $service)
{
$this->service = $service;
}
// ── Helper: usuario logueado ──
private function getUserSession(): ?array
{
if (!session()->has('user_logged_in')) return null;
return [
'tipo' => session('user_tipo'),
'id' => session('user_id'),
];
}
/**
* GET /notificaciones — Listado completo paginado
*/
public function index()
{
$u = $this->getUserSession();
if (!$u) return redirect('/')->with('panel_error', 'Debés iniciar sesión.');
$notificaciones = $this->service->obtenerTodas($u['tipo'], $u['id']);
$totalNoLeidas = $this->service->contarNoLeidas($u['tipo'], $u['id']);
return view('notificaciones.index', compact('notificaciones', 'totalNoLeidas'));
}
/**
* POST /notificaciones/{id}/leer — Marcar una como leída
*/
public function marcarLeida(int $id)
{
$u = $this->getUserSession();
if (!$u) return response()->json(['ok' => false], 401);
$this->service->marcarLeida($id, $u['tipo'], $u['id']);
return response()->json(['ok' => true]);
}
/**
* POST /notificaciones/leer-todas — Marcar todas como leídas
*/
public function marcarTodasLeidas()
{
$u = $this->getUserSession();
if (!$u) return response()->json(['ok' => false], 401);
$count = $this->service->marcarTodasLeidas($u['tipo'], $u['id']);
return response()->json(['ok' => true, 'marcadas' => $count]);
}
/**
* GET /notificaciones/count — Badge AJAX (devuelve JSON {count: N})
*/
public function count()
{
$u = $this->getUserSession();
if (!$u) return response()->json(['count' => 0]);
return response()->json(['count' => $this->service->contarNoLeidas($u['tipo'], $u['id'])]);
}
/**
* GET /notificaciones/latest — ID de la última no leída
*/
public function latest()
{
$u = $this->getUserSession();
if (!$u) return response()->json(['id' => 0]);
$notif = $this->service->obtenerNoLeidas($u['tipo'], $u['id'])->first();
return response()->json([
'id' => $notif ? $notif->id : 0,
'titulo' => $notif ? $notif->titulo : '',
'mensaje' => $notif ? $notif->mensaje : ''
]);
}
/**
* DELETE /notificaciones/{id} — Eliminar una notificación
*/
public function eliminar(int $id)
{
$u = $this->getUserSession();
if (!$u) return response()->json(['ok' => false], 401);
$this->service->eliminar($id, $u['tipo'], $u['id']);
return response()->json(['ok' => true]);
}
/**
* DELETE /notificaciones — Eliminar todas
*/
public function eliminarTodas()
{
$u = $this->getUserSession();
if (!$u) return response()->json(['ok' => false], 401);
$this->service->eliminarTodas($u['tipo'], $u['id']);
return response()->json(['ok' => true]);
}
/**
* POST /notificaciones/subscribe — Guardar suscripción para Web Push
*/
public function subscribe(Request $request)
{
$u = $this->getUserSession();
if (!$u) return response()->json(['ok' => false, 'error' => 'No session'], 401);
$request->validate([
'endpoint' => 'required',
'keys.p256dh' => 'required',
'keys.auth' => 'required',
]);
\App\Models\PushSubscription::updateOrCreate(
[
'id_usuario' => (string) $u['id'],
'tipo_usuario' => $u['tipo'],
'endpoint' => $request->endpoint,
],
[
'p256dh' => $request->keys['p256dh'],
'auth' => $request->keys['auth'],
]
);
return response()->json(['ok' => true]);
}
}
+363
View File
@@ -0,0 +1,363 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\DB;
use App\Models\Jugador;
use App\Models\Aficionado;
use App\Models\Evento;
use App\Models\Equipo;
use App\Models\Promocion;
use App\Models\QrCode;
use App\Models\PromoQr;
use App\Mail\QrCodeMail;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
class PanelController extends Controller
{
private \App\Services\NotificacionService $notifService;
public function __construct(\App\Services\NotificacionService $notifService)
{
$this->notifService = $notifService;
}
// ── Helper: obtener usuario logueado ──
private function getUser()
{
if (!session()->has('user_logged_in')) {
return null;
}
$tipo = session('user_tipo');
$id = session('user_id');
if ($tipo === 'jugador') {
return ['user' => Jugador::find($id), 'tipo' => $tipo];
}
return ['user' => Aficionado::find($id), 'tipo' => $tipo];
}
// ══════════════════════════════════
// PANEL PRINCIPAL
// ══════════════════════════════════
public function index(Request $request)
{
$data = $this->getUser();
if (!$data || !$data['user']) {
session()->flush();
return redirect('/?logout_msg=' . urlencode('Tu usuario no fue encontrado. Iniciá sesión nuevamente.'));
}
$user = $data['user'];
$userTipo = $data['tipo'];
$userId = session('user_id');
// Cargar relaciones de club/equipos para jugadores
if ($userTipo === 'jugador') {
$user->load(['clubActual', 'equipos.club']);
}
// Obtener QRs del usuario (eventos que NO estén borrados)
if ($userTipo === 'jugador') {
$qrCodes = QrCode::whereHas('evento')
->where('id_jugador', $user->id_jugador)
->orderBy('creado', 'desc')
->get();
} else {
$qrCodes = QrCode::whereHas('evento')
->where('id_aficionado', $user->id_aficionado)
->orderBy('creado', 'desc')
->get();
}
// Obtener beneficios de promociones
$promoQrs = PromoQr::with('promocion')
->where('id_usuario', $userId)
->where('tipo_usuario', $userTipo)
->orderBy('generado_en', 'desc')
->get();
return view('panel.index', compact('user', 'userTipo', 'qrCodes', 'promoQrs'));
}
// ══════════════════════════════════
// ACTUALIZAR DATOS
// ══════════════════════════════════
public function actualizarDatos(Request $request)
{
$data = $this->getUser();
if (!$data) return redirect('/');
$request->validate([
'email' => 'required|email',
'telefono' => 'required|string',
'localidad' => 'nullable|string',
]);
$user = $data['user'];
if ($data['tipo'] === 'jugador') {
$user->update([
'email' => $request->input('email'),
'telefono' => $request->input('telefono'),
]);
} else {
$user->update([
'email' => $request->input('email'),
'telefono' => $request->input('telefono'),
'localidad' => $request->input('localidad'),
]);
}
return back()->with('panel_msg', 'Datos actualizados correctamente.');
}
// ══════════════════════════════════
// CAMBIAR CONTRASEÑA
// ══════════════════════════════════
public function cambiarPassword(Request $request)
{
$data = $this->getUser();
if (!$data) return redirect('/');
$request->validate([
'password_actual' => 'required|string',
'password_nueva' => 'required|confirmed|min:6',
]);
$user = $data['user'];
if ($user->password) {
if (!Hash::check($request->input('password_actual'), $user->password)) {
return back()->with('panel_error', 'La contraseña actual es incorrecta.');
}
}
$user->update([
'password' => bcrypt($request->input('password_nueva')),
]);
return back()->with('panel_msg', 'Contraseña cambiada correctamente.');
}
// ══════════════════════════════════
// SOLICITAR QR PARA EVENTO
// ══════════════════════════════════
public function solicitarQr(Request $request)
{
$data = $this->getUser();
if (!$data || !$data['user']) return redirect('/');
$user = $data['user'];
$userTipo = $data['tipo'];
$id_evento = $request->input('id_evento');
$evento = Evento::with(['equipoLocal', 'equipoVisitante'])->find($id_evento);
if (!$evento) {
return back()->with('panel_error', 'Evento no encontrado.');
}
$qrs_a_generar = 0;
if ($userTipo === 'jugador') {
// Verificar si ya generó QRs para este evento
$yaGenero = QrCode::where('id_evento', $id_evento)
->where('id_jugador', $user->id_jugador)
->count();
if ($yaGenero > 0) {
return back()->with('panel_error', 'Ya solicitaste QRs para este evento.');
}
// Verificar si el jugador está activo
if (!$user->activo) {
return back()->with('panel_error', 'Tu registro no está activo. Completá tu registro primero.');
}
// Verificar si pertenece a alguno de los equipos del evento
$pertenece = DB::table('jugador_equipo')
->where('id_jugador', $user->id_jugador)
->whereIn('id_equipo', array_filter([$evento->id_equipo_local, $evento->id_equipo_visitante]))
->exists();
if ($pertenece) {
$qrs_a_generar = $evento->limite_qr_jugador ?? 3; // Límite configurable
$tipoQr = 'invitado';
} else {
// Check if category is "libre"
$edadCategoria = date('Y') - \Carbon\Carbon::parse($user->fecha_nacimiento)->format('Y');
$categoriaDB = \App\Models\Categoria::where('edad_min', '<=', $edadCategoria)
->where('edad_max', '>=', $edadCategoria)
->first();
if ($categoriaDB && $categoriaDB->es_libre) {
$qrs_a_generar = 1;
$tipoQr = 'libre_50'; // Identificador para 50% de descuento
} else {
return back()->with('panel_error', 'No podés generar QR para este partido ya que no pertenecés a los equipos ni sos categoría Libre. Deberás abonar la totalidad de la entrada en puerta.');
}
}
if ($qrs_a_generar === 0) {
return back()->with('panel_error', 'No se permiten QRs para este evento.');
}
// Generar QRs
for ($i = 0; $i < $qrs_a_generar; $i++) {
QrCode::create([
'id_qr' => uniqid('qr_'),
'id_evento' => $id_evento,
'id_jugador' => $user->id_jugador,
'tipo_qr' => $tipoQr ?? 'invitado',
'escaneos_restantes' => 1,
'creado' => now(),
]);
}
} else {
// Aficionado: no puede solicitar QRs para eventos
return back()->with('panel_error', 'Los aficionados no pueden solicitar QRs para eventos. Adquirí tu entrada directamente en el lugar.');
}
// Enviar mail con QRs
try {
Mail::to($user->email)->send(new QrCodeMail($user, $evento, $qrs_a_generar));
} catch (\Exception $e) {
Log::error("Error enviando mail de QRs: " . $e->getMessage());
}
// Notificación interna
$this->notifService->enviar(
$userTipo,
$userTipo === 'jugador' ? $user->id_jugador : $user->id_aficionado,
'sistema',
'🎫 QRs Generados',
"Tus {$qrs_a_generar} QR(s) para el evento " . ($evento->nombre_evento ?? 'seleccionado') . " ya están disponibles.",
route('panel.mis.qrs', ['evento' => $id_evento])
);
return redirect()->route('panel.mis.qrs', ['evento' => $id_evento])
->with('panel_msg', "¡QR(s) generados correctamente! ({$qrs_a_generar})");
}
// ══════════════════════════════════
// MIS QRS (per evento)
// ══════════════════════════════════
public function misQrs(Request $request)
{
$data = $this->getUser();
if (!$data || !$data['user']) return redirect('/');
$user = $data['user'];
$userTipo = $data['tipo'];
$id_evento = $request->query('evento');
// Traer QRs del usuario (solo de eventos activos)
$query = QrCode::whereHas('evento')
->with(['evento.equipoLocal.club', 'evento.equipoVisitante.club', 'evento.torneo']);
if ($userTipo === 'jugador') {
$query->where('id_jugador', $user->id_jugador);
$user->load('clubActual');
} else {
$query->where('id_aficionado', $user->id_aficionado);
}
if ($id_evento) {
$query->where('id_evento', $id_evento);
$evento = Evento::find($id_evento);
} else {
$evento = null;
}
$qrs = $query->orderBy('creado', 'desc')->get();
// Adjuntar información de grupo si hay torneo
foreach ($qrs as $qr) {
if ($qr->evento && $qr->evento->id_torneo && $qr->evento->id_equipo_local) {
$rel = DB::table('torneo_equipo')
->where('id_torneo', $qr->evento->id_torneo)
->where('id_equipo', $qr->evento->id_equipo_local)
->first();
if ($rel) {
$qr->evento->grupo_nombre = $rel->grupo ?? 'General';
}
}
}
return view('panel.mis_qrs', compact('user', 'userTipo', 'qrs', 'evento'));
}
// ══════════════════════════════════
// GENERAR QR PARA PROMOCIÓN
// ══════════════════════════════════
public function generarPromoQr(Request $request)
{
$data = $this->getUser();
if (!$data || !$data['user']) return redirect('/');
$user = $data['user'];
$userTipo = $data['tipo'];
$userId = session('user_id');
$id_promo = $request->input('id_promo');
$promo = Promocion::find($id_promo);
if (!$promo) {
return back()->with('panel_error', 'Promoción no encontrada.');
}
// Verificar si ya generó QR para esta promo
$yaGenero = PromoQr::where('id_promo', $id_promo)
->where('id_usuario', $userId)
->where('tipo_usuario', $userTipo)
->count();
if ($yaGenero > 0) {
return back()->with('panel_error', 'Ya generaste un QR para esta promoción.');
}
$id_qr = bin2hex(random_bytes(8));
PromoQr::create([
'id_qr' => $id_qr,
'id_promo' => $id_promo,
'id_usuario' => $userId,
'tipo_usuario' => $userTipo,
'generado_en' => now(),
'usado' => false,
]);
return redirect()->route('panel.promo.qr.ver', ['id' => $id_qr])
->with('panel_msg', '¡QR de beneficio generado correctamente!');
}
// ══════════════════════════════════
// VER QR DE PROMOCIÓN
// ══════════════════════════════════
public function verPromoQr($id)
{
$data = $this->getUser();
if (!$data || !$data['user']) return redirect('/');
$user = $data['user'];
$userTipo = $data['tipo'];
if ($userTipo === 'jugador') {
$user->load('clubActual');
}
$promoQr = PromoQr::with('promocion')
->where('id_qr', $id)
->where('id_usuario', session('user_id'))
->where('tipo_usuario', $userTipo)
->firstOrFail();
return view('panel.promo_qr', compact('promoQr', 'user', 'userTipo'));
}
}
@@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers;
use App\Models\PromoQr;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class PromoQrController extends Controller
{
public function index()
{
$promoQrs = PromoQr::with('promocion')->get();
return response()->json($promoQrs);
}
public function show($id)
{
$promoQr = PromoQr::with('promocion', 'usuario')->findOrFail($id);
return response()->json($promoQr);
}
public function store(Request $request)
{
$data = $request->validate([
'id_promo' => 'required|integer|exists:promociones,id',
'id_usuario' => 'required|integer',
'tipo_usuario' => 'required|in:jugador,aficionado',
]);
$data['id_qr'] = Str::uuid()->toString();
$promoQr = PromoQr::create($data);
return response()->json($promoQr, 201);
}
public function update(Request $request, $id)
{
$promoQr = PromoQr::findOrFail($id);
$data = $request->validate([
'usado' => 'nullable|boolean',
'usado_en' => 'nullable',
]);
$promoQr->update($data);
return response()->json($promoQr);
}
public function destroy($id)
{
$promoQr = PromoQr::findOrFail($id);
$promoQr->delete();
return response()->json(null, 204);
}
}
@@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers;
use App\Models\Promocion;
use Illuminate\Http\Request;
class PromocionController extends Controller
{
public function index(Request $request)
{
$promociones = Promocion::all();
if ($request->expectsJson()) {
return response()->json($promociones);
}
$categorias = $promociones->pluck('categoria')->filter()->unique()->values();
return view('promos.index', compact('promociones', 'categorias'));
}
public function show($id)
{
$promocion = Promocion::with('promoQrs')->findOrFail($id);
return response()->json($promocion);
}
public function store(Request $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' => 'nullable|string|max:200',
]);
$promocion = Promocion::create($data);
return response()->json($promocion, 201);
}
public function update(Request $request, $id)
{
$promocion = Promocion::findOrFail($id);
$data = $request->validate([
'nombre' => 'sometimes|string|max:100',
'direccion' => 'sometimes|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' => 'nullable|string|max:200',
]);
$promocion->update($data);
return response()->json($promocion);
}
public function destroy($id)
{
$promocion = Promocion::findOrFail($id);
$promocion->delete();
return response()->json(null, 204);
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace App\Http\Controllers;
use App\Models\QrCode;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class QrCodeController extends Controller
{
public function index()
{
$qrCodes = QrCode::with('evento', 'jugador', 'aficionado')->get();
return response()->json($qrCodes);
}
public function show($id)
{
$qrCode = QrCode::with('evento', 'jugador', 'aficionado')->findOrFail($id);
return response()->json($qrCode);
}
public function store(Request $request)
{
$data = $request->validate([
'id_evento' => 'required|string|max:36',
'id_jugador' => 'nullable|string|max:6',
'tipo_qr' => 'required|in:invitado,publico',
'escaneos_restantes' => 'nullable|integer|min:1',
'id_aficionado' => 'nullable|integer',
]);
$data['id_qr'] = Str::uuid()->toString();
$qrCode = QrCode::create($data);
return response()->json($qrCode, 201);
}
public function update(Request $request, $id)
{
$qrCode = QrCode::findOrFail($id);
$data = $request->validate([
'id_evento' => 'sometimes|string|max:36',
'id_jugador' => 'nullable|string|max:6',
'tipo_qr' => 'sometimes|in:invitado,publico',
'escaneos_restantes' => 'nullable|integer|min:1',
'id_aficionado' => 'nullable|integer',
]);
$qrCode->update($data);
return response()->json($qrCode);
}
public function destroy($id)
{
$qrCode = QrCode::findOrFail($id);
$qrCode->delete();
return response()->json(null, 204);
}
}
@@ -0,0 +1,129 @@
<?php
namespace App\Http\Controllers;
use App\Models\QrCode;
use Illuminate\Http\Request;
use Barryvdh\DomPDF\Facade\Pdf;
class QrDownloadController extends Controller
{
public function download($id)
{
$qr = QrCode::with(['evento.equipoLocal.club', 'evento.equipoVisitante.club', 'jugador.clubActual', 'aficionado'])
->where('id_qr', $id)
->firstOrFail();
// Verificar que el usuario tenga permiso para descargar este QR
$isUser = session()->has('user_logged_in') && session('user_logged_in');
$isAdmin = session()->has('admin_logged_in') && session('admin_logged_in');
if ($isUser) {
$userId = session('user_id');
$userTipo = session('user_tipo');
if ($userTipo === 'jugador' && $qr->id_jugador != $userId) abort(403);
if ($userTipo === 'aficionado' && $qr->id_aficionado != $userId) abort(403);
} elseif (!$isAdmin) {
abort(403);
}
$user = $qr->jugador ?: $qr->aficionado;
$userTipo = $qr->id_jugador ? 'jugador' : 'aficionado';
// Convertir imágenes a Base64 para DomPDF (más fiable que URLs remotas/locales)
$qrImageBase64 = '';
try {
$qrUrl = "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=" . urlencode($qr->id_qr);
$qrContent = @file_get_contents($qrUrl);
if ($qrContent) {
$qrImageBase64 = 'data:image/png;base64,' . base64_encode($qrContent);
}
} catch (\Exception $e) { }
$club = ($qr->evento && $qr->evento->equipoLocal && $qr->evento->equipoLocal->club) ? $qr->evento->equipoLocal->club : null;
$backgroundBase64 = null;
if ($club && $club->qr_background) {
// 1. Intentar en storage/app/public (ruta interna Laravel)
$pathInStorage = str_replace(['storage/', 'storage\\'], '', $club->qr_background);
$bgPath = storage_path('app/public/' . $pathInStorage);
// 2. Intentar en public_path (desarrollo local o symlink funcional)
if (!file_exists($bgPath)) {
$bgPath = public_path($club->qr_background);
}
// 3. Intentar estructura Hostinger (public_html es hermano de la carpeta laravel)
if (!file_exists($bgPath)) {
$bgPath = base_path('../public_html/' . $club->qr_background);
}
if (file_exists($bgPath)) {
$bgContent = @file_get_contents($bgPath);
if ($bgContent) {
$ext = pathinfo($bgPath, PATHINFO_EXTENSION);
$backgroundBase64 = 'data:image/' . $ext . ';base64,' . base64_encode($bgContent);
}
}
}
$logoBase64 = null;
$logoPathFinal = null;
// Logo del club del jugador (no del local) si corresponde:
// jugador → su club actual; aficionado/sin club → club local; fallback → logo OnAPB.
$logoClub = null;
if ($userTipo === 'jugador' && $qr->jugador && $qr->jugador->clubActual) {
$logoClub = $qr->jugador->clubActual;
} elseif ($club) {
$logoClub = $club;
}
if ($logoClub && $logoClub->imagen) {
$logoInStorage = str_replace(['storage/', 'storage\\'], '', $logoClub->imagen);
$lPath = storage_path('app/public/' . $logoInStorage);
if (!file_exists($lPath)) {
$lPath = public_path($logoClub->imagen);
}
if (!file_exists($lPath)) {
$lPath = base_path('../public_html/' . $logoClub->imagen);
}
if (file_exists($lPath)) {
$logoPathFinal = $lPath;
}
}
// Si no hay logo, usar logo general
if (!$logoPathFinal) {
if (file_exists(public_path('logo.png'))) {
$logoPathFinal = public_path('logo.png');
}
}
if ($logoPathFinal) {
$logoContent = @file_get_contents($logoPathFinal);
if ($logoContent) {
$ext = pathinfo($logoPathFinal, PATHINFO_EXTENSION);
$logoBase64 = 'data:image/' . $ext . ';base64,' . base64_encode($logoContent);
}
}
$data = [
'qr' => $qr,
'user' => $user,
'userTipo' => $userTipo,
'club' => $club,
'qrImageBase64' => $qrImageBase64,
'backgroundBase64' => $backgroundBase64,
'logoBase64' => $logoBase64,
];
$pdf = Pdf::loadView('pdf.qr_ticket', $data)
->setPaper([0, 0, 320, 500], 'portrait');
return $pdf->download("onapb_qr_{$qr->id_qr}.pdf");
}
}
@@ -0,0 +1,122 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Equipo;
use App\Models\EquipoSeguimiento;
use App\Models\Evento;
use App\Services\NotificacionService;
class SeguimientoController extends Controller
{
private NotificacionService $notifService;
public function __construct(NotificacionService $notifService)
{
$this->notifService = $notifService;
}
private function getUserSession(): ?array
{
if (!session()->has('user_logged_in')) return null;
return ['tipo' => session('user_tipo'), 'id' => session('user_id')];
}
/**
* POST /seguimiento/equipo/{id} — Toggle: seguir o dejar de seguir un equipo
*/
public function toggle(int $id)
{
$u = $this->getUserSession();
if (!$u) {
return response()->json(['ok' => false, 'msg' => 'Debés iniciar sesión para seguir equipos.'], 401);
}
$equipo = Equipo::find($id);
if (!$equipo) {
return response()->json(['ok' => false, 'msg' => 'Equipo no encontrado.'], 404);
}
$existing = EquipoSeguimiento::where('id_equipo', $id)
->where('tipo_usuario', $u['tipo'])
->where('id_usuario', (string)$u['id'])
->first();
if ($existing) {
$existing->delete();
return response()->json(['ok' => true, 'siguiendo' => false, 'msg' => 'Dejaste de seguir este equipo.']);
}
EquipoSeguimiento::create([
'id_equipo' => $id,
'tipo_usuario'=> $u['tipo'],
'id_usuario' => (string)$u['id'],
'created_at' => now(),
]);
return response()->json(['ok' => true, 'siguiendo' => true, 'msg' => '¡Ahora seguís este equipo!']);
}
/**
* GET /seguimiento/mis-equipos — Devuelve equipos seguidos y próximos partidos
*/
public function misEquipos()
{
$u = $this->getUserSession();
if (!$u) return response()->json(['equipos' => []]);
$seguimientos = EquipoSeguimiento::with(['equipo.club'])
->where('tipo_usuario', $u['tipo'])
->where('id_usuario', (string)$u['id'])
->get();
$equiposIds = $seguimientos->pluck('id_equipo');
// Próximos partidos de los equipos seguidos
$proximosPartidos = Evento::with(['equipoLocal.club', 'equipoVisitante.club'])
->where('fecha_evento', '>=', now()->toDateString())
->where(function ($q) use ($equiposIds) {
$q->whereIn('id_equipo_local', $equiposIds)
->orWhereIn('id_equipo_visitante', $equiposIds);
})
->orderBy('fecha_evento')
->orderBy('hora_inicio')
->limit(10)
->get();
return response()->json([
'equipos' => $seguimientos->map(fn($s) => [
'id' => $s->id_equipo,
'nombre' => ($s->equipo->club->nombre ?? 'Equipo') . ' ' . $s->equipo->categoria . ($s->equipo->division ? ' ' . $s->equipo->division : ''),
'club' => $s->equipo->club->nombre ?? '—',
'categoria'=> $s->equipo->categoria,
'division' => $s->equipo->division,
]),
'proximos_partidos' => $proximosPartidos->map(fn($e) => [
'id' => $e->id_evento,
'fecha' => $e->fecha_evento->format('d/m/Y'),
'hora' => $e->hora_inicio ? $e->hora_inicio->format('H:i') : '',
'local' => $e->equipoLocal->club->nombre ?? '?',
'visitante'=> $e->equipoVisitante->club->nombre ?? '?',
'sede' => $e->sede,
]),
]);
}
/**
* GET /seguimiento/estado/{id_equipo} — ¿El usuario sigue este equipo?
*/
public function estado(int $id)
{
$u = $this->getUserSession();
if (!$u) return response()->json(['siguiendo' => false]);
$siguiendo = EquipoSeguimiento::where('id_equipo', $id)
->where('tipo_usuario', $u['tipo'])
->where('id_usuario', (string)$u['id'])
->exists();
return response()->json(['siguiendo' => $siguiendo]);
}
}
+85
View File
@@ -0,0 +1,85 @@
<?php
namespace App\Http\Controllers;
use App\Models\Torneo;
use App\Models\Equipo;
use App\Models\Evento;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class TorneoController extends Controller
{
public function standings(Request $request, $id)
{
$selectedGroup = $request->query('grupo');
$torneo = Torneo::with('equipos.club')->findOrFail($id);
$tournamentService = new \App\Services\TournamentService();
$stats = $tournamentService->getStandings($id, true);
$grupos = array_keys($stats);
if ($selectedGroup && isset($stats[$selectedGroup])) {
$stats = [$selectedGroup => $stats[$selectedGroup]];
}
$followedTeamIds = [];
if (session('user_logged_in') && session('user_id')) {
$followedTeamIds = \App\Models\EquipoSeguimiento::where('id_usuario', session('user_id'))
->where('tipo_usuario', session('user_tipo'))
->pluck('id_equipo')
->toArray();
}
return view('torneos.standings', compact('torneo', 'stats', 'grupos', 'selectedGroup', 'followedTeamIds'));
}
public function topScorers(Request $request, $id)
{
$torneo = Torneo::findOrFail($id);
$selectedGroup = $request->query('grupo');
$query = \App\Models\EventoJugador::with(['jugador.clubActual'])
->whereHas('evento', function($q) use ($id) {
$q->where('id_torneo', $id)->whereNotNull('marcador_local');
});
if ($selectedGroup) {
$query->whereHas('evento.equipoLocal.torneos', function($q) use ($id, $selectedGroup) {
$q->where('torneos.id', $id)->where('torneo_equipo.grupo', $selectedGroup);
});
}
$scorers = $query->select('id_jugador', DB::raw('SUM(puntos) as total_puntos'), DB::raw('COUNT(id_evento) as partidos_jugados'))
->groupBy('id_jugador')
->orderByDesc('total_puntos')
->take(20)->get();
$grupos = DB::table('torneo_equipo')->where('id_torneo', $id)->distinct()->pluck('grupo')->filter();
return view('torneos.scorers', compact('torneo', 'scorers', 'grupos', 'selectedGroup'));
}
public function playoffs(Request $request, $id)
{
$selectedGroup = $request->query('grupo');
$torneo = Torneo::with('equipos.club')->findOrFail($id);
// Get groups from pivot
$grupos = DB::table('torneo_equipo')->where('id_torneo', $id)->distinct()->pluck('grupo')->filter();
$ts = new \App\Services\TournamentService();
$bracket = $ts->getPlayoffBrackets($id);
// Map to simpler keys if needed by view
$bracket = [
'cuartos' => collect($bracket[\App\Models\Evento::FASE_CUARTOS] ?? []),
'semis' => collect($bracket[\App\Models\Evento::FASE_SEMIS] ?? []),
'final' => collect($bracket[\App\Models\Evento::FASE_FINAL] ?? []),
];
return view('torneos.playoffs', compact('torneo', 'bracket', 'grupos', 'selectedGroup'));
}
}