502 lines
20 KiB
PHP
502 lines
20 KiB
PHP
<?php
|
|
|
|
use PHPMailer\PHPMailer\Exception;
|
|
use PHPMailer\PHPMailer\PHPMailer;
|
|
|
|
require_once __DIR__ . '/../../inc/PHPMailer/src/Exception.php';
|
|
require_once __DIR__ . '/../../inc/PHPMailer/src/PHPMailer.php';
|
|
require_once __DIR__ . '/../../inc/PHPMailer/src/SMTP.php';
|
|
|
|
if (!function_exists('timeErrorNotificationsEnsureTables')) {
|
|
function timeErrorNotificationsEnsureTables(PDO $pdo): void
|
|
{
|
|
$legacyColumnStmt = $pdo->prepare(
|
|
"SELECT COUNT(*)
|
|
FROM information_schema.COLUMNS
|
|
WHERE TABLE_SCHEMA = DATABASE()
|
|
AND TABLE_NAME = 'time_error_notifications'
|
|
AND COLUMN_NAME = 'error_date'"
|
|
);
|
|
$legacyColumnStmt->execute();
|
|
if ((int)$legacyColumnStmt->fetchColumn() > 0) {
|
|
$pdo->exec("DROP TABLE IF EXISTS time_error_notifications");
|
|
}
|
|
|
|
$pdo->exec(
|
|
"CREATE TABLE IF NOT EXISTS time_error_notification_state (
|
|
employee_id INT NOT NULL,
|
|
cycle_started_on DATE NOT NULL,
|
|
first_error_date DATE NOT NULL,
|
|
last_notification_stage VARCHAR(50) DEFAULT NULL,
|
|
last_notification_sent_at DATETIME DEFAULT NULL,
|
|
employee_day_1_sent_at DATETIME DEFAULT NULL,
|
|
employee_day_3_sent_at DATETIME DEFAULT NULL,
|
|
admin_day_7_sent_at DATETIME DEFAULT NULL,
|
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (employee_id)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
|
|
);
|
|
|
|
$pdo->exec(
|
|
"CREATE TABLE IF NOT EXISTS time_error_notifications (
|
|
id INT NOT NULL AUTO_INCREMENT,
|
|
employee_id INT NOT NULL,
|
|
cycle_started_on DATE NOT NULL,
|
|
notification_stage VARCHAR(50) NOT NULL,
|
|
recipient_email VARCHAR(255) NOT NULL,
|
|
sent_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (id),
|
|
UNIQUE KEY uniq_time_error_notification (employee_id, cycle_started_on, notification_stage, recipient_email)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!function_exists('timeErrorNotificationsSendMail')) {
|
|
function timeErrorNotificationsSendMail(PDO $pdo, string $recipient, string $subject, string $body): bool
|
|
{
|
|
$stmt = $pdo->prepare("SELECT * FROM config LIMIT 1");
|
|
$stmt->execute();
|
|
$config = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$config) {
|
|
return false;
|
|
}
|
|
|
|
$mail = new PHPMailer(true);
|
|
|
|
try {
|
|
$mail->SMTPDebug = 0;
|
|
$mail->isSMTP();
|
|
$mail->Host = (string)$config['mailserver'];
|
|
$mail->SMTPAuth = true;
|
|
$mail->Username = (string)$config['mailUsername'];
|
|
$mail->Password = (string)$config['mailPassword'];
|
|
|
|
if (!empty($config['mailSMTPSecure'])) {
|
|
$mail->SMTPSecure = (string)$config['mailSMTPSecure'];
|
|
}
|
|
|
|
$mail->Port = (int)$config['mailPort'];
|
|
$mail->CharSet = 'UTF-8';
|
|
$mail->setFrom((string)$config['mailFrom'], (string)$config['mailFromName']);
|
|
$mail->addAddress($recipient);
|
|
$mail->isHTML(true);
|
|
$mail->Subject = $subject;
|
|
$mail->Body = $body;
|
|
$mail->AltBody = trim(html_entity_decode(strip_tags(str_replace(["<br>", "<br/>", "<br />"], "\n", $body)), ENT_QUOTES, 'UTF-8'));
|
|
|
|
$mail->send();
|
|
return true;
|
|
} catch (Exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!function_exists('timeErrorNotificationsFetchInvalidEntries')) {
|
|
function timeErrorNotificationsFetchInvalidEntries(PDO $pdo): array
|
|
{
|
|
$stmt = $pdo->prepare(
|
|
"SELECT
|
|
u.id AS employee_id,
|
|
u.email,
|
|
u.vorname,
|
|
u.nachname,
|
|
DATE(t.timestamp_datetime) AS error_date,
|
|
GROUP_CONCAT(t.timestamp_type ORDER BY t.timestamp_datetime) AS day_sequence
|
|
FROM users u
|
|
INNER JOIN timestamps t ON t.employee_id = u.id
|
|
WHERE u.zeiterfassung = '1'
|
|
AND DATE(t.timestamp_datetime) < CURDATE()
|
|
GROUP BY u.id, u.email, u.vorname, u.nachname, DATE(t.timestamp_datetime)
|
|
ORDER BY error_date ASC, u.nachname ASC, u.vorname ASC"
|
|
);
|
|
$stmt->execute();
|
|
|
|
$entries = [];
|
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
if (!isValidSequence((string)$row['day_sequence'])) {
|
|
$entries[] = $row;
|
|
}
|
|
}
|
|
|
|
return $entries;
|
|
}
|
|
}
|
|
|
|
if (!function_exists('timeErrorNotificationsGroupEntriesByEmployee')) {
|
|
function timeErrorNotificationsGroupEntriesByEmployee(array $entries): array
|
|
{
|
|
$grouped = [];
|
|
|
|
foreach ($entries as $entry) {
|
|
$employeeId = (int)$entry['employee_id'];
|
|
if (!isset($grouped[$employeeId])) {
|
|
$grouped[$employeeId] = [
|
|
'employee_id' => $employeeId,
|
|
'email' => (string)$entry['email'],
|
|
'vorname' => (string)$entry['vorname'],
|
|
'nachname' => (string)$entry['nachname'],
|
|
'first_error_date' => (string)$entry['error_date'],
|
|
'error_dates' => [],
|
|
];
|
|
}
|
|
|
|
$grouped[$employeeId]['error_dates'][] = (string)$entry['error_date'];
|
|
if ((string)$entry['error_date'] < $grouped[$employeeId]['first_error_date']) {
|
|
$grouped[$employeeId]['first_error_date'] = (string)$entry['error_date'];
|
|
}
|
|
}
|
|
|
|
foreach ($grouped as &$employee) {
|
|
$employee['error_dates'] = array_values(array_unique($employee['error_dates']));
|
|
sort($employee['error_dates']);
|
|
}
|
|
unset($employee);
|
|
|
|
return $grouped;
|
|
}
|
|
}
|
|
|
|
if (!function_exists('timeErrorNotificationsFetchAdminRecipients')) {
|
|
function timeErrorNotificationsFetchAdminRecipients(PDO $pdo): array
|
|
{
|
|
$stmt = $pdo->prepare(
|
|
"SELECT DISTINCT
|
|
u.id,
|
|
u.email,
|
|
u.vorname,
|
|
u.nachname
|
|
FROM users u
|
|
LEFT JOIN users_admin ua ON ua.userid = u.id
|
|
WHERE (u.admin = 1 OR ua.userid IS NOT NULL)
|
|
AND u.email IS NOT NULL
|
|
AND u.email <> ''
|
|
ORDER BY u.nachname ASC, u.vorname ASC"
|
|
);
|
|
$stmt->execute();
|
|
|
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
}
|
|
}
|
|
|
|
if (!function_exists('timeErrorNotificationsFetchStateByEmployee')) {
|
|
function timeErrorNotificationsFetchStateByEmployee(PDO $pdo): array
|
|
{
|
|
timeErrorNotificationsEnsureTables($pdo);
|
|
|
|
$stmt = $pdo->prepare("SELECT * FROM time_error_notification_state");
|
|
$stmt->execute();
|
|
|
|
$states = [];
|
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
$states[(int)$row['employee_id']] = $row;
|
|
}
|
|
|
|
return $states;
|
|
}
|
|
}
|
|
|
|
if (!function_exists('timeErrorNotificationsUpsertState')) {
|
|
function timeErrorNotificationsUpsertState(PDO $pdo, array $employee, ?array $existingState): array
|
|
{
|
|
timeErrorNotificationsEnsureTables($pdo);
|
|
|
|
$cycleStartedOn = $existingState['cycle_started_on'] ?? $employee['first_error_date'];
|
|
$stmt = $pdo->prepare(
|
|
"INSERT INTO time_error_notification_state (
|
|
employee_id,
|
|
cycle_started_on,
|
|
first_error_date,
|
|
last_notification_stage,
|
|
last_notification_sent_at,
|
|
employee_day_1_sent_at,
|
|
employee_day_3_sent_at,
|
|
admin_day_7_sent_at
|
|
) VALUES (
|
|
:employee_id,
|
|
:cycle_started_on,
|
|
:first_error_date,
|
|
:last_notification_stage,
|
|
:last_notification_sent_at,
|
|
:employee_day_1_sent_at,
|
|
:employee_day_3_sent_at,
|
|
:admin_day_7_sent_at
|
|
)
|
|
ON DUPLICATE KEY UPDATE
|
|
cycle_started_on = VALUES(cycle_started_on),
|
|
first_error_date = VALUES(first_error_date),
|
|
last_notification_stage = VALUES(last_notification_stage),
|
|
last_notification_sent_at = VALUES(last_notification_sent_at),
|
|
employee_day_1_sent_at = VALUES(employee_day_1_sent_at),
|
|
employee_day_3_sent_at = VALUES(employee_day_3_sent_at),
|
|
admin_day_7_sent_at = VALUES(admin_day_7_sent_at)"
|
|
);
|
|
$stmt->execute([
|
|
'employee_id' => $employee['employee_id'],
|
|
'cycle_started_on' => $cycleStartedOn,
|
|
'first_error_date' => $employee['first_error_date'],
|
|
'last_notification_stage' => $existingState['last_notification_stage'] ?? null,
|
|
'last_notification_sent_at' => $existingState['last_notification_sent_at'] ?? null,
|
|
'employee_day_1_sent_at' => $existingState['employee_day_1_sent_at'] ?? null,
|
|
'employee_day_3_sent_at' => $existingState['employee_day_3_sent_at'] ?? null,
|
|
'admin_day_7_sent_at' => $existingState['admin_day_7_sent_at'] ?? null,
|
|
]);
|
|
|
|
return [
|
|
'employee_id' => $employee['employee_id'],
|
|
'cycle_started_on' => $cycleStartedOn,
|
|
'first_error_date' => $employee['first_error_date'],
|
|
'last_notification_stage' => $existingState['last_notification_stage'] ?? null,
|
|
'last_notification_sent_at' => $existingState['last_notification_sent_at'] ?? null,
|
|
'employee_day_1_sent_at' => $existingState['employee_day_1_sent_at'] ?? null,
|
|
'employee_day_3_sent_at' => $existingState['employee_day_3_sent_at'] ?? null,
|
|
'admin_day_7_sent_at' => $existingState['admin_day_7_sent_at'] ?? null,
|
|
];
|
|
}
|
|
}
|
|
|
|
if (!function_exists('timeErrorNotificationsClearResolvedStates')) {
|
|
function timeErrorNotificationsClearResolvedStates(PDO $pdo, array $openEmployeeIds): int
|
|
{
|
|
timeErrorNotificationsEnsureTables($pdo);
|
|
|
|
if (empty($openEmployeeIds)) {
|
|
$stmt = $pdo->prepare("DELETE FROM time_error_notification_state");
|
|
$stmt->execute();
|
|
return $stmt->rowCount();
|
|
}
|
|
|
|
$placeholders = implode(',', array_fill(0, count($openEmployeeIds), '?'));
|
|
$stmt = $pdo->prepare("DELETE FROM time_error_notification_state WHERE employee_id NOT IN ($placeholders)");
|
|
$stmt->execute(array_values($openEmployeeIds));
|
|
|
|
return $stmt->rowCount();
|
|
}
|
|
}
|
|
|
|
if (!function_exists('timeErrorNotificationsMarkStageSent')) {
|
|
function timeErrorNotificationsMarkStageSent(PDO $pdo, array $state, string $stage, string $recipientEmail): array
|
|
{
|
|
timeErrorNotificationsEnsureTables($pdo);
|
|
|
|
$stmt = $pdo->prepare(
|
|
"INSERT INTO time_error_notifications (employee_id, cycle_started_on, notification_stage, recipient_email)
|
|
VALUES (:employee_id, :cycle_started_on, :notification_stage, :recipient_email)
|
|
ON DUPLICATE KEY UPDATE sent_at = sent_at"
|
|
);
|
|
$stmt->execute([
|
|
'employee_id' => $state['employee_id'],
|
|
'cycle_started_on' => $state['cycle_started_on'],
|
|
'notification_stage' => $stage,
|
|
'recipient_email' => $recipientEmail,
|
|
]);
|
|
|
|
$now = (new DateTimeImmutable('now'))->format('Y-m-d H:i:s');
|
|
$state['last_notification_stage'] = $stage;
|
|
$state['last_notification_sent_at'] = $now;
|
|
|
|
if ($stage === 'employee_day_1') {
|
|
$state['employee_day_1_sent_at'] = $now;
|
|
} elseif ($stage === 'employee_day_3') {
|
|
$state['employee_day_3_sent_at'] = $now;
|
|
} elseif ($stage === 'admin_day_7') {
|
|
$state['admin_day_7_sent_at'] = $now;
|
|
}
|
|
|
|
$stmt = $pdo->prepare(
|
|
"UPDATE time_error_notification_state
|
|
SET last_notification_stage = :last_notification_stage,
|
|
last_notification_sent_at = :last_notification_sent_at,
|
|
employee_day_1_sent_at = :employee_day_1_sent_at,
|
|
employee_day_3_sent_at = :employee_day_3_sent_at,
|
|
admin_day_7_sent_at = :admin_day_7_sent_at
|
|
WHERE employee_id = :employee_id"
|
|
);
|
|
$stmt->execute([
|
|
'last_notification_stage' => $state['last_notification_stage'],
|
|
'last_notification_sent_at' => $state['last_notification_sent_at'],
|
|
'employee_day_1_sent_at' => $state['employee_day_1_sent_at'],
|
|
'employee_day_3_sent_at' => $state['employee_day_3_sent_at'],
|
|
'admin_day_7_sent_at' => $state['admin_day_7_sent_at'],
|
|
'employee_id' => $state['employee_id'],
|
|
]);
|
|
|
|
return $state;
|
|
}
|
|
}
|
|
|
|
if (!function_exists('timeErrorNotificationsDaysSinceDate')) {
|
|
function timeErrorNotificationsDaysSinceDate(string $date, ?DateTimeImmutable $today = null): int
|
|
{
|
|
$today = $today ?: new DateTimeImmutable('today');
|
|
$reference = new DateTimeImmutable(substr($date, 0, 10));
|
|
|
|
return (int)$reference->diff($today)->days;
|
|
}
|
|
}
|
|
|
|
if (!function_exists('timeErrorNotificationsFormatDateList')) {
|
|
function timeErrorNotificationsFormatDateList(array $errorDates): string
|
|
{
|
|
$labels = array_map(static function (string $date): string {
|
|
return date('d.m.Y', strtotime($date));
|
|
}, $errorDates);
|
|
|
|
return implode(', ', $labels);
|
|
}
|
|
}
|
|
|
|
if (!function_exists('timeErrorNotificationsBuildEmployeeMail')) {
|
|
function timeErrorNotificationsBuildEmployeeMail(array $employee, string $stage): array
|
|
{
|
|
$name = trim($employee['vorname'] . ' ' . $employee['nachname']);
|
|
$dateList = timeErrorNotificationsFormatDateList($employee['error_dates']);
|
|
$subject = ($stage === 'employee_day_3')
|
|
? 'Erneute Erinnerung zu offenen Zeitfehlern'
|
|
: 'Erinnerung zu offenen Zeitfehlern';
|
|
|
|
$body = '<p>Hallo ' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . ',</p>'
|
|
. '<p>in deiner Zeiterfassung gibt es weiterhin offene Buchungsfehler.</p>'
|
|
. '<p><strong>Betroffene Tage:</strong> ' . htmlspecialchars($dateList, ENT_QUOTES, 'UTF-8') . '</p>'
|
|
. '<p>Bitte korrigiere die Eintraege in der Zeiterfassung. Solange die Fehler offen bleiben, wird der Vorgang weiter verfolgt.</p>';
|
|
|
|
return [
|
|
'subject' => $subject,
|
|
'body' => $body,
|
|
];
|
|
}
|
|
}
|
|
|
|
if (!function_exists('timeErrorNotificationsBuildAdminMail')) {
|
|
function timeErrorNotificationsBuildAdminMail(array $employee): array
|
|
{
|
|
$employeeName = trim($employee['vorname'] . ' ' . $employee['nachname']);
|
|
$dateList = timeErrorNotificationsFormatDateList($employee['error_dates']);
|
|
$subject = 'Eskalation: Offene Zeitfehler von ' . $employeeName;
|
|
$body = '<p>Bei einem Mitarbeiter bestehen weiterhin offene Zeitfehler.</p>'
|
|
. '<p><strong>Mitarbeiter:</strong> ' . htmlspecialchars($employeeName, ENT_QUOTES, 'UTF-8') . '<br>'
|
|
. '<strong>E-Mail:</strong> ' . htmlspecialchars($employee['email'], ENT_QUOTES, 'UTF-8') . '<br>'
|
|
. '<strong>Betroffene Tage:</strong> ' . htmlspecialchars($dateList, ENT_QUOTES, 'UTF-8') . '</p>'
|
|
. '<p>Bitte pruefen Sie die Zeiterfassung und stimmen Sie die Korrektur mit dem Mitarbeiter ab.</p>';
|
|
|
|
return [
|
|
'subject' => $subject,
|
|
'body' => $body,
|
|
];
|
|
}
|
|
}
|
|
|
|
if (!function_exists('timeErrorNotificationsProcess')) {
|
|
function timeErrorNotificationsProcess(PDO $pdo, ?DateTimeImmutable $today = null): array
|
|
{
|
|
$today = $today ?: new DateTimeImmutable('today');
|
|
timeErrorNotificationsEnsureTables($pdo);
|
|
|
|
$results = [
|
|
'processed_errors' => 0,
|
|
'affected_employees' => 0,
|
|
'employee_day_1_sent' => 0,
|
|
'employee_day_3_sent' => 0,
|
|
'admin_day_7_sent' => 0,
|
|
'resolved_states_cleared' => 0,
|
|
'skipped_missing_employee_email' => 0,
|
|
'skipped_missing_admin_email' => 0,
|
|
'failed' => [],
|
|
];
|
|
|
|
$entries = timeErrorNotificationsFetchInvalidEntries($pdo);
|
|
$employees = timeErrorNotificationsGroupEntriesByEmployee($entries);
|
|
$adminRecipients = timeErrorNotificationsFetchAdminRecipients($pdo);
|
|
$states = timeErrorNotificationsFetchStateByEmployee($pdo);
|
|
|
|
$results['processed_errors'] = count($entries);
|
|
$results['affected_employees'] = count($employees);
|
|
$results['resolved_states_cleared'] = timeErrorNotificationsClearResolvedStates($pdo, array_keys($employees));
|
|
|
|
foreach ($employees as $employeeId => $employee) {
|
|
$state = timeErrorNotificationsUpsertState($pdo, $employee, $states[$employeeId] ?? null);
|
|
$daysSinceCycleStart = timeErrorNotificationsDaysSinceDate($state['cycle_started_on'], $today);
|
|
$employeeEmail = trim($employee['email']);
|
|
|
|
if ($state['employee_day_1_sent_at'] === null && $daysSinceCycleStart >= 1) {
|
|
if ($employeeEmail === '') {
|
|
$results['skipped_missing_employee_email']++;
|
|
continue;
|
|
}
|
|
|
|
$mail = timeErrorNotificationsBuildEmployeeMail($employee, 'employee_day_1');
|
|
if (timeErrorNotificationsSendMail($pdo, $employeeEmail, $mail['subject'], $mail['body'])) {
|
|
$state = timeErrorNotificationsMarkStageSent($pdo, $state, 'employee_day_1', $employeeEmail);
|
|
$results['employee_day_1_sent']++;
|
|
} else {
|
|
$results['failed'][] = [
|
|
'stage' => 'employee_day_1',
|
|
'employee_id' => $employeeId,
|
|
'recipient_email' => $employeeEmail,
|
|
];
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if ($state['employee_day_3_sent_at'] === null && $daysSinceCycleStart >= 3) {
|
|
if ($employeeEmail === '') {
|
|
$results['skipped_missing_employee_email']++;
|
|
continue;
|
|
}
|
|
|
|
$mail = timeErrorNotificationsBuildEmployeeMail($employee, 'employee_day_3');
|
|
if (timeErrorNotificationsSendMail($pdo, $employeeEmail, $mail['subject'], $mail['body'])) {
|
|
$state = timeErrorNotificationsMarkStageSent($pdo, $state, 'employee_day_3', $employeeEmail);
|
|
$results['employee_day_3_sent']++;
|
|
} else {
|
|
$results['failed'][] = [
|
|
'stage' => 'employee_day_3',
|
|
'employee_id' => $employeeId,
|
|
'recipient_email' => $employeeEmail,
|
|
];
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if ($state['admin_day_7_sent_at'] !== null || empty($state['last_notification_sent_at'])) {
|
|
continue;
|
|
}
|
|
|
|
$daysSinceLastNotification = timeErrorNotificationsDaysSinceDate($state['last_notification_sent_at'], $today);
|
|
if ($daysSinceLastNotification < 7) {
|
|
continue;
|
|
}
|
|
|
|
if (empty($adminRecipients)) {
|
|
$results['skipped_missing_admin_email']++;
|
|
continue;
|
|
}
|
|
|
|
foreach ($adminRecipients as $adminRecipient) {
|
|
$adminEmail = trim((string)$adminRecipient['email']);
|
|
if ($adminEmail === '') {
|
|
$results['skipped_missing_admin_email']++;
|
|
continue;
|
|
}
|
|
|
|
$mail = timeErrorNotificationsBuildAdminMail($employee);
|
|
if (timeErrorNotificationsSendMail($pdo, $adminEmail, $mail['subject'], $mail['body'])) {
|
|
$state = timeErrorNotificationsMarkStageSent($pdo, $state, 'admin_day_7', $adminEmail);
|
|
$results['admin_day_7_sent']++;
|
|
} else {
|
|
$results['failed'][] = [
|
|
'stage' => 'admin_day_7',
|
|
'employee_id' => $employeeId,
|
|
'recipient_email' => $adminEmail,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
}
|