diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..e58d8a8
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,18 @@
+{
+ "sqltools.connections": [
+ {
+ "mysqlOptions": {
+ "authProtocol": "default",
+ "enableSsl": "Disabled"
+ },
+ "ssh": "Disabled",
+ "previewLimit": 50,
+ "server": "mysql2fda.netcup.net",
+ "port": 3306,
+ "driver": "MySQL",
+ "name": "Praxis Creutzburg",
+ "database": "k25330_pracreutz",
+ "username": "k25330_pracreutz"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Praxis Creutzburg.session.sql b/Praxis Creutzburg.session.sql
new file mode 100644
index 0000000..5bf6ad1
--- /dev/null
+++ b/Praxis Creutzburg.session.sql
@@ -0,0 +1,24 @@
+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;
+
+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;
diff --git a/admin/sql/2026-03-30_zeiterfassung_time_error_notifications.sql b/admin/sql/2026-03-30_zeiterfassung_time_error_notifications.sql
new file mode 100644
index 0000000..5bf6ad1
--- /dev/null
+++ b/admin/sql/2026-03-30_zeiterfassung_time_error_notifications.sql
@@ -0,0 +1,24 @@
+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;
+
+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;
diff --git a/zeiterfassung/inc/time_error_notifications.inc.php b/zeiterfassung/inc/time_error_notifications.inc.php
new file mode 100644
index 0000000..dcc0f23
--- /dev/null
+++ b/zeiterfassung/inc/time_error_notifications.inc.php
@@ -0,0 +1,501 @@
+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(["
", "
", "
"], "\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 = '
Hallo ' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . ',
' + . 'in deiner Zeiterfassung gibt es weiterhin offene Buchungsfehler.
' + . 'Betroffene Tage: ' . htmlspecialchars($dateList, ENT_QUOTES, 'UTF-8') . '
' + . 'Bitte korrigiere die Eintraege in der Zeiterfassung. Solange die Fehler offen bleiben, wird der Vorgang weiter verfolgt.
'; + + 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 = 'Bei einem Mitarbeiter bestehen weiterhin offene Zeitfehler.
' + . 'Mitarbeiter: ' . htmlspecialchars($employeeName, ENT_QUOTES, 'UTF-8') . '
'
+ . 'E-Mail: ' . htmlspecialchars($employee['email'], ENT_QUOTES, 'UTF-8') . '
'
+ . 'Betroffene Tage: ' . htmlspecialchars($dateList, ENT_QUOTES, 'UTF-8') . '
Bitte pruefen Sie die Zeiterfassung und stimmen Sie die Korrektur mit dem Mitarbeiter ab.
'; + + 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; + } +} diff --git a/zeiterfassung/sendTimeErrorNotifications.php b/zeiterfassung/sendTimeErrorNotifications.php new file mode 100644 index 0000000..c1d51a9 --- /dev/null +++ b/zeiterfassung/sendTimeErrorNotifications.php @@ -0,0 +1,54 @@ + +