query( "SELECT * FROM alertas_config WHERE activo=1 ORDER BY dias_antes DESC" )->fetchAll(); // ── Obtener oficios que requieren alerta ────────────────────────────────────── foreach ($alertasConfig as $config) { $diasAntes = (int)$config['dias_antes']; if ($diasAntes >= 0) { // Próximos a vencer o día mismo $fechaTarget = date('Y-m-d', strtotime("+{$diasAntes} days")); $stmt = $db->prepare( "SELECT o.*, CONCAT(u.nombre,' ',u.apellido) AS responsable_nombre, u.email AS responsable_email FROM oficios o LEFT JOIN usuarios u ON u.id = o.responsable_id WHERE o.deleted_at IS NULL AND o.estado NOT IN ('respondido','archivado') AND o.fecha_vencimiento = ? AND u.email IS NOT NULL" ); $stmt->execute([$fechaTarget]); } else { // Día DESPUÉS de vencido (escalación) $fechaTarget = date('Y-m-d', strtotime("{$diasAntes} days")); $stmt = $db->prepare( "SELECT o.*, CONCAT(u.nombre,' ',u.apellido) AS responsable_nombre, u.email AS responsable_email, u.supervisor_id, CONCAT(s.nombre,' ',s.apellido) AS supervisor_nombre, s.email AS supervisor_email FROM oficios o LEFT JOIN usuarios u ON u.id = o.responsable_id LEFT JOIN usuarios s ON s.id = u.supervisor_id WHERE o.deleted_at IS NULL AND o.estado NOT IN ('respondido','archivado') AND o.fecha_vencimiento = ? AND u.email IS NOT NULL" ); $stmt->execute([$fechaTarget]); } $oficiosAlerta = $stmt->fetchAll(); foreach ($oficiosAlerta as $oficio) { // ── Notificación interna ────────────────────────────────────────────── if ($config['enviar_notificacion'] && $oficio['responsable_id']) { // Evitar duplicados: verificar si ya existe notificación de hoy $existe = $db->prepare( "SELECT id FROM notificaciones WHERE usuario_id=? AND oficio_id=? AND tipo='vencimiento' AND DATE(created_at)=?" ); $existe->execute([$oficio['responsable_id'], $oficio['id'], $hoy]); if (!$existe->fetchColumn()) { $titulo = $diasAntes > 0 ? "⚠️ Oficio vence en {$diasAntes} día(s)" : ($diasAntes === 0 ? '🔴 Oficio vence HOY' : '🚨 Oficio VENCIDO sin respuesta'); $mensaje = "El oficio {$oficio['numero_oficio']} – {$oficio['asunto']} vence el " . date('d/m/Y', strtotime($oficio['fecha_vencimiento'])); $db->prepare( "INSERT INTO notificaciones(usuario_id,oficio_id,tipo,titulo,mensaje) VALUES(?,?,'vencimiento',?,?)" )->execute([$oficio['responsable_id'], $oficio['id'], $titulo, $mensaje]); // Escalación al supervisor si ya venció if ($diasAntes < 0 && $oficio['supervisor_id']) { $db->prepare( "INSERT INTO notificaciones(usuario_id,oficio_id,tipo,titulo,mensaje) VALUES(?,?,'escalacion',?,?)" )->execute([ $oficio['supervisor_id'], $oficio['id'], '🚨 Escalación automática', "El oficio {$oficio['numero_oficio']} a cargo de {$oficio['responsable_nombre']} está vencido sin respuesta." ]); $db->prepare( "INSERT INTO historial_cambios(tabla,registro_id,accion,detalle,usuario_id) VALUES('oficios',?,'escalacion',?,NULL)" )->execute([$oficio['id'], "Escalación automática al supervisor #{$oficio['supervisor_id']}"]); } } } // ── Envío de correo ─────────────────────────────────────────────────── if ($config['enviar_email'] && $mailEnabled && $oficio['responsable_email']) { $result = enviarCorreoAlerta($oficio, $diasAntes, $libPath); if ($result) $enviados++; else $errores++; // Correo al supervisor si hubo escalación if ($diasAntes < 0 && !empty($oficio['supervisor_email'])) { enviarCorreoEscalacion($oficio, $libPath); } } } } // ── Reporte semanal (lunes) ─────────────────────────────────────────────────── if (date('N') === env('WEEKLY_REPORT_DAY', '1')) { $stmt = $db->query( "SELECT u.email, u.nombre, u.apellido, COUNT(o.id) AS total_vencer FROM usuarios u JOIN oficios o ON o.responsable_id = u.id WHERE o.deleted_at IS NULL AND o.estado NOT IN ('respondido','archivado') AND o.fecha_vencimiento BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 7 DAY) GROUP BY u.id HAVING total_vencer > 0" ); $semanal = $stmt->fetchAll(); foreach ($semanal as $row) { if ($mailEnabled && $row['email']) { enviarReporteSemanal($row, $libPath); $enviados++; } } } $log = "[" . date('Y-m-d H:i:s') . "] Alertas enviadas: $enviados | Errores: $errores\n"; file_put_contents(BASE_PATH . '/logs/cron_alertas.log', $log, FILE_APPEND); echo $log; // ── Funciones de correo ─────────────────────────────────────────────────────── function crearMailer(string $libPath): ?\PHPMailer\PHPMailer\PHPMailer { if (!class_exists('\PHPMailer\PHPMailer\PHPMailer')) return null; $mail = new \PHPMailer\PHPMailer\PHPMailer(true); $mail->isSMTP(); $mail->Host = env('MAIL_HOST'); $mail->SMTPAuth = true; $mail->Username = env('MAIL_USERNAME'); $mail->Password = env('MAIL_PASSWORD'); $mail->SMTPSecure = env('MAIL_ENCRYPTION', 'tls'); $mail->Port = (int)env('MAIL_PORT', 587); $mail->setFrom(env('MAIL_FROM_ADDRESS'), env('MAIL_FROM_NAME')); $mail->isHTML(true); $mail->CharSet = 'UTF-8'; return $mail; } function enviarCorreoAlerta(array $oficio, int $diasAntes, string $libPath): bool { try { $mail = crearMailer($libPath); if (!$mail) return false; $mail->addAddress($oficio['responsable_email'], $oficio['responsable_nombre']); $vence = date('d/m/Y', strtotime($oficio['fecha_vencimiento'])); $asuntoMail = $diasAntes > 0 ? "⚠️ ALERTA: Oficio vence en {$diasAntes} día(s)" : ($diasAntes === 0 ? '🔴 ALERTA: Oficio vence HOY' : '🚨 AVISO: Oficio VENCIDO'); $mail->Subject = $asuntoMail . ' – ' . APP_NAME; $mail->Body = emailTemplate($asuntoMail, "El siguiente oficio requiere tu atención:", [ 'Número de Oficio' => $oficio['numero_oficio'], 'Asunto' => $oficio['asunto'], 'Fecha Vencimiento'=> $vence, 'Prioridad' => strtoupper($oficio['prioridad']), 'Estado actual' => $oficio['estado'], ], env('APP_URL') . '/views/oficios/detalle.php?id=' . $oficio['id'] ); $mail->send(); return true; } catch (\Exception $e) { error_log('Mailer alerta error: ' . $e->getMessage()); return false; } } function enviarCorreoEscalacion(array $oficio, string $libPath): bool { try { $mail = crearMailer($libPath); if (!$mail || empty($oficio['supervisor_email'])) return false; $mail->addAddress($oficio['supervisor_email'], $oficio['supervisor_nombre'] ?? 'Supervisor'); $mail->Subject = '🚨 ESCALACIÓN: Oficio vencido – ' . APP_NAME; $mail->Body = emailTemplate('🚨 Escalación Automática', "Se notifica que el siguiente oficio ha vencido sin respuesta. El responsable es {$oficio['responsable_nombre']}.", [ 'Número de Oficio' => $oficio['numero_oficio'], 'Asunto' => $oficio['asunto'], 'Responsable' => $oficio['responsable_nombre'], 'Fecha Vencimiento' => date('d/m/Y', strtotime($oficio['fecha_vencimiento'])), ], env('APP_URL') . '/views/oficios/detalle.php?id=' . $oficio['id'] ); $mail->send(); return true; } catch (\Exception $e) { error_log('Mailer escalacion error: ' . $e->getMessage()); return false; } } function enviarReporteSemanal(array $row, string $libPath): bool { try { $mail = crearMailer($libPath); if (!$mail) return false; $mail->addAddress($row['email'], $row['nombre'] . ' ' . $row['apellido']); $mail->Subject = '📊 Reporte Semanal de Oficios – ' . APP_NAME; $mail->Body = emailTemplate('📊 Reporte Semanal', "Tienes {$row['total_vencer']} oficio(s) próximo(s) a vencer en los próximos 7 días. Revisa el sistema para gestionarlos oportunamente.", [], env('APP_URL') . '/views/oficios/lista.php?semaforo=proximo' ); $mail->send(); return true; } catch (\Exception $e) { error_log('Mailer semanal error: ' . $e->getMessage()); return false; } } function emailTemplate(string $titulo, string $intro, array $campos, string $url): string { $filas = ''; foreach ($campos as $k => $v) { $filas .= "$k".htmlspecialchars($v).""; } return "

".APP_NAME."

$titulo

$intro

".($filas ? "$filas
" : "")." Ver en el Sistema

Este es un mensaje automático del ".APP_NAME.". No responder a este correo.

"; }