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 ' . 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 '

' . htmlspecialchars($title) . '

' . htmlspecialchars($greeting) . '

' . htmlspecialchars($topic) . '

' . $message . '

Apri profilo

ZIBOGOMMA — Formazione

'; }