272 lines
12 KiB
PHP
272 lines
12 KiB
PHP
<?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>";
|
||
}
|