Ahora si(?
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CleanupOldEvents extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:cleanup-old-events';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Elimina eventos y QRs antiguos según la configuración del sistema.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$dias = \App\Models\Configuracion::get('dias_expiracion_eventos', 30);
|
||||
$fechaLimite = now()->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.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class OptimizeImages extends Command
|
||||
{
|
||||
protected $signature = 'optimize:images
|
||||
{--apply : Aplica cambios. Sin esta flag corre en modo dry-run.}
|
||||
{--folder= : Solo procesa esta carpeta (carousel|sponsors|qr|promos|noticias|clubes).}
|
||||
{--no-backup : No crea backup de los originales.}';
|
||||
|
||||
protected $description = 'Comprime y redimensiona imagenes en storage/app/public/. Por defecto dry-run.';
|
||||
|
||||
private array $config = [
|
||||
'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],
|
||||
];
|
||||
|
||||
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
|
||||
? '<fg=red>MODO APLICACION</> — los archivos seran reemplazados.'
|
||||
: '<fg=yellow>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 ? '<fg=green>OK</>' : '<fg=cyan>--</>',
|
||||
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';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\AgentThread;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class PurgeAgentThreads extends Command
|
||||
{
|
||||
protected $signature = 'agent:purge-threads';
|
||||
protected $description = 'Elimina los hilos de conversación del agente que hayan expirado.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$deleted = AgentThread::where('expires_at', '<', now())->delete();
|
||||
|
||||
$this->info("Se eliminaron {$deleted} hilo(s) expirado(s).");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\Evento;
|
||||
use App\Models\EquipoSeguimiento;
|
||||
use App\Services\NotificacionService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RecordatorioPartidos extends Command
|
||||
{
|
||||
protected $signature = 'notificaciones:recordatorio-partido {--test : Simula sin guardar}';
|
||||
protected $description = 'Envía recordatorios 48hs antes de cada partido a sus seguidores';
|
||||
|
||||
public function handle(NotificacionService $notifService): int
|
||||
{
|
||||
$desde = Carbon::now()->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\Evento;
|
||||
use App\Models\Configuracion;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class ReporteSemanal extends Command
|
||||
{
|
||||
protected $signature = 'reportes:semanal {--dry-run : Solo muestra el contenido sin enviar}';
|
||||
protected $description = 'Genera y envía el reporte semanal de actividad a los administradores';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$ahora = Carbon::now();
|
||||
$semanaAnteriorDesde = $ahora->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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user