user profile
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
<?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) */
|
||||
$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 <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>';
|
||||
}
|
||||
Reference in New Issue
Block a user