356 lines
15 KiB
PHP
356 lines
15 KiB
PHP
<?php
|
|
/**
|
|
* Formazione — Email reminder cron script
|
|
* Run daily: 0 7 * * * php /var/www/html/public/userarea/cron/send_training_reminders.php
|
|
*
|
|
* Sends "due_soon" emails when next_due_date is within the reminder window
|
|
* (override reminder_days > topic default > 30 days).
|
|
* Sends "expired" emails when next_due_date is in the past.
|
|
* Skips rows with next_due_date IS NULL (one-off trainings).
|
|
* Skips already-sent notifications (same training + addressee + next_due_date).
|
|
* Recipients: the employee (employees.email or auth_users.email) + every HR user
|
|
* with role Admin / Superuser / employee-hr / manager.
|
|
*
|
|
* Optional CLI flags:
|
|
* --dry-run — log only, no SMTP, no DB write
|
|
* --only-email=foo@bar — restrict to a single addressee (for testing)
|
|
*/
|
|
|
|
require_once __DIR__ . '/../class/db-functions.php';
|
|
require_once __DIR__ . '/../../../vendor/autoload.php';
|
|
|
|
use Dotenv\Dotenv;
|
|
use PHPMailer\PHPMailer\PHPMailer;
|
|
use PHPMailer\PHPMailer\Exception;
|
|
|
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/../../../');
|
|
$dotenv->load();
|
|
|
|
$db = DBHandlerSelect::getInstance();
|
|
$pdo = $db->getConnection();
|
|
|
|
$today = date('Y-m-d');
|
|
$appUrl = rtrim($_ENV['APP_URL'] ?? 'http://localhost:8001', '/');
|
|
|
|
/* CLI flags */
|
|
$dryRun = false;
|
|
$onlyEmail = null;
|
|
foreach (array_slice($argv ?? [], 1) as $a) {
|
|
if ($a === '--dry-run' || $a === '-n') {
|
|
$dryRun = true;
|
|
} elseif (strpos($a, '--only-email=') === 0) {
|
|
$onlyEmail = substr($a, strlen('--only-email='));
|
|
}
|
|
}
|
|
|
|
$sent = 0;
|
|
$skipped = 0;
|
|
$errors = 0;
|
|
|
|
/* Candidate trainings (with optional override reminder + topic default).
|
|
Only the most recent record per (employee, topic) — older history rows skipped. */
|
|
$stmt = $pdo->query("
|
|
SELECT et.id, et.employee_id, et.completed_date, et.next_due_date,
|
|
et.reminder_days, et.delivered_by,
|
|
tt.name AS topic_name, tt.default_reminder_days AS topic_default_rem,
|
|
e.first_name, e.last_name, e.employee_code,
|
|
e.email AS employee_email_direct,
|
|
au.email AS employee_email_auth
|
|
FROM employee_trainings et
|
|
JOIN training_topics tt ON tt.id = et.training_topic_id
|
|
JOIN employees e ON e.id = et.employee_id
|
|
LEFT JOIN auth_users au ON au.id = e.auth_user_id
|
|
WHERE et.next_due_date IS NOT NULL
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM employee_trainings et2
|
|
WHERE et2.employee_id = et.employee_id
|
|
AND et2.training_topic_id = et.training_topic_id
|
|
AND (et2.completed_date > et.completed_date
|
|
OR (et2.completed_date = et.completed_date AND et2.id > et.id))
|
|
)
|
|
");
|
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
if (empty($rows)) {
|
|
echo date('Y-m-d H:i:s') . " — Nessuna formazione da notificare.\n";
|
|
exit(0);
|
|
}
|
|
|
|
/* HR addressees (one query, reused per training) */
|
|
$hrUsers = $pdo->query("
|
|
SELECT u.id, u.email, TRIM(CONCAT(COALESCE(u.first_name,''),' ',COALESCE(u.last_name,''))) AS name
|
|
FROM auth_users u
|
|
JOIN auth_roles r ON r.id = u.role_id
|
|
WHERE r.name IN ('Admin','Superuser','employee-hr','manager')
|
|
AND u.email IS NOT NULL AND u.email <> ''
|
|
")->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$checkSent = $pdo->prepare("
|
|
SELECT COUNT(*) FROM training_reminder_log
|
|
WHERE training_id = ? AND addressee_email = ? AND next_due_date = ?
|
|
");
|
|
$insertLog = $pdo->prepare("
|
|
INSERT INTO training_reminder_log
|
|
(training_id, addressee_email, next_due_date, status_at_send, sent_at)
|
|
VALUES (?, ?, ?, ?, NOW())
|
|
");
|
|
|
|
foreach ($rows as $r) {
|
|
$rem = $r['reminder_days'] !== null
|
|
? (int)$r['reminder_days']
|
|
: ($r['topic_default_rem'] !== null ? (int)$r['topic_default_rem'] : 30);
|
|
$isOverdue = $r['next_due_date'] < $today;
|
|
$daysLeft = (int)((strtotime($r['next_due_date']) - strtotime($today)) / 86400);
|
|
|
|
if (!$isOverdue && $daysLeft > $rem) {
|
|
continue; // not yet in the reminder window
|
|
}
|
|
$type = $isOverdue ? 'expired' : 'update_to_be_scheduled';
|
|
|
|
$employeeFullName = trim($r['first_name'] . ' ' . $r['last_name']);
|
|
$employeeEmail = !empty($r['employee_email_direct'])
|
|
? $r['employee_email_direct']
|
|
: (!empty($r['employee_email_auth']) ? $r['employee_email_auth'] : null);
|
|
|
|
/* Collect addressees (employee + HR), deduplicated by lowercased email */
|
|
$recipients = [];
|
|
if ($employeeEmail) {
|
|
$key = strtolower(trim($employeeEmail));
|
|
$recipients[$key] = ['email' => $employeeEmail, 'name' => $employeeFullName, 'is_hr' => false];
|
|
}
|
|
foreach ($hrUsers as $hr) {
|
|
$key = strtolower(trim((string)$hr['email']));
|
|
if ($key === '' || isset($recipients[$key])) continue;
|
|
$recipients[$key] = ['email' => $hr['email'], 'name' => trim((string)$hr['name']), 'is_hr' => true];
|
|
}
|
|
if (empty($recipients)) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($recipients as $email => $rec) {
|
|
if ($onlyEmail !== null && strcasecmp($rec['email'], $onlyEmail) !== 0) continue;
|
|
|
|
$checkSent->execute([$r['id'], $rec['email'], $r['next_due_date']]);
|
|
if ($checkSent->fetchColumn() > 0) {
|
|
$skipped++;
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$mail = new PHPMailer(true);
|
|
|
|
// SMTP config from .env
|
|
$mailer = $_ENV['MAIL_MAILER'] ?? 'mail';
|
|
if ($mailer === 'smtp') {
|
|
$mail->isSMTP();
|
|
$mail->Host = $_ENV['MAIL_HOST'] ?? 'localhost';
|
|
$mail->Port = (int)($_ENV['MAIL_PORT'] ?? 587);
|
|
if (!empty($_ENV['MAIL_USERNAME']) && $_ENV['MAIL_USERNAME'] !== 'null') {
|
|
$mail->SMTPAuth = true;
|
|
$mail->Username = $_ENV['MAIL_USERNAME'];
|
|
$mail->Password = $_ENV['MAIL_PASSWORD'] ?? '';
|
|
}
|
|
$enc = $_ENV['MAIL_ENCRYPTION'] ?? '';
|
|
if ($enc && $enc !== 'null') {
|
|
$mail->SMTPSecure = $enc;
|
|
}
|
|
}
|
|
|
|
$mail->CharSet = 'UTF-8';
|
|
$mail->setFrom(
|
|
$_ENV['MAIL_FROM_ADDRESS'] ?? 'noreply@zibogomma.it',
|
|
$_ENV['MAIL_FROM_NAME'] ?? 'Formazione ZIBOGOMMA'
|
|
);
|
|
$mail->addAddress($rec['email'], $rec['name'] ?: $rec['email']);
|
|
|
|
$profileUrl = $appUrl . '/userarea/employee-profile.php?id=' . (int)$r['employee_id'] . '#tab-training';
|
|
$topicText = $r['topic_name'] . ' — ' . $employeeFullName
|
|
. (!empty($r['employee_code']) ? ' (' . $r['employee_code'] . ')' : '');
|
|
|
|
if ($isOverdue) {
|
|
$mail->Subject = '⚠️ Formazione scaduta: ' . $r['topic_name'];
|
|
$mail->Body = buildHtml(
|
|
'Formazione scaduta',
|
|
$topicText,
|
|
'Completata il <strong>' . date('d/m/Y', strtotime($r['completed_date'])) . '</strong>. '
|
|
. 'Il prossimo aggiornamento era previsto per <strong>' . date('d/m/Y', strtotime($r['next_due_date'])) . '</strong>'
|
|
. ' (scaduta da <strong>' . abs($daysLeft) . ' giorni</strong>).',
|
|
'#dc3545',
|
|
$profileUrl,
|
|
$rec['is_hr']
|
|
);
|
|
} else {
|
|
$mail->Subject = '📚 Formazione in scadenza: ' . $r['topic_name'];
|
|
$daysText = $daysLeft === 0 ? 'oggi' : 'tra <strong>' . $daysLeft . ' giorni</strong>';
|
|
$mail->Body = buildHtml(
|
|
'Formazione in scadenza',
|
|
$topicText,
|
|
'Completata il <strong>' . date('d/m/Y', strtotime($r['completed_date'])) . '</strong>. '
|
|
. 'Prossimo aggiornamento previsto per <strong>' . date('d/m/Y', strtotime($r['next_due_date'])) . '</strong>'
|
|
. ' (' . $daysText . ').',
|
|
'#e8930c',
|
|
$profileUrl,
|
|
$rec['is_hr']
|
|
);
|
|
}
|
|
|
|
$mail->isHTML(true);
|
|
$mail->AltBody = strip_tags(str_replace('<br>', "\n", $mail->Body));
|
|
|
|
if ($dryRun) {
|
|
echo date('H:i:s') . " ◌ DRY {$type} → {$rec['email']} — {$r['topic_name']}\n";
|
|
$sent++;
|
|
continue;
|
|
}
|
|
|
|
$mail->send();
|
|
$insertLog->execute([$r['id'], $rec['email'], $r['next_due_date'], $type]);
|
|
$sent++;
|
|
echo date('H:i:s') . " ✓ {$type} → {$rec['email']} — {$r['topic_name']}\n";
|
|
|
|
} catch (Exception $e) {
|
|
$errors++;
|
|
echo date('H:i:s') . " ✗ Errore {$rec['email']}: {$e->getMessage()}\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ============================================================================
|
|
NOT-PRESENT reminders — mandatory topics with no record for an employee.
|
|
Notify HR only.
|
|
De-dup by (employee_id, training_topic_id, addressee_email).
|
|
============================================================================ */
|
|
$missingStmt = $pdo->query("
|
|
SELECT e.id AS employee_id, e.first_name, e.last_name, e.employee_code,
|
|
tt.id AS topic_id, tt.name AS topic_name
|
|
FROM employees e
|
|
CROSS JOIN training_topics tt
|
|
WHERE tt.is_active = 1 AND tt.is_mandatory = 1
|
|
AND (e.status IS NULL OR e.status = 'active')
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM employee_trainings et
|
|
WHERE et.employee_id = e.id AND et.training_topic_id = tt.id
|
|
)
|
|
ORDER BY e.last_name, e.first_name, tt.name
|
|
");
|
|
$missingRows = $missingStmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
$checkMissingSent = $pdo->prepare("
|
|
SELECT COUNT(*) FROM training_reminder_log
|
|
WHERE employee_id = ? AND training_topic_id = ? AND addressee_email = ?
|
|
AND status_at_send = 'not_present'
|
|
");
|
|
$insertMissingLog = $pdo->prepare("
|
|
INSERT INTO training_reminder_log
|
|
(training_id, employee_id, training_topic_id, addressee_email, next_due_date, status_at_send, sent_at)
|
|
VALUES (NULL, ?, ?, ?, NULL, 'not_present', NOW())
|
|
");
|
|
|
|
foreach ($missingRows as $m) {
|
|
$employeeFullName = trim($m['first_name'] . ' ' . $m['last_name']);
|
|
|
|
foreach ($hrUsers as $hr) {
|
|
$email = trim((string)$hr['email']);
|
|
if ($email === '') continue;
|
|
if ($onlyEmail !== null && strcasecmp($email, $onlyEmail) !== 0) continue;
|
|
|
|
$checkMissingSent->execute([$m['employee_id'], $m['topic_id'], $email]);
|
|
if ($checkMissingSent->fetchColumn() > 0) {
|
|
$skipped++;
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$mail = new PHPMailer(true);
|
|
$mailer = $_ENV['MAIL_MAILER'] ?? 'mail';
|
|
if ($mailer === 'smtp') {
|
|
$mail->isSMTP();
|
|
$mail->Host = $_ENV['MAIL_HOST'] ?? 'localhost';
|
|
$mail->Port = (int)($_ENV['MAIL_PORT'] ?? 587);
|
|
if (!empty($_ENV['MAIL_USERNAME']) && $_ENV['MAIL_USERNAME'] !== 'null') {
|
|
$mail->SMTPAuth = true;
|
|
$mail->Username = $_ENV['MAIL_USERNAME'];
|
|
$mail->Password = $_ENV['MAIL_PASSWORD'] ?? '';
|
|
}
|
|
$enc = $_ENV['MAIL_ENCRYPTION'] ?? '';
|
|
if ($enc && $enc !== 'null') {
|
|
$mail->SMTPSecure = $enc;
|
|
}
|
|
}
|
|
|
|
$mail->CharSet = 'UTF-8';
|
|
$mail->setFrom(
|
|
$_ENV['MAIL_FROM_ADDRESS'] ?? 'noreply@zibogomma.it',
|
|
$_ENV['MAIL_FROM_NAME'] ?? 'Formazione ZIBOGOMMA'
|
|
);
|
|
$mail->addAddress($email, trim((string)$hr['name']) ?: $email);
|
|
|
|
$profileUrl = $appUrl . '/userarea/employee-profile.php?id=' . (int)$m['employee_id'] . '#tab-training';
|
|
$topicText = $m['topic_name'] . ' — ' . $employeeFullName
|
|
. (!empty($m['employee_code']) ? ' (' . $m['employee_code'] . ')' : '');
|
|
|
|
$mail->Subject = '🔔 Formazione obbligatoria non presente: ' . $m['topic_name'];
|
|
$mail->Body = buildHtml(
|
|
'Formazione obbligatoria non presente',
|
|
$topicText,
|
|
'Il dipendente <strong>' . htmlspecialchars($employeeFullName) . '</strong> non ha nessuna registrazione per il corso obbligatorio <strong>' . htmlspecialchars($m['topic_name']) . '</strong>. Programma la prima erogazione.',
|
|
'#6b7280',
|
|
$profileUrl,
|
|
true
|
|
);
|
|
$mail->isHTML(true);
|
|
$mail->AltBody = strip_tags(str_replace('<br>', "\n", $mail->Body));
|
|
|
|
if ($dryRun) {
|
|
echo date('H:i:s') . " ◌ DRY not_present → {$email} — {$m['topic_name']} / {$employeeFullName}\n";
|
|
$sent++;
|
|
continue;
|
|
}
|
|
|
|
$mail->send();
|
|
$insertMissingLog->execute([$m['employee_id'], $m['topic_id'], $email]);
|
|
$sent++;
|
|
echo date('H:i:s') . " ✓ not_present → {$email} — {$m['topic_name']} / {$employeeFullName}\n";
|
|
|
|
} catch (Exception $e) {
|
|
$errors++;
|
|
echo date('H:i:s') . " ✗ Errore {$email}: {$e->getMessage()}\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
echo "\n" . date('Y-m-d H:i:s') . " — Completato. Inviate: {$sent}, Saltate: {$skipped}, Errori: {$errors}\n";
|
|
|
|
// --- HTML email template ---
|
|
function buildHtml(string $title, string $topic, string $message, string $accentColor, string $url, bool $isForHr): string
|
|
{
|
|
$greeting = $isForHr
|
|
? 'Una formazione richiede attenzione.'
|
|
: 'Una delle tue formazioni richiede attenzione.';
|
|
return '
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head><meta charset="UTF-8"></head>
|
|
<body style="margin:0;padding:0;background:#f4f6f9;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif">
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="padding:30px 0">
|
|
<tr><td align="center">
|
|
<table width="560" cellpadding="0" cellspacing="0" style="background:#fff;border-radius:12px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.06)">
|
|
<tr><td style="background:' . $accentColor . ';padding:20px 30px">
|
|
<h1 style="margin:0;color:#fff;font-size:18px">' . htmlspecialchars($title) . '</h1>
|
|
</td></tr>
|
|
<tr><td style="padding:30px">
|
|
<p style="margin:0 0 12px;color:#444;font-size:14px">' . htmlspecialchars($greeting) . '</p>
|
|
<h2 style="margin:0 0 15px;color:#2c3e6b;font-size:16px">' . htmlspecialchars($topic) . '</h2>
|
|
<p style="margin:0 0 20px;color:#444;font-size:14px;line-height:1.6">' . $message . '</p>
|
|
<a href="' . htmlspecialchars($url) . '" style="display:inline-block;background:#5a8fd8;color:#fff;padding:10px 24px;border-radius:6px;text-decoration:none;font-weight:600;font-size:14px">Apri profilo</a>
|
|
</td></tr>
|
|
<tr><td style="padding:15px 30px;background:#f8f9fb;border-top:1px solid #eee">
|
|
<p style="margin:0;color:#999;font-size:11px">ZIBOGOMMA — Formazione</p>
|
|
</td></tr>
|
|
</table>
|
|
</td></tr>
|
|
</table>
|
|
</body>
|
|
</html>';
|
|
}
|