ProyectoGestionDocumentos/cron/enviar_alertas.php

272 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* enviar_alertas.php — Script de alertas (ejecutado por cron job)
* Cron recomendado: 0 8 * * * php /ruta/al/proyecto/cron/enviar_alertas.php
* O invocar desde navegador con: /cron/enviar_alertas.php?secret=CLAVE_SECRETA
*/
define('RUNNING_FROM_CLI', php_sapi_name() === 'cli');
if (!RUNNING_FROM_CLI) {
// Verificar clave secreta si se invoca por HTTP
require_once __DIR__ . '/../config/config.php';
$secret = $_GET['secret'] ?? '';
if (!hash_equals(env('ALERT_CRON_SECRET', ''), $secret)) {
http_response_code(403);
die('Acceso denegado.');
}
} else {
require_once __DIR__ . '/../config/config.php';
}
$libPath = BASE_PATH . '/lib/PHPMailer/src/';
$mailEnabled = file_exists($libPath . 'PHPMailer.php');
if ($mailEnabled) {
require_once $libPath . 'PHPMailer.php';
require_once $libPath . 'SMTP.php';
require_once $libPath . 'Exception.php';
}
$db = getDB();
$hoy = date('Y-m-d');
$enviados = 0;
$errores = 0;
// ── Cargar configuración de alertas activas ───────────────────────────────────
$alertasConfig = $db->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 <strong>{$oficio['responsable_nombre']}</strong>.",
[
'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 <strong>{$row['total_vencer']}</strong> 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 .= "<tr><td style='padding:6px 12px;font-weight:600;color:#64748b;white-space:nowrap'>$k</td><td style='padding:6px 12px'>".htmlspecialchars($v)."</td></tr>";
}
return "
<div style='font-family:Inter,Arial,sans-serif;max-width:600px;margin:0 auto;background:#f8fafc;padding:24px'>
<div style='background:#1e1b4b;padding:24px;border-radius:12px 12px 0 0;text-align:center'>
<h1 style='color:#fff;font-size:1.1rem;margin:0'>".APP_NAME."</h1>
</div>
<div style='background:#fff;padding:24px;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px'>
<h2 style='color:#1e293b;font-size:1.1rem;margin:0 0 12px'>$titulo</h2>
<p style='color:#475569;margin:0 0 20px'>$intro</p>
".($filas ? "<table style='width:100%;border-collapse:collapse;margin-bottom:20px;border:1px solid #e2e8f0;border-radius:8px;overflow:hidden'>$filas</table>" : "")."
<a href='$url' style='display:inline-block;background:#4f46e5;color:#fff;padding:12px 24px;border-radius:8px;text-decoration:none;font-weight:600'>
Ver en el Sistema
</a>
<p style='color:#94a3b8;font-size:.8rem;margin-top:24px'>
Este es un mensaje automático del ".APP_NAME.". No responder a este correo.
</p>
</div>
</div>";
}