From 0841794c50603277e95f8d78fd4bd3f3e61a1b75 Mon Sep 17 00:00:00 2001 From: Laucha1312 Date: Thu, 4 Jun 2026 15:14:46 -0300 Subject: [PATCH] todo mal --- app/AI/Prompts/SystemPromptAdmin.php | 72 - app/AI/Prompts/SystemPromptPublic.php | 51 - app/AI/Tools/CargarPuntajeTool.php | 30 - app/AI/Tools/CrearPartidoTool.php | 43 - app/AI/Tools/EliminarNoticiaTool.php | 29 - app/AI/Tools/EliminarPartidoTool.php | 28 - app/AI/Tools/ListarEquiposTool.php | 33 - app/AI/Tools/ListarEventosTool.php | 40 - app/AI/Tools/ListarTorneosTool.php | 20 - app/AI/Tools/RedactarNoticiaTool.php | 33 - app/Console/Commands/CleanupOldEvents.php | 51 - app/Console/Commands/OptimizeImages.php | 196 -- app/Console/Commands/PurgeAgentThreads.php | 21 - app/Console/Commands/RecordatorioPartidos.php | 86 - app/Console/Commands/ReporteSemanal.php | 106 -- .../Controllers/Admin/AdminUserController.php | 115 -- .../Admin/CarouselItemController.php | 113 -- .../Controllers/Admin/CategoriaController.php | 74 - .../Controllers/Admin/FixtureController.php | 380 ---- app/Http/Controllers/Admin/PaseController.php | 129 -- app/Http/Controllers/AdminController.php | 1667 ----------------- app/Http/Controllers/AdminUserController.php | 55 - app/Http/Controllers/AficionadoController.php | 67 - app/Http/Controllers/AuthController.php | 411 ---- app/Http/Controllers/ClubController.php | 47 - app/Http/Controllers/Controller.php | 8 - .../Controllers/DocumentacionController.php | 221 --- app/Http/Controllers/EquipoController.php | 57 - app/Http/Controllers/EventoController.php | 84 - .../Controllers/GeniusAgentController.php | 81 - app/Http/Controllers/HomeController.php | 86 - app/Http/Controllers/JugadorController.php | 76 - .../Controllers/JugadorEquipoController.php | 73 - app/Http/Controllers/NoticiaController.php | 61 - .../Controllers/NotificacionController.php | 143 -- app/Http/Controllers/PanelController.php | 363 ---- app/Http/Controllers/PromoQrController.php | 52 - app/Http/Controllers/PromocionController.php | 70 - app/Http/Controllers/QrCodeController.php | 57 - app/Http/Controllers/QrDownloadController.php | 129 -- .../Controllers/SeguimientoController.php | 122 -- app/Http/Controllers/TorneoController.php | 85 - app/Http/Middleware/SecurityHeaders.php | 40 - app/Mail/QrCodeMail.php | 29 - app/Mail/ResetPasswordMail.php | 27 - app/Mail/WelcomeMail.php | 27 - app/Models/AdminUser.php | 36 - app/Models/Aficionado.php | 38 - app/Models/AgentThread.php | 42 - app/Models/CarouselItem.php | 25 - app/Models/Categoria.php | 19 - app/Models/Club.php | 50 - app/Models/Configuracion.php | 45 - app/Models/Equipo.php | 41 - app/Models/EquipoSeguimiento.php | 28 - app/Models/Evento.php | 96 - app/Models/EventoJugador.php | 27 - app/Models/Jugador.php | 79 - app/Models/JugadorEquipo.php | 34 - app/Models/Noticia.php | 26 - app/Models/Notificacion.php | 40 - app/Models/Pase.php | 33 - app/Models/PromoQr.php | 46 - app/Models/Promocion.php | 35 - app/Models/PushSubscription.php | 24 - app/Models/QrCode.php | 48 - app/Models/Sponsor.php | 23 - app/Models/Torneo.php | 33 - app/Models/User.php | 49 - app/Observers/EventoObserver.php | 142 -- app/Providers/AppServiceProvider.php | 57 - app/Services/FixtureService.php | 138 -- app/Services/GeniusAgentService.php | 229 --- app/Services/ImageOptimizer.php | 106 -- app/Services/NotificacionService.php | 211 --- app/Services/TournamentService.php | 149 -- 76 files changed, 7737 deletions(-) delete mode 100644 app/AI/Prompts/SystemPromptAdmin.php delete mode 100644 app/AI/Prompts/SystemPromptPublic.php delete mode 100644 app/AI/Tools/CargarPuntajeTool.php delete mode 100644 app/AI/Tools/CrearPartidoTool.php delete mode 100644 app/AI/Tools/EliminarNoticiaTool.php delete mode 100644 app/AI/Tools/EliminarPartidoTool.php delete mode 100644 app/AI/Tools/ListarEquiposTool.php delete mode 100644 app/AI/Tools/ListarEventosTool.php delete mode 100644 app/AI/Tools/ListarTorneosTool.php delete mode 100644 app/AI/Tools/RedactarNoticiaTool.php delete mode 100644 app/Console/Commands/CleanupOldEvents.php delete mode 100644 app/Console/Commands/OptimizeImages.php delete mode 100644 app/Console/Commands/PurgeAgentThreads.php delete mode 100644 app/Console/Commands/RecordatorioPartidos.php delete mode 100644 app/Console/Commands/ReporteSemanal.php delete mode 100644 app/Http/Controllers/Admin/AdminUserController.php delete mode 100644 app/Http/Controllers/Admin/CarouselItemController.php delete mode 100644 app/Http/Controllers/Admin/CategoriaController.php delete mode 100644 app/Http/Controllers/Admin/FixtureController.php delete mode 100644 app/Http/Controllers/Admin/PaseController.php delete mode 100644 app/Http/Controllers/AdminController.php delete mode 100644 app/Http/Controllers/AdminUserController.php delete mode 100644 app/Http/Controllers/AficionadoController.php delete mode 100644 app/Http/Controllers/AuthController.php delete mode 100644 app/Http/Controllers/ClubController.php delete mode 100644 app/Http/Controllers/Controller.php delete mode 100644 app/Http/Controllers/DocumentacionController.php delete mode 100644 app/Http/Controllers/EquipoController.php delete mode 100644 app/Http/Controllers/EventoController.php delete mode 100644 app/Http/Controllers/GeniusAgentController.php delete mode 100644 app/Http/Controllers/HomeController.php delete mode 100644 app/Http/Controllers/JugadorController.php delete mode 100644 app/Http/Controllers/JugadorEquipoController.php delete mode 100644 app/Http/Controllers/NoticiaController.php delete mode 100644 app/Http/Controllers/NotificacionController.php delete mode 100644 app/Http/Controllers/PanelController.php delete mode 100644 app/Http/Controllers/PromoQrController.php delete mode 100644 app/Http/Controllers/PromocionController.php delete mode 100644 app/Http/Controllers/QrCodeController.php delete mode 100644 app/Http/Controllers/QrDownloadController.php delete mode 100644 app/Http/Controllers/SeguimientoController.php delete mode 100644 app/Http/Controllers/TorneoController.php delete mode 100644 app/Http/Middleware/SecurityHeaders.php delete mode 100644 app/Mail/QrCodeMail.php delete mode 100644 app/Mail/ResetPasswordMail.php delete mode 100644 app/Mail/WelcomeMail.php delete mode 100644 app/Models/AdminUser.php delete mode 100644 app/Models/Aficionado.php delete mode 100644 app/Models/AgentThread.php delete mode 100644 app/Models/CarouselItem.php delete mode 100644 app/Models/Categoria.php delete mode 100644 app/Models/Club.php delete mode 100644 app/Models/Configuracion.php delete mode 100644 app/Models/Equipo.php delete mode 100644 app/Models/EquipoSeguimiento.php delete mode 100644 app/Models/Evento.php delete mode 100644 app/Models/EventoJugador.php delete mode 100644 app/Models/Jugador.php delete mode 100644 app/Models/JugadorEquipo.php delete mode 100644 app/Models/Noticia.php delete mode 100644 app/Models/Notificacion.php delete mode 100644 app/Models/Pase.php delete mode 100644 app/Models/PromoQr.php delete mode 100644 app/Models/Promocion.php delete mode 100644 app/Models/PushSubscription.php delete mode 100644 app/Models/QrCode.php delete mode 100644 app/Models/Sponsor.php delete mode 100644 app/Models/Torneo.php delete mode 100644 app/Models/User.php delete mode 100644 app/Observers/EventoObserver.php delete mode 100644 app/Providers/AppServiceProvider.php delete mode 100644 app/Services/FixtureService.php delete mode 100644 app/Services/GeniusAgentService.php delete mode 100644 app/Services/ImageOptimizer.php delete mode 100644 app/Services/NotificacionService.php delete mode 100644 app/Services/TournamentService.php diff --git a/app/AI/Prompts/SystemPromptAdmin.php b/app/AI/Prompts/SystemPromptAdmin.php deleted file mode 100644 index 199f942..0000000 --- a/app/AI/Prompts/SystemPromptAdmin.php +++ /dev/null @@ -1,72 +0,0 @@ -'); - $faqPos = strpos($content, '## ❓ Preguntas Frecuentes'); - - if ($cap4Pos !== false && $faqPos !== false && $faqPos > $cap4Pos) { - $content = substr($content, 0, $cap4Pos) . substr($content, $faqPos); - } - - return $content; - } -} diff --git a/app/AI/Tools/CargarPuntajeTool.php b/app/AI/Tools/CargarPuntajeTool.php deleted file mode 100644 index cb54d13..0000000 --- a/app/AI/Tools/CargarPuntajeTool.php +++ /dev/null @@ -1,30 +0,0 @@ - "No se encontró el partido con ID: {$id_evento}"]); - } - - $evento->update([ - 'marcador_local' => $marcador_local, - 'marcador_visitante' => $marcador_visitante, - ]); - - return json_encode([ - 'success' => true, - 'mensaje' => "Puntaje actualizado: {$marcador_local} - {$marcador_visitante}", - ]); - } -} diff --git a/app/AI/Tools/CrearPartidoTool.php b/app/AI/Tools/CrearPartidoTool.php deleted file mode 100644 index 1b94e5b..0000000 --- a/app/AI/Tools/CrearPartidoTool.php +++ /dev/null @@ -1,43 +0,0 @@ - (string) Str::uuid(), - 'id_equipo_local' => $id_equipo_local, - 'id_equipo_visitante' => $id_equipo_visitante, - 'fecha_evento' => $fecha_evento, - 'hora_inicio' => str_contains($hora_inicio, ':') ? $hora_inicio . (strlen($hora_inicio) === 5 ? ':00' : '') : $hora_inicio, - 'hora_fin' => str_contains($hora_fin, ':') ? $hora_fin . (strlen($hora_fin) === 5 ? ':00' : '') : $hora_fin, - 'sede' => $sede, - 'id_torneo' => $id_torneo, - 'precio' => $precio ?? 0, - 'fase' => Evento::FASE_REGULAR, - ]); - - return json_encode([ - 'success' => true, - 'id_evento' => $evento->id_evento, - 'mensaje' => "Partido creado correctamente. ID: {$evento->id_evento}", - ]); - } catch (\Throwable $e) { - return json_encode(['error' => 'No se pudo crear el partido: ' . $e->getMessage()]); - } - } -} diff --git a/app/AI/Tools/EliminarNoticiaTool.php b/app/AI/Tools/EliminarNoticiaTool.php deleted file mode 100644 index 0497111..0000000 --- a/app/AI/Tools/EliminarNoticiaTool.php +++ /dev/null @@ -1,29 +0,0 @@ - "No se encontró la noticia con ID: {$id_noticia}"]); - } - - $titulo = $noticia->titulo; - $noticia->delete(); - - return json_encode([ - 'success' => true, - 'mensaje' => "Noticia \"{$titulo}\" (ID {$id_noticia}) eliminada correctamente.", - ]); - } catch (\Throwable $e) { - return json_encode(['error' => 'No se pudo eliminar la noticia: ' . $e->getMessage()]); - } - } -} diff --git a/app/AI/Tools/EliminarPartidoTool.php b/app/AI/Tools/EliminarPartidoTool.php deleted file mode 100644 index 0308804..0000000 --- a/app/AI/Tools/EliminarPartidoTool.php +++ /dev/null @@ -1,28 +0,0 @@ - "No se encontró el partido con ID: {$id_evento}"]); - } - - $evento->delete(); - - return json_encode([ - 'success' => true, - 'mensaje' => "Partido {$id_evento} eliminado correctamente (soft delete).", - ]); - } catch (\Throwable $e) { - return json_encode(['error' => 'No se pudo eliminar el partido: ' . $e->getMessage()]); - } - } -} diff --git a/app/AI/Tools/ListarEquiposTool.php b/app/AI/Tools/ListarEquiposTool.php deleted file mode 100644 index 3ab9def..0000000 --- a/app/AI/Tools/ListarEquiposTool.php +++ /dev/null @@ -1,33 +0,0 @@ -join('torneo_equipo', 'equipos.id_equipo', '=', 'torneo_equipo.id_equipo') - ->where('torneo_equipo.id_torneo', $id_torneo); - - if ($grupo !== null) { - $query->where('torneo_equipo.grupo', $grupo); - } - - $query->select('equipos.id_equipo', 'equipos.categoria', 'equipos.division', 'equipos.id_club'); - } - - $equipos = $query->get()->map(fn($e) => [ - 'id_equipo' => $e->id_equipo, - 'categoria' => $e->categoria, - 'division' => $e->division, - 'club' => $e->club?->nombre, - ]); - - return json_encode($equipos); - } -} diff --git a/app/AI/Tools/ListarEventosTool.php b/app/AI/Tools/ListarEventosTool.php deleted file mode 100644 index fb6aa72..0000000 --- a/app/AI/Tools/ListarEventosTool.php +++ /dev/null @@ -1,40 +0,0 @@ -whereNull('deleted_at'); - - if ($fecha_desde) { - $query->whereDate('fecha_evento', '>=', $fecha_desde); - } - if ($fecha_hasta) { - $query->whereDate('fecha_evento', '<=', $fecha_hasta); - } - if ($id_torneo) { - $query->where('id_torneo', $id_torneo); - } - - $eventos = $query->orderBy('fecha_evento')->get()->map(fn($e) => [ - 'id_evento' => $e->id_evento, - 'fecha' => $e->fecha_evento?->format('Y-m-d'), - 'hora' => $e->hora_inicio?->format('H:i'), - 'local' => $e->equipoLocal?->club?->nombre, - 'visitante' => $e->equipoVisitante?->club?->nombre, - 'marcador_local' => $e->marcador_local, - 'marcador_visitante' => $e->marcador_visitante, - 'sede' => $e->sede, - ]); - - return json_encode($eventos); - } -} diff --git a/app/AI/Tools/ListarTorneosTool.php b/app/AI/Tools/ListarTorneosTool.php deleted file mode 100644 index a1b8f0e..0000000 --- a/app/AI/Tools/ListarTorneosTool.php +++ /dev/null @@ -1,20 +0,0 @@ -get()->map(fn ($t) => [ - 'id_torneo' => $t->id, - 'nombre' => $t->nombre, - 'fecha_inicio' => $t->fecha_inicio?->format('Y-m-d'), - 'fecha_fin' => $t->fecha_fin?->format('Y-m-d'), - ]); - - return json_encode($torneos); - } -} diff --git a/app/AI/Tools/RedactarNoticiaTool.php b/app/AI/Tools/RedactarNoticiaTool.php deleted file mode 100644 index 271ad01..0000000 --- a/app/AI/Tools/RedactarNoticiaTool.php +++ /dev/null @@ -1,33 +0,0 @@ - $titulo, - 'contenido' => $contenido, - 'fecha' => now(), - 'id_torneo' => $id_torneo, - 'categoria' => $categoria, - ]); - - return json_encode([ - 'success' => true, - 'id_noticia' => $noticia->id, - 'mensaje' => "Noticia \"{$titulo}\" creada correctamente.", - ]); - } catch (\Throwable $e) { - return json_encode(['error' => 'No se pudo crear la noticia: ' . $e->getMessage()]); - } - } -} diff --git a/app/Console/Commands/CleanupOldEvents.php b/app/Console/Commands/CleanupOldEvents.php deleted file mode 100644 index 6c0d370..0000000 --- a/app/Console/Commands/CleanupOldEvents.php +++ /dev/null @@ -1,51 +0,0 @@ -subDays((int)$dias); - - $eventosAEliminar = \App\Models\Evento::withTrashed() - ->where('fecha_evento', '<', $fechaLimite->toDateString()) - ->get(); - $total = $eventosAEliminar->count(); - - if ($total === 0) { - $this->info("No hay eventos antiguos para eliminar."); - return; - } - - foreach ($eventosAEliminar as $evento) { - /** @var \App\Models\Evento $evento */ - // Eliminar QRs asociados - $evento->qrCodes()->delete(); - // Ya no eliminamos el evento para mantener registro de puntos y goleadores - // $evento->delete(); - } - - $this->info("Se han limpiado los QRs de $total eventos antiguos (Antigüedad > $dias días). Los eventos permanecen en el sistema."); - } -} diff --git a/app/Console/Commands/OptimizeImages.php b/app/Console/Commands/OptimizeImages.php deleted file mode 100644 index d2a430c..0000000 --- a/app/Console/Commands/OptimizeImages.php +++ /dev/null @@ -1,196 +0,0 @@ - ['maxWidth' => 1600, 'quality' => 82], - 'noticias' => ['maxWidth' => 1200, 'quality' => 82], - 'promos' => ['maxWidth' => 1200, 'quality' => 82], - 'clubes' => ['maxWidth' => 512, 'quality' => 85], - 'sponsors' => ['maxWidth' => 600, 'quality' => 85], - 'qr' => ['maxWidth' => 800, 'quality' => 85], - ]; - - public function handle(): int - { - $apply = (bool) $this->option('apply'); - $only = $this->option('folder'); - $noBackup = (bool) $this->option('no-backup'); - - if (!extension_loaded('gd')) { - $this->error('La extension GD de PHP no esta instalada.'); - return self::FAILURE; - } - - // Usa la config real del disk 'public' (respeta override de Hostinger) - $base = realpath(config('filesystems.disks.public.root')) ?: config('filesystems.disks.public.root'); - if (!is_dir($base)) { - $this->error("No existe: $base"); - return self::FAILURE; - } - $this->line("Base: $base"); - - $backupBase = $base . '/_backup_optimize'; - if ($apply && !$noBackup && !is_dir($backupBase)) { - mkdir($backupBase, 0755, true); - } - - $this->line(''); - $this->line($apply - ? 'MODO APLICACION — los archivos seran reemplazados.' - : 'MODO DRY-RUN — no se modifica nada. Pasa --apply para aplicar.'); - $this->line(''); - - $totalOrig = 0; $totalNew = 0; $totalProcessed = 0; $totalSkipped = 0; - - foreach ($this->config as $folder => $cfg) { - if ($only && $only !== $folder) continue; - - $path = $base . '/' . $folder; - if (!is_dir($path)) { - $this->warn(" - $folder/ (no existe, skip)"); - continue; - } - - $this->info("Carpeta: $folder/ (max {$cfg['maxWidth']}px, q={$cfg['quality']})"); - $files = glob($path . '/*.{jpg,jpeg,png,webp,JPG,JPEG,PNG,WEBP}', GLOB_BRACE); - - $folderOrig = 0; $folderNew = 0; $folderProcessed = 0; $folderSkipped = 0; - - foreach ($files as $file) { - $origSize = filesize($file); - $totalOrig += $origSize; $folderOrig += $origSize; - - $info = @getimagesize($file); - if (!$info) { $folderSkipped++; $totalSkipped++; continue; } - - [$w, $h] = $info; - $needsResize = $w > $cfg['maxWidth']; - $needsRecomp = $origSize > 100 * 1024; // skip si ya pesa <100KB - - if (!$needsResize && !$needsRecomp) { - $folderSkipped++; $totalSkipped++; $totalNew += $origSize; $folderNew += $origSize; - continue; - } - - $result = $this->processImage($file, $cfg['maxWidth'], $cfg['quality']); - if ($result === null) { - $folderSkipped++; $totalSkipped++; $totalNew += $origSize; $folderNew += $origSize; - continue; - } - - [$newBytes, $newW, $newH] = $result; - - if ($newBytes >= $origSize) { - $folderSkipped++; $totalSkipped++; $totalNew += $origSize; $folderNew += $origSize; - $this->line(sprintf(" - %-40s %dKB no mejora, skip", basename($file), $origSize / 1024)); - continue; - } - - $folderProcessed++; $totalProcessed++; - $totalNew += $newBytes; $folderNew += $newBytes; - - $reduction = round((1 - $newBytes / $origSize) * 100, 1); - $this->line(sprintf( - " %s %-40s %dKB -> %dKB (-%s%%) %dx%d -> %dx%d", - $apply ? 'OK' : '--', - basename($file), $origSize / 1024, $newBytes / 1024, $reduction, - $w, $h, $newW, $newH - )); - - if ($apply) { - if (!$noBackup) { - $backupDir = $backupBase . '/' . $folder; - if (!is_dir($backupDir)) mkdir($backupDir, 0755, true); - copy($file, $backupDir . '/' . basename($file)); - } - file_put_contents($file, $result[3]); - } - } - - $this->line(sprintf( - " -> %s files procesados, %s skip, %s -> %s (-%s%%)", - $folderProcessed, $folderSkipped, - $this->fmt($folderOrig), $this->fmt($folderNew), - $folderOrig > 0 ? round((1 - $folderNew / max($folderOrig, 1)) * 100, 1) : 0 - )); - $this->line(''); - } - - $this->line(str_repeat('=', 60)); - $this->info(sprintf( - 'TOTAL: %d procesados, %d skip. %s -> %s (ahorro: %s, -%s%%)', - $totalProcessed, $totalSkipped, - $this->fmt($totalOrig), $this->fmt($totalNew), - $this->fmt($totalOrig - $totalNew), - $totalOrig > 0 ? round((1 - $totalNew / max($totalOrig, 1)) * 100, 1) : 0 - )); - - if (!$apply && $totalProcessed > 0) { - $this->line(''); - $this->warn('Para aplicar realmente: php artisan optimize:images --apply'); - } - - return self::SUCCESS; - } - - private function processImage(string $file, int $maxWidth, int $quality): ?array - { - $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); - - $img = match ($ext) { - 'jpg', 'jpeg' => @imagecreatefromjpeg($file), - 'png' => @imagecreatefrompng($file), - 'webp' => @imagecreatefromwebp($file), - default => null, - }; - if (!$img) return null; - - $w = imagesx($img); $h = imagesy($img); - $newW = $w; $newH = $h; - - if ($w > $maxWidth) { - $newW = $maxWidth; - $newH = (int) round($h * ($maxWidth / $w)); - $resized = imagecreatetruecolor($newW, $newH); - - if (in_array($ext, ['png', 'webp'])) { - imagealphablending($resized, false); - imagesavealpha($resized, true); - $transparent = imagecolorallocatealpha($resized, 0, 0, 0, 127); - imagefilledrectangle($resized, 0, 0, $newW, $newH, $transparent); - } - - imagecopyresampled($resized, $img, 0, 0, 0, 0, $newW, $newH, $w, $h); - $img = $resized; - } - - ob_start(); - match ($ext) { - 'jpg', 'jpeg' => imagejpeg($img, null, $quality), - 'png' => imagepng($img, null, 9), - 'webp' => imagewebp($img, null, $quality), - }; - $bytes = ob_get_clean(); - - return [strlen($bytes), $newW, $newH, $bytes]; - } - - private function fmt(int $bytes): string - { - if ($bytes < 1024) return $bytes . 'B'; - if ($bytes < 1024 * 1024) return round($bytes / 1024, 1) . 'KB'; - return round($bytes / 1024 / 1024, 2) . 'MB'; - } -} diff --git a/app/Console/Commands/PurgeAgentThreads.php b/app/Console/Commands/PurgeAgentThreads.php deleted file mode 100644 index c95e329..0000000 --- a/app/Console/Commands/PurgeAgentThreads.php +++ /dev/null @@ -1,21 +0,0 @@ -delete(); - - $this->info("Se eliminaron {$deleted} hilo(s) expirado(s)."); - - return self::SUCCESS; - } -} diff --git a/app/Console/Commands/RecordatorioPartidos.php b/app/Console/Commands/RecordatorioPartidos.php deleted file mode 100644 index 2f56a12..0000000 --- a/app/Console/Commands/RecordatorioPartidos.php +++ /dev/null @@ -1,86 +0,0 @@ -addHours(24); - $hasta = Carbon::now()->addHours(50); // ventana de ~26hs para no perder ninguno - - $eventos = Evento::with(['equipoLocal.club', 'equipoVisitante.club']) - ->whereNotNull('id_equipo_local') - ->whereNotNull('id_equipo_visitante') - ->whereBetween('fecha_evento', [$desde->toDateString(), $hasta->toDateString()]) - ->get(); - - if ($eventos->isEmpty()) { - $this->info('No hay partidos en las próximas 48hs.'); - return self::SUCCESS; - } - - $totalNotif = 0; - - foreach ($eventos as $evento) { - $nombreLocal = $evento->equipoLocal->club->nombre ?? '?'; - $nombreVisitante = $evento->equipoVisitante->club->nombre ?? '?'; - $fechaStr = $evento->fecha_evento->format('d/m/Y'); - $horaStr = $evento->hora_inicio ? $evento->hora_inicio->format('H:i') : ''; - $sedeStr = $evento->sede ? " | {$evento->sede}" : ''; - - $titulo = "⏰ Partido mañana: {$nombreLocal} vs {$nombreVisitante}"; - $mensaje = "Recordatorio: el partido es el {$fechaStr}" . ($horaStr ? " a las {$horaStr}" : '') . $sedeStr . '.'; - $url = '/eventos/' . $evento->id_evento; - - // Recolectar destinatarios - $idEquipos = array_filter([$evento->id_equipo_local, $evento->id_equipo_visitante]); - $destinatarios = []; - $yaAgregados = []; - - $seguimientos = EquipoSeguimiento::whereIn('id_equipo', $idEquipos)->get(); - foreach ($seguimientos as $s) { - $key = $s->tipo_usuario . ':' . $s->id_usuario; - if (!isset($yaAgregados[$key])) { - $destinatarios[] = ['tipo' => $s->tipo_usuario, 'id' => $s->id_usuario]; - $yaAgregados[$key] = true; - } - } - - $jugadores = \DB::table('jugador_equipo') - ->whereIn('id_equipo', $idEquipos) - ->pluck('id_jugador'); - - foreach ($jugadores as $idJ) { - $key = 'jugador:' . $idJ; - if (!isset($yaAgregados[$key])) { - $destinatarios[] = ['tipo' => 'jugador', 'id' => $idJ]; - $yaAgregados[$key] = true; - } - } - - if (empty($destinatarios)) continue; - - if ($this->option('test')) { - $this->line(" [TEST] Evento {$evento->id_evento}: {$titulo} → " . count($destinatarios) . " destinatarios"); - } else { - $notifService->enviarMasivo($destinatarios, 'partido', $titulo, $mensaje, $url); - } - - $totalNotif += count($destinatarios); - } - - $this->info("✅ Recordatorios enviados: {$eventos->count()} partidos, {$totalNotif} notificaciones."); - return self::SUCCESS; - } -} diff --git a/app/Console/Commands/ReporteSemanal.php b/app/Console/Commands/ReporteSemanal.php deleted file mode 100644 index 9eb1abb..0000000 --- a/app/Console/Commands/ReporteSemanal.php +++ /dev/null @@ -1,106 +0,0 @@ -copy()->subDays(7)->startOfDay(); - $semanaAnteriorHasta = $ahora->copy()->subDay()->endOfDay(); - $proximaSemanaHasta = $ahora->copy()->addDays(7)->endOfDay(); - - // ── Partidos jugados la semana anterior ── - $jugados = Evento::with(['equipoLocal.club', 'equipoVisitante.club']) - ->whereBetween('fecha_evento', [$semanaAnteriorDesde->toDateString(), $semanaAnteriorHasta->toDateString()]) - ->whereNotNull('marcador_local') - ->get(); - - // ── Próximos partidos ── - $proximos = Evento::with(['equipoLocal.club', 'equipoVisitante.club']) - ->where('fecha_evento', '>=', $ahora->toDateString()) - ->where('fecha_evento', '<=', $proximaSemanaHasta->toDateString()) - ->orderBy('fecha_evento') - ->orderBy('hora_inicio') - ->get(); - - // ── Top goleadores ── - $topGoleadores = DB::table('evento_jugador') - ->join('jugadores', 'evento_jugador.id_jugador', '=', 'jugadores.id_jugador') - ->selectRaw('jugadores.nombre, jugadores.apellido, SUM(evento_jugador.puntos) as total_puntos, COUNT(evento_jugador.id_evento) as partidos') - ->groupBy('jugadores.id_jugador', 'jugadores.nombre', 'jugadores.apellido') - ->orderByDesc('total_puntos') - ->limit(10) - ->get(); - - // ── QRs de la semana ── - $qrsSemana = DB::table('qr_codes') - ->where('creado', '>=', $semanaAnteriorDesde) - ->count(); - - $qrsValidados = DB::table('qr_codes') - ->where('creado', '>=', $semanaAnteriorDesde) - ->where('escaneos_restantes', 0) - ->count(); - - $data = compact('jugados', 'proximos', 'topGoleadores', 'qrsSemana', 'qrsValidados', 'semanaAnteriorDesde', 'semanaAnteriorHasta'); - - if ($this->option('dry-run')) { - $this->info('=== REPORTE SEMANAL (DRY RUN) ==='); - $this->line("Partidos jugados: {$jugados->count()}"); - $this->line("Próximos partidos: {$proximos->count()}"); - $this->line("Top goleador: " . ($topGoleadores->first() ? $topGoleadores->first()->apellido . ' (' . $topGoleadores->first()->total_puntos . ' pts)' : 'N/A')); - $this->line("QRs generados: {$qrsSemana} | Validados: {$qrsValidados}"); - return self::SUCCESS; - } - - // 1. Priorizar email configurado en sistema - $emails = []; - $configEmail = \App\Models\Configuracion::get('email_reportes'); - if ($configEmail) { - $emails[] = $configEmail; - } - - // 2. Si no hay, buscar superadmins (rol=1) - if (empty($emails)) { - $emails = DB::table('admin_users')->where('rol', 1)->whereNotNull('email')->pluck('email')->toArray(); - } - - // 3. Fallback: email del .env - if (empty($emails)) { - $fallback = config('mail.from.address'); - if ($fallback) $emails = [$fallback]; - } - - if (empty($emails)) { - $this->error('No hay destinatarios configurados para enviar el reporte.'); - return self::FAILURE; - } - - try { - Mail::send('emails.reporte_semanal', $data, function ($mail) use ($emails, $ahora) { - $mail->to($emails) - ->subject("📊 Reporte Semanal ONAPB — Semana del " . $ahora->startOfWeek()->format('d/m')); - }); - $this->info("✅ Reporte enviado a: " . implode(', ', $emails)); - } catch (\Exception $e) { - Log::error('Error enviando reporte semanal: ' . $e->getMessage()); - $this->error('Error: ' . $e->getMessage()); - return self::FAILURE; - } - - return self::SUCCESS; - } -} diff --git a/app/Http/Controllers/Admin/AdminUserController.php b/app/Http/Controllers/Admin/AdminUserController.php deleted file mode 100644 index 3042a13..0000000 --- a/app/Http/Controllers/Admin/AdminUserController.php +++ /dev/null @@ -1,115 +0,0 @@ -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.'); - } -} diff --git a/app/Http/Controllers/Admin/CarouselItemController.php b/app/Http/Controllers/Admin/CarouselItemController.php deleted file mode 100644 index afecb19..0000000 --- a/app/Http/Controllers/Admin/CarouselItemController.php +++ /dev/null @@ -1,113 +0,0 @@ -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.'); - } -} diff --git a/app/Http/Controllers/Admin/CategoriaController.php b/app/Http/Controllers/Admin/CategoriaController.php deleted file mode 100644 index 66a8ef3..0000000 --- a/app/Http/Controllers/Admin/CategoriaController.php +++ /dev/null @@ -1,74 +0,0 @@ -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.'); - } -} diff --git a/app/Http/Controllers/Admin/FixtureController.php b/app/Http/Controllers/Admin/FixtureController.php deleted file mode 100644 index 7101fa7..0000000 --- a/app/Http/Controllers/Admin/FixtureController.php +++ /dev/null @@ -1,380 +0,0 @@ -fixtureService = $fixtureService; - } - - private function checkSuperAdmin() - { - if (!session('admin_logged_in') || session('admin_role') != 1) { - abort(403, 'Solo Súper Administradores pueden generar fixtures.'); - } - } - - /** - * POST /admin/torneos/{id}/generar-fixture — Preview del fixture - */ - public function preview(Request $request, int $id) - { - $this->checkSuperAdmin(); - - $torneo = Torneo::with('equipos.club')->findOrFail($id); - - $request->validate([ - 'fecha_inicio' => 'required|date|after_or_equal:today', - 'dias_entre_jornadas' => 'required|integer|min:1|max:60', - 'sede_default' => 'nullable|string|max:200', - 'doble_rueda' => 'nullable|boolean', - ]); - - try { - $partidos = $this->fixtureService->generarRoundRobin( - torneo: $torneo, - fechaInicio: $request->input('fecha_inicio'), - diasEntreJornadas: (int) $request->input('dias_entre_jornadas', 7), - sedeDefault: $request->input('sede_default', ''), - dobleRueda: (bool) $request->input('doble_rueda', false), - ); - - // Enriquecer con nombres para la vista - $partidosEnriquecidos = collect($partidos)->map(function ($p) { - $local = \App\Models\Equipo::with('club')->find($p['id_equipo_local']); - $visitante = \App\Models\Equipo::with('club')->find($p['id_equipo_visitante']); - return array_merge($p, [ - 'nombre_local' => ($local->club->nombre ?? '?') . ' (' . ($local->categoria ?? '') . ')', - 'nombre_visitante' => ($visitante->club->nombre ?? '?') . ' (' . ($visitante->categoria ?? '') . ')', - ]); - })->toArray(); - - } catch (\InvalidArgumentException $e) { - return back()->with('admin_error', $e->getMessage()); - } - - $fixtureParams = [ - 'fecha_inicio' => $request->input('fecha_inicio'), - 'dias_entre_jornadas' => $request->input('dias_entre_jornadas', 7), - 'sede_default' => $request->input('sede_default', ''), - 'doble_rueda' => (bool) $request->input('doble_rueda', false), - ]; - - return view('admin.torneos.fixture_preview', compact('torneo', 'partidosEnriquecidos', 'fixtureParams')); - - } - - /** - * POST /admin/torneos/{id}/confirmar-fixture — Persiste el fixture - */ - public function confirmar(Request $request, int $id) - { - $this->checkSuperAdmin(); - - $torneo = Torneo::with('equipos.club')->findOrFail($id); - - $request->validate([ - 'fecha_inicio' => 'required|date', - 'dias_entre_jornadas' => 'required|integer|min:1', - 'sede_default' => 'nullable|string|max:200', - 'doble_rueda' => 'nullable|boolean', - ]); - - try { - $partidos = $this->fixtureService->generarRoundRobin( - torneo: $torneo, - fechaInicio: $request->input('fecha_inicio'), - diasEntreJornadas: (int) $request->input('dias_entre_jornadas', 7), - sedeDefault: $request->input('sede_default', ''), - dobleRueda: (bool) $request->input('doble_rueda', false), - ); - - $creados = $this->fixtureService->persistirFixture($partidos, $torneo); - - } catch (\InvalidArgumentException $e) { - return back()->with('admin_error', $e->getMessage()); - } - - return redirect()->route('admin.torneos.edit', $id) - ->with('admin_msg', "✅ Fixture generado correctamente: {$creados} partidos creados."); - } - - public function importForm(int $id) - { - $this->checkSuperAdmin(); - $torneo = Torneo::findOrFail($id); - return view('admin.torneos.importar', compact('torneo')); - } - - public function importStore(Request $request, int $id) - { - $this->checkSuperAdmin(); - $torneo = Torneo::with(['equipos.club'])->findOrFail($id); - - $request->validate([ - 'texto_importar' => 'required|string', - ]); - - $lineas = explode("\n", $request->input('texto_importar')); - $creados = 0; - $errores = []; - - foreach ($lineas as $index => $linea) { - $linea = trim($linea); - if (empty($linea)) continue; - - try { - // Formato esperado: Fecha, ClubL, CatL, ClubV, CatV, MarcadorL, MarcadorV, Sede, Grupo - $partes = str_getcsv($linea); - - if (count($partes) < 7) { - throw new \Exception("Formato insuficiente. Se esperan al menos 7 columnas."); - } - - $fechaRaw = trim($partes[0]); - $fecha = \Carbon\Carbon::parse(str_replace('/', '-', $fechaRaw))->format('Y-m-d'); - - $nombreClubL = trim($partes[1]); - $catL = trim($partes[2]); - $nombreClubV = trim($partes[3]); - $catV = trim($partes[4]); - $marcadorL = trim($partes[5]); - $marcadorV = trim($partes[6]); - $sede = $partes[7] ?? null; - $grupo = $partes[8] ?? null; - - // Buscar equipos con validación de grupo (prioridad) y categoría - $equipoL = $this->buscarEquipo($nombreClubL, $catL, $torneo, $grupo); - $equipoV = $this->buscarEquipo($nombreClubV, $catV, $torneo, $grupo); - - if (!$equipoL || !$equipoV) { - $missing = !$equipoL ? "'{$nombreClubL} ({$catL})'" : "'{$nombreClubV} ({$catV})'"; - throw new \Exception("No se encontró el equipo {$missing} en el grupo '" . ($grupo ?? 'N/A') . "' ni por categoría."); - } - - \App\Models\Evento::create([ - 'id_evento' => uniqid('ev_imp_'), - 'id_torneo' => $id, - 'fase' => \App\Models\Evento::FASE_REGULAR, - 'fecha_evento' => $fecha, - 'id_equipo_local' => $equipoL->id_equipo, - 'id_equipo_visitante' => $equipoV->id_equipo, - 'marcador_local' => $marcadorL !== '' ? (int)$marcadorL : null, - 'marcador_visitante' => $marcadorV !== '' ? (int)$marcadorV : null, - 'nombre_evento' => $equipoL->club->nombre . ' vs ' . $equipoV->club->nombre, - 'hora_inicio' => '19:00:00', - 'hora_fin' => '21:00:00', - 'sede' => $sede, - 'precio' => 0, - ]); - - $creados++; - } catch (\Exception $e) { - $errores[] = "Línea " . ($index + 1) . ": " . $e->getMessage(); - continue; - } - } - - $msg = "Se importaron {$creados} partidos exitosamente."; - if (count($errores) > 0) { - return back()->with('admin_msg', $msg)->with('admin_error', "Se detectaron errores en " . count($errores) . " líneas:
" . implode("
", $errores)); - } - - return redirect()->route('admin.torneos.edit', $id)->with('admin_msg', $msg); - } - - private function buscarEquipo(string $clubName, string $category, Torneo $torneo, ?string $grupo = null) - { - $clubNameNorm = $this->normalizeString($clubName); - $categoryNorm = $this->normalizeString($category); - $grupoNorm = $grupo ? $this->normalizeString($grupo) : null; - - // PASO 1: Prioridad absoluta al GRUPO (vía pivot) - if ($grupoNorm) { - $match = $torneo->equipos->first(function($e) use ($clubNameNorm, $grupoNorm) { - $eClubName = $this->normalizeString($e->club->nombre ?? ''); - $eGrupo = $this->normalizeString($e->pivot->grupo ?? ''); - - return ($eGrupo === $grupoNorm) && - (strpos($eClubName, $clubNameNorm) !== false || strpos($clubNameNorm, $eClubName) !== false); - }); - if ($match) return $match; - } - - // PASO 2: Fallback a búsqueda por CATEGORÍA si el grupo no coincide o no se especificó - return $torneo->equipos->first(function($e) use ($clubNameNorm, $categoryNorm) { - $eClubName = $this->normalizeString($e->club->nombre ?? ''); - $eCategory = $this->normalizeString($e->categoria ?? ''); - - $matchClub = (strpos($eClubName, $clubNameNorm) !== false || strpos($clubNameNorm, $eClubName) !== false); - $matchCat = (strpos($eCategory, $categoryNorm) !== false || strpos($categoryNorm, $eCategory) !== false); - - return $matchClub && $matchCat; - }); - } - - private function normalizeString(?string $str): string - { - if (!$str) return ''; - $str = mb_strtolower(trim($str), 'UTF-8'); - $str = str_replace( - ['á', 'é', 'í', 'ó', 'ú', 'ü', 'ñ'], - ['a', 'e', 'i', 'o', 'u', 'u', 'n'], - $str - ); - // Eliminar caracteres no alfanuméricos para un matching más flexible - return preg_replace('/[^a-z0-9]/', '', $str); - } - - public function generarPlayoffs(Request $request, int $id) - { - $this->checkSuperAdmin(); - $torneo = Torneo::findOrFail($id); - $grupo = $request->input('grupo'); - $formato = (int) $request->input('formato', 1); // 1, 3, 5 - - if (!$grupo) { - return back()->with('admin_error', 'Debes seleccionar un grupo para generar los playoffs.'); - } - - $ts = new \App\Services\TournamentService(); - $standings = $ts->getStandings($id, true); - - if (!isset($standings[$grupo])) { - return back()->with('admin_error', "No hay equipos en el grupo {$grupo}."); - } - - $top8 = array_slice($standings[$grupo], 0, 8); - - if (count($top8) < 2) { - return back()->with('admin_error', "Se necesitan al menos 2 equipos para generar playoffs."); - } - - // Determinar emparejamientos (1 vs 8, 4 vs 5, 2 vs 7, 3 vs 6) - // Usamos el seeding oficial: 1v8, 4v5, 2v7, 3v6 - $parejas = [ - ['local' => $top8[0]['id'], 'visit' => $top8[7]['id'] ?? null, 'nro' => 1], - ['local' => $top8[3]['id'] ?? null, 'visit' => $top8[4]['id'] ?? null, 'nro' => 2], - ['local' => $top8[1]['id'] ?? null, 'visit' => $top8[6]['id'] ?? null, 'nro' => 3], - ['local' => $top8[2]['id'] ?? null, 'visit' => $top8[5]['id'] ?? null, 'nro' => 4], - ]; - - $count = 0; - foreach ($parejas as $p) { - if (!$p['local'] || !$p['visit']) continue; - - $local = \App\Models\Equipo::with('club')->find($p['local']); - $visit = \App\Models\Equipo::with('club')->find($p['visit']); - - // Crear N partidos según el formato - for ($i = 0; $i < $formato; $i++) { - \App\Models\Evento::create([ - 'id_evento' => uniqid('ply_'), - 'id_torneo' => $id, - 'fase' => \App\Models\Evento::FASE_CUARTOS, - 'numero_partido_bracket' => $p['nro'], - 'id_equipo_local' => $p['local'], - 'id_equipo_visitante' => $p['visit'], - 'nombre_evento' => "4tos (J".($i+1)."): " . $local->club->nombre . " vs " . $visit->club->nombre . " ({$grupo})", - 'fecha_evento' => now()->addDays(7 + ($i * 3))->format('Y-m-d'), - 'hora_inicio' => '20:00:00', - 'hora_fin' => '22:00:00', - 'precio' => 0, - ]); - $count++; - } - } - - return redirect()->route('admin.torneos.playoffs.manage', $id) - ->with('admin_msg', "Playoffs generados: {$count} partidos creados para el grupo {$grupo}."); - } - - public function managePlayoffs(int $id) - { - $this->checkSuperAdmin(); - $torneo = Torneo::findOrFail($id); - - $ts = new \App\Services\TournamentService(); - $bracket = $ts->getPlayoffBrackets($id); - - $grupos = \DB::table('torneo_equipo')->where('id_torneo', $id)->distinct()->pluck('grupo')->filter(); - - return view('admin.torneos.playoff_manage', compact('torneo', 'bracket', 'grupos')); - } - - public function avanzarGanador(Request $request, int $id) - { - $this->checkSuperAdmin(); - $faseActual = (int) $request->input('fase'); - $nroBracket = (int) $request->input('nro_bracket'); - $idGanador = (int) $request->input('id_ganador'); - - $torneo = Torneo::findOrFail($id); - $ganador = \App\Models\Equipo::with('club')->findOrFail($idGanador); - - // Mapeo de avance - // Cuartos 1 & 2 -> Semis 1 - // Cuartos 3 & 4 -> Semis 2 - // Semis 1 & 2 -> Final 1 - $mapping = [ - \App\Models\Evento::FASE_CUARTOS => [ - 1 => ['next_fase' => \App\Models\Evento::FASE_SEMIS, 'next_nro' => 1, 'side' => 'local'], - 2 => ['next_fase' => \App\Models\Evento::FASE_SEMIS, 'next_nro' => 1, 'side' => 'visitante'], - 3 => ['next_fase' => \App\Models\Evento::FASE_SEMIS, 'next_nro' => 2, 'side' => 'local'], - 4 => ['next_fase' => \App\Models\Evento::FASE_SEMIS, 'next_nro' => 2, 'side' => 'visitante'], - ], - \App\Models\Evento::FASE_SEMIS => [ - 1 => ['next_fase' => \App\Models\Evento::FASE_FINAL, 'next_nro' => 1, 'side' => 'local'], - 2 => ['next_fase' => \App\Models\Evento::FASE_FINAL, 'next_nro' => 1, 'side' => 'visitante'], - ] - ]; - - if (!isset($mapping[$faseActual][$nroBracket])) { - return back()->with('admin_error', 'No se puede avanzar desde esta fase.'); - } - - $next = $mapping[$faseActual][$nroBracket]; - - // Buscar si ya existe el partido en la siguiente fase - $eventoNext = \App\Models\Evento::where('id_torneo', $id) - ->where('fase', $next['next_fase']) - ->where('numero_partido_bracket', $next['next_nro']) - ->first(); - - if ($eventoNext) { - if ($next['side'] == 'local') { - $eventoNext->id_equipo_local = $idGanador; - } else { - $eventoNext->id_equipo_visitante = $idGanador; - } - $eventoNext->nombre_evento = ($eventoNext->equipoLocal->club->nombre ?? 'TBD') . ' vs ' . ($eventoNext->equipoVisitante->club->nombre ?? 'TBD'); - $eventoNext->save(); - } else { - // Crear el primer partido de la serie (por defecto 1 partido, el admin puede agregar más) - \App\Models\Evento::create([ - 'id_evento' => uniqid('ply_adv_'), - 'id_torneo' => $id, - 'fase' => $next['next_fase'], - 'numero_partido_bracket' => $next['next_nro'], - 'id_equipo_local' => $next['side'] == 'local' ? $idGanador : null, - 'id_equipo_visitante' => $next['side'] == 'visitante' ? $idGanador : null, - 'nombre_evento' => $next['side'] == 'local' ? ($ganador->club->nombre . " vs TBD") : ("TBD vs " . $ganador->club->nombre), - 'fecha_evento' => now()->addDays(14)->format('Y-m-d'), - 'hora_inicio' => '20:00:00', - 'hora_fin' => '22:00:00', - 'precio' => 0, - ]); - } - - return back()->with('admin_msg', "✅ Equipo {$ganador->club->nombre} avanzado a la siguiente ronda."); - } -} diff --git a/app/Http/Controllers/Admin/PaseController.php b/app/Http/Controllers/Admin/PaseController.php deleted file mode 100644 index e05d4c5..0000000 --- a/app/Http/Controllers/Admin/PaseController.php +++ /dev/null @@ -1,129 +0,0 @@ -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.'); - } -} diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php deleted file mode 100644 index 10de9e6..0000000 --- a/app/Http/Controllers/AdminController.php +++ /dev/null @@ -1,1667 +0,0 @@ -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.'); - } -} diff --git a/app/Http/Controllers/AdminUserController.php b/app/Http/Controllers/AdminUserController.php deleted file mode 100644 index 8cf1ca3..0000000 --- a/app/Http/Controllers/AdminUserController.php +++ /dev/null @@ -1,55 +0,0 @@ -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); - } -} diff --git a/app/Http/Controllers/AficionadoController.php b/app/Http/Controllers/AficionadoController.php deleted file mode 100644 index f3eadd7..0000000 --- a/app/Http/Controllers/AficionadoController.php +++ /dev/null @@ -1,67 +0,0 @@ -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); - } -} diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php deleted file mode 100644 index a801d3a..0000000 --- a/app/Http/Controllers/AuthController.php +++ /dev/null @@ -1,411 +0,0 @@ -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; - } -} diff --git a/app/Http/Controllers/ClubController.php b/app/Http/Controllers/ClubController.php deleted file mode 100644 index 9ad2eb8..0000000 --- a/app/Http/Controllers/ClubController.php +++ /dev/null @@ -1,47 +0,0 @@ -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); - } -} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php deleted file mode 100644 index 8677cd5..0000000 --- a/app/Http/Controllers/Controller.php +++ /dev/null @@ -1,8 +0,0 @@ -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' => '', - 'cap2' => '', - 'cap3' => '', - 'cap4' => '', - 'cap5' => '', - '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); - } -} diff --git a/app/Http/Controllers/EquipoController.php b/app/Http/Controllers/EquipoController.php deleted file mode 100644 index acbb688..0000000 --- a/app/Http/Controllers/EquipoController.php +++ /dev/null @@ -1,57 +0,0 @@ -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); - } -} diff --git a/app/Http/Controllers/EventoController.php b/app/Http/Controllers/EventoController.php deleted file mode 100644 index 826cd21..0000000 --- a/app/Http/Controllers/EventoController.php +++ /dev/null @@ -1,84 +0,0 @@ -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'; - } -} diff --git a/app/Http/Controllers/GeniusAgentController.php b/app/Http/Controllers/GeniusAgentController.php deleted file mode 100644 index 7de4886..0000000 --- a/app/Http/Controllers/GeniusAgentController.php +++ /dev/null @@ -1,81 +0,0 @@ -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]); - } -} diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php deleted file mode 100644 index e32bb4c..0000000 --- a/app/Http/Controllers/HomeController.php +++ /dev/null @@ -1,86 +0,0 @@ -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'; - } -} diff --git a/app/Http/Controllers/JugadorController.php b/app/Http/Controllers/JugadorController.php deleted file mode 100644 index 1867573..0000000 --- a/app/Http/Controllers/JugadorController.php +++ /dev/null @@ -1,76 +0,0 @@ -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); - } -} diff --git a/app/Http/Controllers/JugadorEquipoController.php b/app/Http/Controllers/JugadorEquipoController.php deleted file mode 100644 index 60e9d3a..0000000 --- a/app/Http/Controllers/JugadorEquipoController.php +++ /dev/null @@ -1,73 +0,0 @@ -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); - } -} diff --git a/app/Http/Controllers/NoticiaController.php b/app/Http/Controllers/NoticiaController.php deleted file mode 100644 index d2e017c..0000000 --- a/app/Http/Controllers/NoticiaController.php +++ /dev/null @@ -1,61 +0,0 @@ -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); - } -} diff --git a/app/Http/Controllers/NotificacionController.php b/app/Http/Controllers/NotificacionController.php deleted file mode 100644 index eae56a9..0000000 --- a/app/Http/Controllers/NotificacionController.php +++ /dev/null @@ -1,143 +0,0 @@ -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]); - } -} diff --git a/app/Http/Controllers/PanelController.php b/app/Http/Controllers/PanelController.php deleted file mode 100644 index c92dd3a..0000000 --- a/app/Http/Controllers/PanelController.php +++ /dev/null @@ -1,363 +0,0 @@ -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')); - } -} diff --git a/app/Http/Controllers/PromoQrController.php b/app/Http/Controllers/PromoQrController.php deleted file mode 100644 index 4f424c7..0000000 --- a/app/Http/Controllers/PromoQrController.php +++ /dev/null @@ -1,52 +0,0 @@ -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); - } -} diff --git a/app/Http/Controllers/PromocionController.php b/app/Http/Controllers/PromocionController.php deleted file mode 100644 index 9d97dfe..0000000 --- a/app/Http/Controllers/PromocionController.php +++ /dev/null @@ -1,70 +0,0 @@ -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); - } -} diff --git a/app/Http/Controllers/QrCodeController.php b/app/Http/Controllers/QrCodeController.php deleted file mode 100644 index 559b56b..0000000 --- a/app/Http/Controllers/QrCodeController.php +++ /dev/null @@ -1,57 +0,0 @@ -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); - } -} diff --git a/app/Http/Controllers/QrDownloadController.php b/app/Http/Controllers/QrDownloadController.php deleted file mode 100644 index c606bd7..0000000 --- a/app/Http/Controllers/QrDownloadController.php +++ /dev/null @@ -1,129 +0,0 @@ -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"); - } -} diff --git a/app/Http/Controllers/SeguimientoController.php b/app/Http/Controllers/SeguimientoController.php deleted file mode 100644 index 7d7fb98..0000000 --- a/app/Http/Controllers/SeguimientoController.php +++ /dev/null @@ -1,122 +0,0 @@ -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]); - } -} diff --git a/app/Http/Controllers/TorneoController.php b/app/Http/Controllers/TorneoController.php deleted file mode 100644 index d96bd0d..0000000 --- a/app/Http/Controllers/TorneoController.php +++ /dev/null @@ -1,85 +0,0 @@ -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')); - } -} diff --git a/app/Http/Middleware/SecurityHeaders.php b/app/Http/Middleware/SecurityHeaders.php deleted file mode 100644 index 440135b..0000000 --- a/app/Http/Middleware/SecurityHeaders.php +++ /dev/null @@ -1,40 +0,0 @@ -headers->set('X-Frame-Options', 'SAMEORIGIN'); - $response->headers->set('X-Content-Type-Options', 'nosniff'); - $response->headers->set('X-XSS-Protection', '1; mode=block'); - $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin'); - $response->headers->set( - 'Permissions-Policy', - 'camera=(), microphone=(), geolocation=()' - ); - $response->headers->set( - 'Strict-Transport-Security', - 'max-age=31536000; includeSubDomains; preload' - ); - $response->headers->set( - 'Content-Security-Policy', - "default-src 'self'; " . - "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://challenges.cloudflare.com; " . - "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " . - "font-src 'self' data: https://fonts.gstatic.com; " . - "img-src 'self' data: https:; " . - "connect-src 'self' https://challenges.cloudflare.com; " . - "frame-src 'self' https://challenges.cloudflare.com; " . - "frame-ancestors 'self';" - ); - - return $response; - } -} diff --git a/app/Mail/QrCodeMail.php b/app/Mail/QrCodeMail.php deleted file mode 100644 index d10222a..0000000 --- a/app/Mail/QrCodeMail.php +++ /dev/null @@ -1,29 +0,0 @@ -user = $user; - $this->evento = $evento; - $this->cantidad = $cantidad; - } - - public function build() - { - return $this->subject('Tus QRs para el evento: ' . $this->evento->nombre_evento) - ->view('emails.qrcodes'); - } -} diff --git a/app/Mail/ResetPasswordMail.php b/app/Mail/ResetPasswordMail.php deleted file mode 100644 index 4ca8203..0000000 --- a/app/Mail/ResetPasswordMail.php +++ /dev/null @@ -1,27 +0,0 @@ -user = $user; - $this->token = $token; - } - - public function build() - { - return $this->subject('Recuperar contraseña - OnAPB') - ->view('emails.reset_password'); - } -} diff --git a/app/Mail/WelcomeMail.php b/app/Mail/WelcomeMail.php deleted file mode 100644 index f328ca4..0000000 --- a/app/Mail/WelcomeMail.php +++ /dev/null @@ -1,27 +0,0 @@ -user = $user; - $this->tipo = $tipo; - } - - public function build() - { - return $this->subject('¡Bienvenido a OnAPB!') - ->view('emails.welcome'); - } -} diff --git a/app/Models/AdminUser.php b/app/Models/AdminUser.php deleted file mode 100644 index 0113782..0000000 --- a/app/Models/AdminUser.php +++ /dev/null @@ -1,36 +0,0 @@ -belongsTo(Club::class, 'id_club', 'id_club'); - } - - protected $hidden = [ - 'password', - 'reset_token', - ]; - - protected $casts = [ - 'role' => 'integer', - 'reset_expira' => 'datetime', - ]; -} diff --git a/app/Models/Aficionado.php b/app/Models/Aficionado.php deleted file mode 100644 index 003a298..0000000 --- a/app/Models/Aficionado.php +++ /dev/null @@ -1,38 +0,0 @@ - 'integer', - 'fecha_nacimiento' => 'date', - 'fecha_registro' => 'datetime', - 'reset_expira' => 'datetime', - ]; -} diff --git a/app/Models/AgentThread.php b/app/Models/AgentThread.php deleted file mode 100644 index 16b55cc..0000000 --- a/app/Models/AgentThread.php +++ /dev/null @@ -1,42 +0,0 @@ - 'array', - 'expires_at' => 'datetime', - ]; - - public static function findOrCreateForAdmin(?string $threadId, int $adminId): static - { - if ($threadId) { - $thread = static::where('thread_id', $threadId) - ->where('admin_id', $adminId) - ->first(); - if ($thread) { - return $thread; - } - } - - return static::create([ - 'thread_id' => (string) Str::uuid(), - 'admin_id' => $adminId, - 'messages' => [], - 'expires_at' => now()->addDays(30), - ]); - } -} diff --git a/app/Models/CarouselItem.php b/app/Models/CarouselItem.php deleted file mode 100644 index b2a2f7e..0000000 --- a/app/Models/CarouselItem.php +++ /dev/null @@ -1,25 +0,0 @@ - 'boolean', - 'orden' => 'integer', - ]; -} diff --git a/app/Models/Categoria.php b/app/Models/Categoria.php deleted file mode 100644 index 9d9a672..0000000 --- a/app/Models/Categoria.php +++ /dev/null @@ -1,19 +0,0 @@ -id_club)) { - $model->id_club = (int) self::withTrashed()->max('id_club') + 1; - } - }); - } - - protected $casts = [ - 'id_club' => 'integer', - 'es_seleccion' => 'boolean', - ]; - - public function equipos() - { - return $this->hasMany(Equipo::class, 'id_club', 'id_club'); - } - - public function jugadores() - { - return $this->hasMany(Jugador::class, 'id_club_actual', 'id_club'); - } -} diff --git a/app/Models/Configuracion.php b/app/Models/Configuracion.php deleted file mode 100644 index 5c77e24..0000000 --- a/app/Models/Configuracion.php +++ /dev/null @@ -1,45 +0,0 @@ -first(); - return $config ? $config->valor : $default; - } - - /** - * Establecer o actualizar un valor de configuración. - * - * @param string $clave - * @param mixed $valor - * @param string|null $descripcion - * @return self - */ - public static function set($clave, $valor, $descripcion = null) - { - return self::updateOrCreate( - ['clave' => $clave], - ['valor' => $valor, 'descripcion' => $descripcion] - ); - } -} diff --git a/app/Models/Equipo.php b/app/Models/Equipo.php deleted file mode 100644 index bb0d9c2..0000000 --- a/app/Models/Equipo.php +++ /dev/null @@ -1,41 +0,0 @@ - 'integer', - 'id_club' => 'integer', - ]; - - public function club() - { - return $this->belongsTo(Club::class, 'id_club', 'id_club'); - } - - public function jugadores() - { - return $this->belongsToMany(Jugador::class, 'jugador_equipo', 'id_equipo', 'id_jugador') - ->withPivot('fecha_alta'); - } - - public function torneos() - { - return $this->belongsToMany(Torneo::class, 'torneo_equipo', 'id_equipo', 'id_torneo'); - } -} diff --git a/app/Models/EquipoSeguimiento.php b/app/Models/EquipoSeguimiento.php deleted file mode 100644 index 79e1dc4..0000000 --- a/app/Models/EquipoSeguimiento.php +++ /dev/null @@ -1,28 +0,0 @@ - 'integer', - 'created_at' => 'datetime', - ]; - - public function equipo() - { - return $this->belongsTo(Equipo::class, 'id_equipo', 'id_equipo'); - } -} diff --git a/app/Models/Evento.php b/app/Models/Evento.php deleted file mode 100644 index 85c24b0..0000000 --- a/app/Models/Evento.php +++ /dev/null @@ -1,96 +0,0 @@ - 'string', - 'id_torneo' => 'integer', - 'id_equipo_local' => 'integer', - 'id_equipo_visitante' => 'integer', - 'marcador_local' => 'integer', - 'marcador_visitante' => 'integer', - 'precio' => 'decimal:2', - 'fase' => 'integer', - 'numero_partido_bracket' => 'integer', - ]; - - public function getFechaEventoAttribute($value) - { - return $value ? Carbon::parse($value) : null; - } - - public function getHoraInicioAttribute($value) - { - return $value ? Carbon::parse($value) : null; - } - - public function getHoraFinAttribute($value) - { - return $value ? Carbon::parse($value) : null; - } - - public function torneo() - { - return $this->belongsTo(Torneo::class, 'id_torneo'); - } - - public function equipoLocal() - { - return $this->belongsTo(Equipo::class, 'id_equipo_local', 'id_equipo'); - } - - public function equipoVisitante() - { - return $this->belongsTo(Equipo::class, 'id_equipo_visitante', 'id_equipo'); - } - - public function pagos() - { - return $this->hasMany(PagoMp::class, 'event_id', 'id_evento'); - } - - public function qrCodes() - { - return $this->hasMany(QrCode::class, 'id_evento', 'id_evento'); - } - - public function jugadoresPuntos() - { - return $this->hasMany(EventoJugador::class, 'id_evento', 'id_evento'); - } -} diff --git a/app/Models/EventoJugador.php b/app/Models/EventoJugador.php deleted file mode 100644 index 18dd668..0000000 --- a/app/Models/EventoJugador.php +++ /dev/null @@ -1,27 +0,0 @@ -belongsTo(Evento::class, 'id_evento', 'id_evento'); - } - - public function jugador() - { - return $this->belongsTo(Jugador::class, 'id_jugador', 'id_jugador'); - } -} diff --git a/app/Models/Jugador.php b/app/Models/Jugador.php deleted file mode 100644 index 8bd86a8..0000000 --- a/app/Models/Jugador.php +++ /dev/null @@ -1,79 +0,0 @@ - 'string', - 'fecha_nacimiento' => 'date', - 'edad' => 'integer', - 'id_club_actual' => 'integer', - 'id_club_origen' => 'integer', - 'activo' => 'boolean', - 'reset_expira' => 'datetime', - ]; - - public function getCategoriaCalculadaAttribute() - { - if (!$this->fecha_nacimiento) return 'Sin categoría'; - - // Calculate age for the current year. (Categoría U15 is for players turning 14 and 15 in the current year). - // That means current_year - birth_year - $edadCategoria = date('Y') - $this->fecha_nacimiento->format('Y'); - - $categoria = Categoria::where('edad_min', '<=', $edadCategoria) - ->where('edad_max', '>=', $edadCategoria) - ->first(); - - return $categoria ? $categoria->nombre : 'Sin categoría'; - } - - public function clubActual() - { - return $this->belongsTo(Club::class, 'id_club_actual', 'id_club'); - } - - public function clubOrigen() - { - return $this->belongsTo(Club::class, 'id_club_origen', 'id_club'); - } - - public function equipos() - { - return $this->belongsToMany(Equipo::class, 'jugador_equipo', 'id_jugador', 'id_equipo') - ->withPivot('fecha_alta'); - } -} diff --git a/app/Models/JugadorEquipo.php b/app/Models/JugadorEquipo.php deleted file mode 100644 index 2efefa6..0000000 --- a/app/Models/JugadorEquipo.php +++ /dev/null @@ -1,34 +0,0 @@ - 'string', - 'id_equipo' => 'integer', - 'fecha_alta' => 'date', - ]; - - public function jugador() - { - return $this->belongsTo(Jugador::class, 'id_jugador', 'id_jugador'); - } - - public function equipo() - { - return $this->belongsTo(Equipo::class, 'id_equipo', 'id_equipo'); - } -} diff --git a/app/Models/Noticia.php b/app/Models/Noticia.php deleted file mode 100644 index f275db4..0000000 --- a/app/Models/Noticia.php +++ /dev/null @@ -1,26 +0,0 @@ - 'integer', - 'fecha' => 'datetime', - ]; -} diff --git a/app/Models/Notificacion.php b/app/Models/Notificacion.php deleted file mode 100644 index 1dcb753..0000000 --- a/app/Models/Notificacion.php +++ /dev/null @@ -1,40 +0,0 @@ - 'boolean', - 'enviada_email' => 'boolean', - 'creada_en' => 'datetime', - ]; - - // ── Scopes ── - public function scopeNoLeidas($query) - { - return $query->where('leida', false); - } - - public function scopeParaUsuario($query, string $tipo, $id) - { - return $query->where('tipo_destinatario', $tipo)->where('id_destinatario', (string)$id); - } -} diff --git a/app/Models/Pase.php b/app/Models/Pase.php deleted file mode 100644 index 0f0283e..0000000 --- a/app/Models/Pase.php +++ /dev/null @@ -1,33 +0,0 @@ -belongsTo(Jugador::class, 'id_jugador', 'id_jugador'); - } - - public function clubOrigen() - { - return $this->belongsTo(Club::class, 'id_club_origen', 'id_club'); - } - - public function clubDestino() - { - return $this->belongsTo(Club::class, 'id_club_destino', 'id_club'); - } -} diff --git a/app/Models/PromoQr.php b/app/Models/PromoQr.php deleted file mode 100644 index e3287a7..0000000 --- a/app/Models/PromoQr.php +++ /dev/null @@ -1,46 +0,0 @@ - 'string', - 'id_promo' => 'integer', - 'id_usuario' => 'integer', - 'tipo_usuario' => 'string', - 'generado_en' => 'datetime', - 'usado' => 'boolean', - 'usado_en' => 'datetime', - ]; - - public function promocion() - { - return $this->belongsTo(Promocion::class, 'id_promo', 'id'); - } - - public function usuario() - { - if ($this->tipo_usuario === 'jugador') { - return $this->belongsTo(Jugador::class, 'id_usuario', 'id_jugador'); - } - return $this->belongsTo(Aficionado::class, 'id_usuario', 'id_aficionado'); - } -} diff --git a/app/Models/Promocion.php b/app/Models/Promocion.php deleted file mode 100644 index 3896198..0000000 --- a/app/Models/Promocion.php +++ /dev/null @@ -1,35 +0,0 @@ - 'integer', - 'lat' => 'decimal:8', - 'lng' => 'decimal:8', - ]; - - public function promoQrs() - { - return $this->hasMany(PromoQr::class, 'id_promo', 'id'); - } -} diff --git a/app/Models/PushSubscription.php b/app/Models/PushSubscription.php deleted file mode 100644 index c137170..0000000 --- a/app/Models/PushSubscription.php +++ /dev/null @@ -1,24 +0,0 @@ -where('tipo_usuario', $tipo)->where('id_usuario', $id); - } -} diff --git a/app/Models/QrCode.php b/app/Models/QrCode.php deleted file mode 100644 index 484396a..0000000 --- a/app/Models/QrCode.php +++ /dev/null @@ -1,48 +0,0 @@ - 'string', - 'id_evento' => 'string', - 'id_jugador' => 'string', - 'tipo_qr' => 'string', - 'escaneos_restantes' => 'integer', - 'creado' => 'datetime', - 'id_aficionado' => 'integer', - ]; - - public function evento() - { - return $this->belongsTo(Evento::class, 'id_evento', 'id_evento'); - } - - public function jugador() - { - return $this->belongsTo(Jugador::class, 'id_jugador', 'id_jugador'); - } - - public function aficionado() - { - return $this->belongsTo(Aficionado::class, 'id_aficionado', 'id_aficionado'); - } -} diff --git a/app/Models/Sponsor.php b/app/Models/Sponsor.php deleted file mode 100644 index 2fb3743..0000000 --- a/app/Models/Sponsor.php +++ /dev/null @@ -1,23 +0,0 @@ - 'boolean', - ]; -} diff --git a/app/Models/Torneo.php b/app/Models/Torneo.php deleted file mode 100644 index 6447d9d..0000000 --- a/app/Models/Torneo.php +++ /dev/null @@ -1,33 +0,0 @@ - 'datetime', - 'fecha_fin' => 'datetime', - ]; - - public function equipos() - { - return $this->belongsToMany(Equipo::class, 'torneo_equipo', 'id_torneo', 'id_equipo')->withPivot('grupo'); - } - - public function eventos() - { - return $this->hasMany(Evento::class, 'id_torneo'); - } -} diff --git a/app/Models/User.php b/app/Models/User.php deleted file mode 100644 index 68f3a66..0000000 --- a/app/Models/User.php +++ /dev/null @@ -1,49 +0,0 @@ - */ - use HasFactory, Notifiable; - - /** - * The attributes that are mass assignable. - * - * @var list - */ - protected $fillable = [ - 'name', - 'email', - 'password', - ]; - - /** - * The attributes that should be hidden for serialization. - * - * @var list - */ - protected $hidden = [ - 'password', - 'remember_token', - ]; - - /** - * Get the attributes that should be cast. - * - * @return array - */ - protected function casts(): array - { - return [ - 'email_verified_at' => 'datetime', - 'password' => 'hashed', - ]; - } -} diff --git a/app/Observers/EventoObserver.php b/app/Observers/EventoObserver.php deleted file mode 100644 index e28c8d3..0000000 --- a/app/Observers/EventoObserver.php +++ /dev/null @@ -1,142 +0,0 @@ -notifService = $notifService; - } - - /** - * Al CREAR un partido: notificar a todos los seguidores de ambos equipos. - */ - public function created(Evento $evento): void - { - if (!$evento->id_equipo_local || !$evento->id_equipo_visitante) return; - - $evento->load(['equipoLocal.club', 'equipoVisitante.club']); - - $nombreLocal = $evento->equipoLocal->club->nombre ?? '?'; - $nombreVisitante= $evento->equipoVisitante->club->nombre ?? '?'; - $fechaStr = $evento->fecha_evento ? $evento->fecha_evento->format('d/m/Y') : '—'; - $horaStr = $evento->hora_inicio ? \Carbon\Carbon::parse($evento->hora_inicio)->format('H:i') : ''; - $sedeStr = $evento->sede ? " en {$evento->sede}" : ''; - - $titulo = "🏀 Nuevo Partido Programado"; - $mensaje = "{$nombreLocal} vs {$nombreVisitante} — {$fechaStr}" . ($horaStr ? " a las {$horaStr}" : '') . $sedeStr . '.'; - $url = '/eventos/' . $evento->id_evento; - - $this->notificarSeguidoresDeEquipos( - [$evento->id_equipo_local, $evento->id_equipo_visitante], - 'partido', - $titulo, - $mensaje, - $url - ); - } - - /** - * Al ACTUALIZAR un partido: si cambia el marcador, notificar resultado. - */ - public function updated(Evento $evento): void - { - $dirty = $evento->getDirty(); - - // Solo disparar si se actualizó el marcador - if (!array_key_exists('marcador_local', $dirty) && !array_key_exists('marcador_visitante', $dirty)) { - return; - } - - if ($evento->marcador_local === null || $evento->marcador_visitante === null) return; - - // Validación Horaria (Buenos Aires) - $tz = 'America/Argentina/Buenos_Aires'; - $ahora = \Carbon\Carbon::now($tz); - - // El usuario solicita que se emita cuando fin > real - $fecha = $evento->fecha_evento instanceof \Carbon\Carbon ? $evento->fecha_evento->format('Y-m-d') : substr($evento->fecha_evento, 0, 10); - $horaFin = $evento->hora_fin instanceof \Carbon\Carbon ? $evento->hora_fin->format('H:i:s') : $evento->hora_fin; - $fin = \Carbon\Carbon::createFromFormat('Y-m-d H:i:s', "{$fecha} {$horaFin}", $tz); - - if (!($fin->gt($ahora))) { - return; - } - - $evento->load(['equipoLocal.club', 'equipoVisitante.club']); - - $nombreLocal = $evento->equipoLocal->club->nombre ?? '?'; - $nombreVisitante = $evento->equipoVisitante->club->nombre ?? '?'; - $mloc = $evento->marcador_local; - $mvis = $evento->marcador_visitante; - - if ($mloc > $mvis) { - $resultado = "🏆 Ganó {$nombreLocal}"; - } elseif ($mvis > $mloc) { - $resultado = "🏆 Ganó {$nombreVisitante}"; - } else { - $resultado = "🤝 Empate"; - } - - $titulo = "Resultado: {$nombreLocal} {$mloc} - {$mvis} {$nombreVisitante}"; - $mensaje = "{$resultado}. Partido finalizado."; - $url = '/eventos/' . $evento->id_evento; - - $this->notificarSeguidoresDeEquipos( - [$evento->id_equipo_local, $evento->id_equipo_visitante], - 'resultado', - $titulo, - $mensaje, - $url - ); - } - - /** - * Obtiene todos los seguidores de una lista de equipos y envía notificaciones. - * También incluye a los jugadores de esos equipos automáticamente. - */ - private function notificarSeguidoresDeEquipos(array $idEquipos, string $tipo, string $titulo, string $mensaje, ?string $url): void - { - $idEquipos = array_filter($idEquipos); - if (empty($idEquipos)) return; - - $destinatarios = []; - $yaAgregados = []; - - // Seguidores registrados en equipo_seguimiento - $seguimientos = EquipoSeguimiento::whereIn('id_equipo', $idEquipos)->get(); - foreach ($seguimientos as $s) { - $key = $s->tipo_usuario . ':' . $s->id_usuario; - if (!isset($yaAgregados[$key])) { - $destinatarios[] = ['tipo' => $s->tipo_usuario, 'id' => $s->id_usuario]; - $yaAgregados[$key] = true; - } - } - - // Jugadores que pertenecen a estos equipos (siguen automáticamente) - $jugadores = \DB::table('jugador_equipo') - ->whereIn('id_equipo', $idEquipos) - ->pluck('id_jugador'); - - foreach ($jugadores as $idJ) { - $key = 'jugador:' . $idJ; - if (!isset($yaAgregados[$key])) { - $destinatarios[] = ['tipo' => 'jugador', 'id' => $idJ]; - $yaAgregados[$key] = true; - } - } - - if (!empty($destinatarios)) { - $this->notifService->enviarMasivo($destinatarios, $tipo, $titulo, $mensaje, $url); - } - } -} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php deleted file mode 100644 index f9857ed..0000000 --- a/app/Providers/AppServiceProvider.php +++ /dev/null @@ -1,57 +0,0 @@ -app->singleton(NotificacionService::class); - } - - public function boot(): void - { - \Illuminate\Pagination\Paginator::useBootstrapFive(); - - // Registrar observer del modelo Evento - Evento::observe(EventoObserver::class); - - view()->composer('layouts.app', function ($view) { - try { - if (\Illuminate\Support\Facades\Schema::hasTable('sponsors')) { - $view->with('footerSponsors', \App\Models\Sponsor::where('activo', true)->orderBy('orden')->get()); - } else { - $view->with('footerSponsors', collect()); - } - - if (\Illuminate\Support\Facades\Schema::hasTable('torneos')) { - $view->with('navTorneos', \App\Models\Torneo::orderByDesc('fecha_inicio')->take(5)->get()); - } else { - $view->with('navTorneos', collect()); - } - - // Badge de notificaciones para el layout - if (session()->has('user_logged_in') && \Illuminate\Support\Facades\Schema::hasTable('notificaciones')) { - $service = app(NotificacionService::class); - $view->with('notifCount', $service->contarNoLeidas(session('user_tipo'), session('user_id'))); - } else { - $view->with('notifCount', 0); - } - - } catch (\Exception $e) { - $view->with('footerSponsors', collect()); - $view->with('navTorneos', collect()); - $view->with('notifCount', 0); - } - }); - } -} - diff --git a/app/Services/FixtureService.php b/app/Services/FixtureService.php deleted file mode 100644 index 5e0b581..0000000 --- a/app/Services/FixtureService.php +++ /dev/null @@ -1,138 +0,0 @@ -equipos()->get()->values()->toArray(); - $n = count($equipos); - - if ($n < 2) { - throw new \InvalidArgumentException('Se necesitan al menos 2 equipos para generar un fixture.'); - } - - // Si número impar, agregar BYE - $hayBye = false; - if ($n % 2 !== 0) { - $equipos[] = null; // BYE - $n++; - $hayBye = true; - } - - $mitad = $n / 2; - $jornadas = $n - 1; - $partidos = []; - $fechaActual = Carbon::parse($fechaInicio); - - // Algoritmo Round-Robin: fijar el primer equipo, rotar el resto - $lista = range(0, $n - 1); - - for ($jornada = 0; $jornada < $jornadas; $jornada++) { - $jornadaPartidos = []; - - for ($i = 0; $i < $mitad; $i++) { - $localIdx = $lista[$i]; - $visitanteIdx= $lista[$n - 1 - $i]; - - // Si alguno es BYE (null), saltar - if ($equipos[$localIdx] === null || $equipos[$visitanteIdx] === null) continue; - - $local = $equipos[$localIdx]; - $visitante = $equipos[$visitanteIdx]; - - // Alternar localía: en jornadas pares, invertir - if ($jornada % 2 === 1) { - [$local, $visitante] = [$visitante, $local]; - } - - $jornadaPartidos[] = [ - 'id_equipo_local' => $local['id_equipo'], - 'id_equipo_visitante' => $visitante['id_equipo'], - 'fecha_evento' => $fechaActual->format('Y-m-d'), - 'hora_inicio' => '20:00:00', - 'hora_fin' => '22:00:00', - 'sede' => $sedeDefault, - 'id_torneo' => $torneo->id, - 'nombre_evento' => null, // se genera automáticamente - 'precio' => 0, - 'jornada' => $jornada + 1, - ]; - } - - $partidos = array_merge($partidos, $jornadaPartidos); - $fechaActual->addDays($diasEntreJornadas); - - // Rotar la lista (el primer elemento fijo) - $rotatable = array_slice($lista, 1); - array_push($rotatable, array_shift($rotatable)); - $lista = array_merge([$lista[0]], $rotatable); - } - - // Doble rueda: agregar vuelta con localías invertidas - if ($dobleRueda && !empty($partidos)) { - $vuelta = []; - foreach ($partidos as $p) { - $vuelta[] = array_merge($p, [ - 'id_equipo_local' => $p['id_equipo_visitante'], - 'id_equipo_visitante' => $p['id_equipo_local'], - 'fecha_evento' => Carbon::parse($p['fecha_evento'])->addDays($jornadas * $diasEntreJornadas)->format('Y-m-d'), - 'jornada' => $p['jornada'] + $jornadas, - ]); - } - $partidos = array_merge($partidos, $vuelta); - } - - return $partidos; - } - - /** - * Persiste el fixture generado como Evento records en la DB. - */ - public function persistirFixture(array $partidos, Torneo $torneo): int - { - $contador = 0; - foreach ($partidos as $p) { - $local = Equipo::with('club')->find($p['id_equipo_local']); - $visitante = Equipo::with('club')->find($p['id_equipo_visitante']); - - $nombreEvento = ($local->club->nombre ?? '?') . ' vs ' . ($visitante->club->nombre ?? '?'); - - \App\Models\Evento::create([ - 'id_evento' => uniqid('ev_'), - 'nombre_evento' => $nombreEvento, - 'id_equipo_local' => $p['id_equipo_local'], - 'id_equipo_visitante' => $p['id_equipo_visitante'], - 'fecha_evento' => $p['fecha_evento'], - 'hora_inicio' => $p['hora_inicio'], - 'hora_fin' => $p['hora_fin'], - 'sede' => $p['sede'] ?: null, - 'id_torneo' => $torneo->id, - 'precio' => $p['precio'] ?? 0, - ]); - - $contador++; - } - return $contador; - } -} diff --git a/app/Services/GeniusAgentService.php b/app/Services/GeniusAgentService.php deleted file mode 100644 index f079732..0000000 --- a/app/Services/GeniusAgentService.php +++ /dev/null @@ -1,229 +0,0 @@ -buildMessages($history, $message); - - try { - $response = Prism::text() - ->using(self::PROVIDER, $this->model()) - ->withSystemPrompt(SystemPromptPublic::get()) - ->withMessages($messages) - ->asText(); - - return $response->text; - } catch (PrismRateLimitedException $e) { - report($e); - return 'Demasiadas consultas en poco tiempo. Por favor, esperá un momento e intentá de nuevo.'; - } catch (Throwable $e) { - report($e); - return 'El agente no responde en este momento. Por favor, intentá de nuevo en unos instantes.'; - } - } - - public function chatAdmin(string $message, AgentThread $thread, bool $isSuperadmin = false): string - { - $messages = $this->buildMessages($thread->messages ?? [], $message); - - try { - $response = Prism::text() - ->using(self::PROVIDER, $this->model()) - ->withSystemPrompt(SystemPromptAdmin::get($isSuperadmin)) - ->withMessages($messages) - ->withTools($this->getAdminTools($isSuperadmin)) - ->withMaxSteps(self::MAX_STEPS) - ->asText(); - - if ($response->text !== '') { - $reply = $response->text; - } else { - Log::warning('Genius admin: empty text from model', [ - 'finishReason' => isset($response->finishReason) - ? (is_object($response->finishReason) ? ($response->finishReason->name ?? get_class($response->finishReason)) : $response->finishReason) - : null, - 'steps_count' => is_countable($response->steps ?? null) ? count($response->steps) : null, - 'tool_calls' => is_countable($response->toolCalls ?? null) ? count($response->toolCalls) : null, - 'tool_results' => is_countable($response->toolResults ?? null) ? count($response->toolResults) : null, - ]); - $reply = $this->fallbackFromToolResults($response); - } - - $updated = array_merge($thread->messages ?? [], [ - ['role' => 'user', 'content' => $message], - ['role' => 'assistant', 'content' => $reply], - ]); - - $thread->update(['messages' => $updated]); - - return $reply; - } catch (PrismRateLimitedException $e) { - report($e); - return 'Demasiadas consultas en poco tiempo. Por favor, esperá un momento e intentá de nuevo.'; - } catch (Throwable $e) { - report($e); - return 'El agente no responde en este momento. Por favor, intentá de nuevo en unos instantes.'; - } - } - - private function fallbackFromToolResults($response): string - { - $collected = []; - - $steps = $response->steps ?? []; - foreach ($steps as $step) { - if (!empty($step->text ?? '')) { - return $step->text; - } - - foreach (($step->toolResults ?? []) as $tr) { - $collected[] = [ - 'tool' => $tr->toolName ?? ($tr->name ?? 'tool'), - 'result' => $tr->result ?? null, - ]; - } - } - - if (empty($collected)) { - foreach (($response->toolResults ?? []) as $tr) { - $collected[] = [ - 'tool' => $tr->toolName ?? ($tr->name ?? 'tool'), - 'result' => $tr->result ?? null, - ]; - } - } - - if (empty($collected)) { - return 'El agente no devolvió una respuesta. Revisá storage/logs/laravel.log para el detalle.'; - } - - $lines = ["Datos obtenidos (la IA no generó un resumen en texto):"]; - foreach ($collected as $item) { - $decoded = is_string($item['result']) ? json_decode($item['result'], true) : $item['result']; - $pretty = is_array($decoded) || is_object($decoded) - ? json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) - : (string) $item['result']; - $lines[] = "\n**{$item['tool']}**:\n{$pretty}"; - } - - return implode("\n", $lines); - } - - /** @return array */ - private function buildMessages(array $history, string $newMessage): array - { - $limit = (int) config('services.genius.history_limit', 10); - $trimmed = $limit > 0 ? array_slice($history, -$limit * 2) : $history; - - $messages = []; - - foreach ($trimmed as $entry) { - if (($entry['role'] ?? '') === 'user') { - $messages[] = new UserMessage($entry['content']); - } elseif (($entry['role'] ?? '') === 'assistant') { - $messages[] = new AssistantMessage($entry['content']); - } - } - - $messages[] = new UserMessage($newMessage); - - return $messages; - } - - /** @return array */ - private function getAdminTools(bool $isSuperadmin): array - { - $readOnly = [ - Tool::as('listar_torneos') - ->for('Lista todos los torneos del sistema con sus IDs y fechas. Úsalo cuando el admin mencione un torneo por nombre para obtener el id_torneo.') - ->using(new ListarTorneosTool()), - - Tool::as('listar_equipos') - ->for('Lista los equipos del sistema. Acepta filtros opcionales por torneo (id_torneo) y grupo.') - ->withNumberParameter('id_torneo', 'ID del torneo para filtrar equipos', false) - ->withStringParameter('grupo', 'Nombre del grupo (ej. "A", "B") para filtrar dentro del torneo', false) - ->using(new ListarEquiposTool()), - - Tool::as('listar_eventos') - ->for('Lista los partidos/eventos del sistema. Acepta filtros opcionales por rango de fechas y torneo.') - ->withStringParameter('fecha_desde', 'Fecha de inicio del rango (formato YYYY-MM-DD)', false) - ->withStringParameter('fecha_hasta', 'Fecha de fin del rango (formato YYYY-MM-DD)', false) - ->withNumberParameter('id_torneo', 'ID del torneo para filtrar eventos', false) - ->using(new ListarEventosTool()), - ]; - - if (!$isSuperadmin) { - return $readOnly; - } - - $writeTools = [ - Tool::as('crear_partido') - ->for('Crea un nuevo partido en el sistema. SOLO ejecutar tras confirmación explícita del superadmin.') - ->withNumberParameter('id_equipo_local', 'ID del equipo local') - ->withNumberParameter('id_equipo_visitante', 'ID del equipo visitante') - ->withStringParameter('fecha_evento', 'Fecha del partido (formato YYYY-MM-DD)') - ->withStringParameter('hora_inicio', 'Hora de inicio (formato HH:MM)') - ->withStringParameter('hora_fin', 'Hora de fin (formato HH:MM)') - ->withStringParameter('sede', 'Nombre de la cancha o sede del partido') - ->withNumberParameter('id_torneo', 'ID del torneo al que pertenece el partido') - ->using(new CrearPartidoTool()), - - Tool::as('cargar_puntaje') - ->for('Actualiza el marcador de un partido existente. SOLO ejecutar tras confirmación explícita del superadmin.') - ->withStringParameter('id_evento', 'UUID del partido a actualizar') - ->withNumberParameter('marcador_local', 'Puntos del equipo local') - ->withNumberParameter('marcador_visitante', 'Puntos del equipo visitante') - ->using(new CargarPuntajeTool()), - - Tool::as('redactar_noticia') - ->for('Publica una noticia en el portal OnAPB. SOLO ejecutar tras confirmación explícita del superadmin.') - ->withStringParameter('titulo', 'Título de la noticia') - ->withStringParameter('contenido', 'Cuerpo completo de la noticia en HTML o texto plano') - ->withNumberParameter('id_torneo', 'ID del torneo relacionado (opcional)', false) - ->withStringParameter('categoria', 'Categoría de la noticia (opcional)', false) - ->using(new RedactarNoticiaTool()), - - Tool::as('eliminar_noticia') - ->for('Elimina (rollback) una noticia publicada. Usar cuando el superadmin pida deshacer una creación.') - ->withNumberParameter('id_noticia', 'ID numérico de la noticia a eliminar') - ->using(new EliminarNoticiaTool()), - - Tool::as('eliminar_partido') - ->for('Elimina (soft delete / rollback) un partido creado. Usar cuando el superadmin pida deshacer.') - ->withStringParameter('id_evento', 'UUID del partido a eliminar') - ->using(new EliminarPartidoTool()), - ]; - - return array_merge($readOnly, $writeTools); - } -} diff --git a/app/Services/ImageOptimizer.php b/app/Services/ImageOptimizer.php deleted file mode 100644 index 4c69f5f..0000000 --- a/app/Services/ImageOptimizer.php +++ /dev/null @@ -1,106 +0,0 @@ - ['maxWidth' => 1600, 'quality' => 82], - 'noticias' => ['maxWidth' => 1200, 'quality' => 82], - 'promos' => ['maxWidth' => 1200, 'quality' => 82], - 'clubes' => ['maxWidth' => 512, 'quality' => 85], - 'sponsors' => ['maxWidth' => 600, 'quality' => 85], - 'qr' => ['maxWidth' => 800, 'quality' => 85], - ]; - - private const MIN_RECOMPRESS_BYTES = 100 * 1024; - - /** - * Sube el archivo al disk 'public' y lo optimiza in-place. - * Retorna el path relativo (ej: "carousel/abc123.jpg"). - */ - public function storeAndOptimize(UploadedFile $file, string $folder): string - { - $path = $file->store($folder, 'public'); - - if (!extension_loaded('gd')) { - return $path; - } - - $cfg = self::FOLDERS[$folder] ?? null; - if (!$cfg) { - return $path; - } - - $abs = Storage::disk('public')->path($path); - $this->optimizeFile($abs, $cfg['maxWidth'], $cfg['quality']); - - return $path; - } - - /** - * Optimiza un archivo en disco (resize + recompress). - * Devuelve [origSize, newSize, newW, newH] o null si no se modifico. - */ - public function optimizeFile(string $file, int $maxWidth, int $quality): ?array - { - if (!file_exists($file)) return null; - - $origSize = filesize($file); - $info = @getimagesize($file); - if (!$info) return null; - - [$w, $h] = $info; - $needsResize = $w > $maxWidth; - $needsRecomp = $origSize > self::MIN_RECOMPRESS_BYTES; - if (!$needsResize && !$needsRecomp) return null; - - $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); - $img = match ($ext) { - 'jpg', 'jpeg' => @imagecreatefromjpeg($file), - 'png' => @imagecreatefrompng($file), - 'webp' => @imagecreatefromwebp($file), - default => null, - }; - if (!$img) return null; - - $newW = $w; $newH = $h; - if ($w > $maxWidth) { - $newW = $maxWidth; - $newH = (int) round($h * ($maxWidth / $w)); - $resized = imagecreatetruecolor($newW, $newH); - - if (in_array($ext, ['png', 'webp'])) { - imagealphablending($resized, false); - imagesavealpha($resized, true); - $transparent = imagecolorallocatealpha($resized, 0, 0, 0, 127); - imagefilledrectangle($resized, 0, 0, $newW, $newH, $transparent); - } - - imagecopyresampled($resized, $img, 0, 0, 0, 0, $newW, $newH, $w, $h); - $img = $resized; - } - - ob_start(); - match ($ext) { - 'jpg', 'jpeg' => imagejpeg($img, null, $quality), - 'png' => imagepng($img, null, 9), - 'webp' => imagewebp($img, null, $quality), - }; - $bytes = ob_get_clean(); - - if (strlen($bytes) >= $origSize) { - return null; - } - - file_put_contents($file, $bytes); - return [$origSize, strlen($bytes), $newW, $newH]; - } -} diff --git a/app/Services/NotificacionService.php b/app/Services/NotificacionService.php deleted file mode 100644 index 985efe1..0000000 --- a/app/Services/NotificacionService.php +++ /dev/null @@ -1,211 +0,0 @@ - $tipo_dest, - 'id_destinatario' => (string) $id_dest, - 'tipo' => $tipo, - 'titulo' => $titulo, - 'mensaje' => $mensaje, - 'url_accion' => $url, - 'leida' => false, - 'enviada_email' => false, - 'creada_en' => now(), - ]); - - // Intentar envío Push - $this->enviarPush([['tipo' => $tipo_dest, 'id' => $id_dest]], $titulo, $mensaje, $url); - - return $notif; - } - - /** - * Enviar la misma notificación a múltiples destinatarios. - * $destinatarios = [['tipo' => 'jugador', 'id' => 'XXX'], ...] - */ - public function enviarMasivo(array $destinatarios, string $tipo, string $titulo, string $mensaje, ?string $url = null): int - { - $rows = []; - $ahora = now()->toDateTimeString(); - - foreach ($destinatarios as $dest) { - $rows[] = [ - 'tipo_destinatario' => $dest['tipo'], - 'id_destinatario' => (string) $dest['id'], - 'tipo' => $tipo, - 'titulo' => $titulo, - 'mensaje' => $mensaje, - 'url_accion' => $url, - 'leida' => false, - 'enviada_email' => false, - 'creada_en' => $ahora, - ]; - } - - if (empty($rows)) return 0; - - // Insert en chunks para no sobrecargar - foreach (array_chunk($rows, 500) as $chunk) { - Notificacion::insert($chunk); - } - - // Intentar envío Push masivo - $this->enviarPush($destinatarios, $titulo, $mensaje, $url); - - return count($rows); - } - - /** - * Lógica centralizada para enviar Web Push Notifications - */ - private function enviarPush(array $destinatarios, string $titulo, string $mensaje, ?string $url = null): void - { - $vPublic = env('VAPID_PUBLIC_KEY'); - $vPrivate = env('VAPID_PRIVATE_KEY'); - - if (!$vPublic || !$vPrivate) return; - - // Buscar todas las suscripciones de estos usuarios - $subsFound = PushSubscription::where(function($query) use ($destinatarios) { - foreach ($destinatarios as $d) { - $query->orWhere(function($q) use ($d) { - $q->where('tipo_usuario', $d['tipo']) - ->where('id_usuario', (string)$d['id']); - }); - } - })->get(); - - if ($subsFound->isEmpty()) return; - - try { - $auth = [ - 'VAPID' => [ - 'subject' => env('APP_URL', 'http://localhost'), - 'publicKey' => $vPublic, - 'privateKey' => $vPrivate, - ], - ]; - - $webPush = new WebPush($auth); - $payload = json_encode([ - 'title' => $titulo, - 'body' => $mensaje, - 'url' => $url ?: '/', - ]); - - foreach ($subsFound as $sub) { - $webPush->queueNotification( - Subscription::create([ - 'endpoint' => $sub->endpoint, - 'publicKey' => $sub->p256dh, - 'authToken' => $sub->auth, - ]), - $payload - ); - } - - foreach ($webPush->flush() as $report) { - if (!$report->isSuccess()) { - // Si falló (ej: suscripción expirada), la borramos para no reintentar - if ($report->isSubscriptionExpired()) { - $endpoint = $report->getEndpoint(); - PushSubscription::where('endpoint', $endpoint)->delete(); - } - } - } - } catch (\Exception $e) { - Log::error("Error enviando Web Push: " . $e->getMessage()); - } - } - - /** - * Obtener notificaciones no leídas de un usuario. - */ - public function obtenerNoLeidas(string $tipo, string|int $id): Collection - { - return Notificacion::paraUsuario($tipo, $id) - ->noLeidas() - ->orderByDesc('creada_en') - ->limit(50) - ->get(); - } - - /** - * Obtener todas las notificaciones de un usuario (paginadas). - */ - public function obtenerTodas(string $tipo, string|int $id, int $perPage = 20) - { - return Notificacion::paraUsuario($tipo, $id) - ->orderByDesc('creada_en') - ->paginate($perPage); - } - - /** - * Contar notificaciones no leídas. - */ - public function contarNoLeidas(string $tipo, string|int $id): int - { - return Notificacion::paraUsuario($tipo, $id)->noLeidas()->count(); - } - - /** - * Marcar una notificación como leída (verificando pertenencia). - */ - public function marcarLeida(int $id_notif, string $tipo, string|int $id_dest): bool - { - return (bool) Notificacion::paraUsuario($tipo, $id_dest) - ->where('id', $id_notif) - ->update(['leida' => true]); - } - - /** - * Marcar todas como leídas para un usuario. - */ - public function marcarTodasLeidas(string $tipo, string|int $id_dest): int - { - return Notificacion::paraUsuario($tipo, $id_dest) - ->noLeidas() - ->update(['leida' => true]); - } - - /** - * Eliminar una notificación (verificando pertenencia). - */ - public function eliminar(int $id_notif, string $tipo, string|int $id_dest): bool - { - return (bool) Notificacion::paraUsuario($tipo, $id_dest) - ->where('id', $id_notif) - ->delete(); - } - - /** - * Eliminar todas las notificaciones de un usuario. - */ - public function eliminarTodas(string $tipo, string|int $id_dest): int - { - return Notificacion::paraUsuario($tipo, $id_dest)->delete(); - } -} - diff --git a/app/Services/TournamentService.php b/app/Services/TournamentService.php deleted file mode 100644 index cf42cb9..0000000 --- a/app/Services/TournamentService.php +++ /dev/null @@ -1,149 +0,0 @@ -findOrFail($idTorneo); - $stats = []; - - foreach ($torneo->equipos as $equipo) { - $groupName = $equipo->pivot->grupo ?: ($equipo->categoria . ' ' . $equipo->division); - - $stats[$groupName][$equipo->id_equipo] = [ - 'id' => $equipo->id_equipo, - 'nombre' => $equipo->club->nombre ?? 'Equipo', - 'logo' => $equipo->club->imagen ?? null, - 'categoria' => $equipo->categoria, - 'pj' => 0, - 'pg' => 0, - 'pp' => 0, - 'tf' => 0, - 'tc' => 0, - 'pts' => 0, - ]; - } - - $query = Evento::where('id_torneo', $idTorneo) - ->whereNotNull('marcador_local') - ->whereNotNull('marcador_visitante'); - - if ($onlyRegular) { - $query->where('fase', Evento::FASE_REGULAR); - } - - $matches = $query->get(); - - foreach ($matches as $m) { - $localGroup = null; - $visitGroup = null; - - foreach ($stats as $group => $teams) { - if (isset($teams[$m->id_equipo_local])) $localGroup = $group; - if (isset($teams[$m->id_equipo_visitante])) $visitGroup = $group; - } - - if (!$localGroup || !$visitGroup) continue; - - $stats[$localGroup][$m->id_equipo_local]['pj']++; - $stats[$localGroup][$m->id_equipo_local]['tf'] += $m->marcador_local; - $stats[$localGroup][$m->id_equipo_local]['tc'] += $m->marcador_visitante; - - $stats[$visitGroup][$m->id_equipo_visitante]['pj']++; - $stats[$visitGroup][$m->id_equipo_visitante]['tf'] += $m->marcador_visitante; - $stats[$visitGroup][$m->id_equipo_visitante]['tc'] += $m->marcador_local; - - if ($m->marcador_local > $m->marcador_visitante) { - $stats[$localGroup][$m->id_equipo_local]['pg']++; - $stats[$localGroup][$m->id_equipo_local]['pts'] += 2; - $stats[$visitGroup][$m->id_equipo_visitante]['pp']++; - $stats[$visitGroup][$m->id_equipo_visitante]['pts'] += 1; - } elseif ($m->marcador_visitante > $m->marcador_local) { - $stats[$visitGroup][$m->id_equipo_visitante]['pg']++; - $stats[$visitGroup][$m->id_equipo_visitante]['pts'] += 2; - $stats[$localGroup][$m->id_equipo_local]['pp']++; - $stats[$localGroup][$m->id_equipo_local]['pts'] += 1; - } else { - $stats[$localGroup][$m->id_equipo_local]['pts'] += 1; - $stats[$visitGroup][$m->id_equipo_visitante]['pts'] += 1; - } - } - - foreach ($stats as $group => &$teams) { - usort($teams, function($a, $b) { - if ($b['pts'] !== $a['pts']) return $b['pts'] - $a['pts']; - $diffA = $a['tf'] - $a['tc']; - $diffB = $b['tf'] - $b['tc']; - if ($diffB !== $diffA) return $diffB - $diffA; - return $b['tf'] - $a['tf']; - }); - } - - return $stats; - } - - public function getPlayoffBrackets(int $idTorneo): array - { - $playoffs = Evento::where('id_torneo', $idTorneo) - ->where('fase', '>', Evento::FASE_REGULAR) - ->with(['equipoLocal.club', 'equipoVisitante.club']) - ->orderBy('fase') - ->orderBy('numero_partido_bracket') - ->orderBy('fecha_evento') - ->get(); - - $bracket = [ - Evento::FASE_CUARTOS => collect(), - Evento::FASE_SEMIS => collect(), - Evento::FASE_FINAL => collect(), - ]; - - // Agrupar por fase y número de llave (bracket) - $grouped = $playoffs->groupBy(function($item) { - return $item->fase . '-' . $item->numero_partido_bracket; - }); - - foreach ($grouped as $key => $matches) { - $first = $matches->first(); - $fase = $first->fase; - $nro = $first->numero_partido_bracket; - - $winsLocal = 0; - $winsVisit = 0; - $finished = 0; - - foreach ($matches as $m) { - if ($m->marcador_local !== null && $m->marcador_visitante !== null) { - $finished++; - if ($m->marcador_local > $m->marcador_visitante) $winsLocal++; - elseif ($m->marcador_visitante > $m->marcador_local) $winsVisit++; - } - } - - $bracket[$fase][$nro] = [ - 'matches' => $matches, - 'wins_local' => $winsLocal, - 'wins_visitante' => $winsVisit, - 'total_partidos' => $matches->count(), - 'terminados' => $finished, - 'equipo_local' => $first->equipoLocal, - 'equipo_visitante' => $first->equipoVisitante, - ]; - } - - return $bracket; - } -}