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) */
$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
");
$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 ' . date('d/m/Y', strtotime($r['completed_date'])) . '. '
. 'Il prossimo aggiornamento era previsto per ' . date('d/m/Y', strtotime($r['next_due_date'])) . ''
. ' (scaduta da ' . abs($daysLeft) . ' giorni).',
'#dc3545',
$profileUrl,
$rec['is_hr']
);
} else {
$mail->Subject = '📚 Formazione in scadenza: ' . $r['topic_name'];
$daysText = $daysLeft === 0 ? 'oggi' : 'tra ' . $daysLeft . ' giorni';
$mail->Body = buildHtml(
'Formazione in scadenza',
$topicText,
'Completata il ' . date('d/m/Y', strtotime($r['completed_date'])) . '. '
. 'Prossimo aggiornamento previsto per ' . date('d/m/Y', strtotime($r['next_due_date'])) . ''
. ' (' . $daysText . ').',
'#e8930c',
$profileUrl,
$rec['is_hr']
);
}
$mail->isHTML(true);
$mail->AltBody = strip_tags(str_replace('
', "\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 ' . htmlspecialchars($employeeFullName) . ' non ha nessuna registrazione per il corso obbligatorio ' . htmlspecialchars($m['topic_name']) . '. Programma la prima erogazione.',
'#6b7280',
$profileUrl,
true
);
$mail->isHTML(true);
$mail->AltBody = strip_tags(str_replace('
', "\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 '