limpieza
This commit is contained in:
@@ -1,138 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Torneo;
|
||||
use App\Models\Equipo;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class FixtureService
|
||||
{
|
||||
/**
|
||||
* Genera un fixture Round-Robin para los equipos de un torneo.
|
||||
*
|
||||
* @param Torneo $torneo
|
||||
* @param string $fechaInicio 'Y-m-d'
|
||||
* @param int $diasEntreJornadas
|
||||
* @param string $sedeDefault
|
||||
* @param bool $dobleRueda Si true, genera ida y vuelta
|
||||
* @return array Array de arrays con los datos de cada evento a crear
|
||||
*/
|
||||
public function generarRoundRobin(
|
||||
Torneo $torneo,
|
||||
string $fechaInicio,
|
||||
int $diasEntreJornadas = 7,
|
||||
?string $sedeDefault = '',
|
||||
bool $dobleRueda = false
|
||||
): array {
|
||||
$equipos = $torneo->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;
|
||||
}
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\AI\Prompts\SystemPromptAdmin;
|
||||
use App\AI\Prompts\SystemPromptPublic;
|
||||
use App\AI\Tools\CargarPuntajeTool;
|
||||
use App\AI\Tools\CrearPartidoTool;
|
||||
use App\AI\Tools\EliminarNoticiaTool;
|
||||
use App\AI\Tools\EliminarPartidoTool;
|
||||
use App\AI\Tools\ListarEquiposTool;
|
||||
use App\AI\Tools\ListarEventosTool;
|
||||
use App\AI\Tools\ListarTorneosTool;
|
||||
use App\AI\Tools\RedactarNoticiaTool;
|
||||
use App\Models\AgentThread;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Prism\Prism\Exceptions\PrismRateLimitedException;
|
||||
use Prism\Prism\Facades\Prism;
|
||||
use Prism\Prism\Facades\Tool;
|
||||
use Prism\Prism\ValueObjects\Messages\AssistantMessage;
|
||||
use Prism\Prism\ValueObjects\Messages\UserMessage;
|
||||
use Throwable;
|
||||
|
||||
class GeniusAgentService
|
||||
{
|
||||
private const PROVIDER = 'gemini';
|
||||
private const MAX_STEPS = 6;
|
||||
|
||||
private function model(): string
|
||||
{
|
||||
return config('services.genius.model', 'gemini-2.5-flash-lite');
|
||||
}
|
||||
|
||||
public function chatPublic(string $message, array $history): string
|
||||
{
|
||||
$messages = $this->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<int, \Prism\Prism\Contracts\Message> */
|
||||
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<int, \Prism\Prism\Tool> */
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ImageOptimizer
|
||||
{
|
||||
/**
|
||||
* Configuracion por carpeta. Una unica fuente de verdad — usada por el
|
||||
* comando optimize:images y por los uploads de los controllers admin.
|
||||
*/
|
||||
public const FOLDERS = [
|
||||
'carousel' => ['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];
|
||||
}
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Notificacion;
|
||||
use App\Models\PushSubscription;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Minishlink\WebPush\WebPush;
|
||||
|
||||
use Minishlink\WebPush\Subscription;
|
||||
|
||||
class NotificacionService
|
||||
{
|
||||
/**
|
||||
* Enviar una notificación in-app a un usuario (jugador o aficionado).
|
||||
*/
|
||||
public function enviar(
|
||||
string $tipo_dest,
|
||||
string|int $id_dest,
|
||||
string $tipo,
|
||||
string $titulo,
|
||||
string $mensaje,
|
||||
?string $url = null
|
||||
): Notificacion {
|
||||
$notif = Notificacion::create([
|
||||
'tipo_destinatario' => $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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Torneo;
|
||||
use App\Models\Evento;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TournamentService
|
||||
{
|
||||
/**
|
||||
* Calcula la tabla de posiciones de un torneo.
|
||||
*
|
||||
* @param int $idTorneo
|
||||
* @param bool $onlyRegular Si true, solo toma partidos de fase regular
|
||||
* @return array
|
||||
*/
|
||||
public function getStandings(int $idTorneo, bool $onlyRegular = true): array
|
||||
{
|
||||
$torneo = Torneo::with('equipos.club')->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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user