From aae89a45a8909e8b5abd41b9cd2c3e2fdcf3dbdf Mon Sep 17 00:00:00 2001 From: Clemens Creutzburg Date: Wed, 1 Apr 2026 23:59:28 +0200 Subject: [PATCH] Abwesenheitskalender erweitert --- admin/zeiterfassung_hilfe.php | 30 +- zeiterfassung/admin_absence_calendar.php | 233 +++++++++ zeiterfassung/api/vacations.php | 532 +++++++++++++++------ zeiterfassung/approveVacation.php | 213 +++++---- zeiterfassung/header.php | 13 +- zeiterfassung/inc/vacation_absence.inc.php | 102 ++++ zeiterfassung/my_vacations_calendar.php | 153 +++--- zeiterfassung/urlaub_genehmigen.php | 246 +++++----- zeiterfassung/urlaubsantrag.php | 50 +- zeiterfassung/vacations_calendar.php | 140 +++--- zeiterfassung/vacations_calendar_all.php | 44 +- zeiterfassung/vacations_overview.php | 282 +++++++---- 12 files changed, 1416 insertions(+), 622 deletions(-) create mode 100644 zeiterfassung/admin_absence_calendar.php create mode 100644 zeiterfassung/inc/vacation_absence.inc.php diff --git a/admin/zeiterfassung_hilfe.php b/admin/zeiterfassung_hilfe.php index a10d929..1686dcf 100644 --- a/admin/zeiterfassung_hilfe.php +++ b/admin/zeiterfassung_hilfe.php @@ -22,7 +22,7 @@ if (!$user) {
- Kurzüberblick: Die Administration steuert Anfragen, Inhalte, Einstellungen und Sonderbereiche. Die Zeiterfassung steuert Stempelungen, Korrekturen, Urlaube, Fehlbuchungen, PDF-Ausgaben und Benachrichtigungen. + Kurzüberblick: Die Administration steuert Anfragen, Inhalte, Einstellungen und Sonderbereiche. Die Zeiterfassung steuert Stempelungen, Korrekturen, Abwesenheiten, Fehlbuchungen, PDF-Ausgaben und Benachrichtigungen.

1. Administration

@@ -46,7 +46,7 @@ if (!$user) {

Im Bereich Einstellungen werden zentrale Konfigurationen gepflegt, zum Beispiel Benachrichtigungsadressen und technische Grundeinstellungen. Änderungen dort sollten bewusst vorgenommen werden.

Zeiterfassung aus der Admin-Oberfläche

-

Die Zeiterfassung ist aus dem Admin-Menü direkt erreichbar. Dort wechseln Administratoren in den operativen Bereich für Zeiten, Fehlbuchungen, Urlaub und Mitarbeiterverwaltung.

+

Die Zeiterfassung ist aus dem Admin-Menü direkt erreichbar. Dort wechseln Administratoren in den operativen Bereich für Zeiten, Fehlbuchungen, Abwesenheiten und Mitarbeiterverwaltung.


@@ -61,11 +61,11 @@ if (!$user) {

Fehlbuchungen

Der Bereich Fehlbuchungen zeigt unvollständige oder fehlerhafte KOMMEN/GEHEN-Folgen an. Mitarbeiter sehen dort ihre eigenen problematischen Tage und können diese korrigieren.

-

Urlaubsantrag

-

Über Urlaubsantrag wird Urlaub eingereicht. Der Antrag wird anschließend über die Genehmigungsfunktionen der Admins geprüft.

+

Abwesenheitsantrag

+

Über Abwesenheitsantrag werden Urlaube und weitere Abwesenheitsgründe eingereicht. Der Antrag wird anschließend über die Genehmigungsfunktionen der Admins geprüft.

-

Mein Urlaubskalender

-

Im eigenen Urlaubskalender sind persönliche Urlaubszeiten sichtbar. So kann jeder Mitarbeiter seine eigenen Anträge und genehmigten Zeiten prüfen.

+

Mein Abwesenheitskalender

+

Im eigenen Kalender sind die persönlichen Abwesenheiten sichtbar. So kann jeder Mitarbeiter seine eigenen Anträge und genehmigten Zeiten prüfen.

Team-Urlaubskalender

Der Team-Kalender zeigt genehmigte Urlaubseinträge des Teams sowie Betriebsurlaub. Damit lassen sich Überschneidungen und Abwesenheiten leichter erkennen.

@@ -89,11 +89,14 @@ if (!$user) {

Mitarbeiterverwaltung

In der Mitarbeiterverwaltung werden Mitarbeiter angelegt und gepflegt. Dort werden unter anderem E-Mail, Rollen, Zeiterfassungsberechtigung, Admin-Status und Kartenzuordnungen verwaltet.

-

Urlaubsübersicht

-

Die Urlaubsübersicht dient zur Kontrolle aller Urlaubseinträge. Sie ist besonders für Planung und Rückfragen hilfreich.

+

Abwesenheitsübersicht

+

Die Abwesenheitsübersicht dient zur Kontrolle aller Abwesenheitseinträge. Dort werden pro Mitarbeiter die Urlaubstage für den Anspruch sowie die übrigen Abwesenheitsgründe je Jahr zusammengefasst.

-

Urlaubsanträge genehmigen

-

Im Bereich Urlaubsanträge genehmigen prüfen Admins eingereichte Urlaube und können diese annehmen oder ablehnen.

+

Abwesenheiten genehmigen

+

Im Bereich Abwesenheiten genehmigen prüfen Admins eingereichte Abwesenheiten und können diese annehmen oder ablehnen.

+ +

Leitungskalender

+

Der Leitungskalender zeigt alle Abwesenheitstermine über alle Personen hinweg. Damit lassen sich Urlaub, Krankheit, Weiterbildung und weitere Gründe zentral koordinieren.

Betriebsurlaub

Unter Betriebsurlaub werden zentrale Schließzeiten der Praxis gepflegt. Diese Einträge erscheinen im Urlaubskontext und können mit Vertreterinformationen hinterlegt werden.

@@ -118,13 +121,13 @@ if (!$user) {

Eigene fehlerhafte Tage können in der Zeiterfassung angepasst werden. Größere Korrekturen oder Sammelkorrekturen werden durch Admins vorgenommen.

Wo sehe ich meinen Urlaub?

-

Im Bereich Mein Urlaubskalender. Dort sind die eigenen Urlaubszeiträume sichtbar.

+

Im Bereich Mein Abwesenheitskalender. Dort sind die eigenen Abwesenheitszeiträume sichtbar.

Wo sehe ich, wann Kollegen im Urlaub sind?

Im Team-Urlaubskalender. Dort werden freigegebene Urlaube und Betriebsurlaub angezeigt.

Was bedeutet Betriebsurlaub?

-

Betriebsurlaub sind zentrale Schließzeiten der Praxis. Diese werden administrativ gepflegt und im Urlaubskalender sichtbar gemacht.

+

Betriebsurlaub sind zentrale Schließzeiten der Praxis. Diese werden administrativ gepflegt und im Abwesenheitskalender sichtbar gemacht.

An wen wende ich mich bei falschen Zeiten, wenn ich sie nicht selbst korrigieren kann?

Dann sollte ein Admin oder Vorgesetzter informiert werden. Admins können einzelne Tage bearbeiten oder automatisch fehlende Ausstempelungen ergänzen.

@@ -151,6 +154,9 @@ if (!$user) {

Wo pflege ich Vertreterdaten beim Betriebsurlaub?

Im Bereich Betriebsurlaub. Dort werden Beschreibung, Vertretung, Telefonnummer, Adresse und URL gepflegt.

+

Welche Abwesenheiten zählen auf den Urlaubsanspruch?

+

Nur Urlaub zählt auf den Urlaubsanspruch. Krankheit, Berufsschule, Weiterbildung, persönliche Gründe und Sonstiges werden separat ausgewertet.

+

Wo finde ich den schnellsten Rückweg zwischen Admin und Zeiterfassung?

Es gibt direkte Menüeinträge zwischen beiden Bereichen. In der Zeiterfassung führt Zur Admin-Oberfläche zurück in die Verwaltung.

diff --git a/zeiterfassung/admin_absence_calendar.php b/zeiterfassung/admin_absence_calendar.php new file mode 100644 index 0000000..3c30d7a --- /dev/null +++ b/zeiterfassung/admin_absence_calendar.php @@ -0,0 +1,233 @@ +getMessage(); +} + +function adminAbsenceStatusLabel(?string $status): string +{ + $status = trim((string)$status); + if ($status === '' || $status === 'beantragt') { + return 'Beantragt'; + } + if ($status === 'genehmigt') { + return 'Genehmigt'; + } + if ($status === 'abgelehnt') { + return 'Abgelehnt'; + } + return ucfirst($status); +} + +function adminAbsenceColor(string $reason, string $status): string +{ + $reason = vacationAbsenceNormalizeReason($reason); + $status = trim(strtolower($status)); + + $palette = [ + 'urlaub' => '#2f855a', + 'krankheit_mit_atest' => '#c53030', + 'krankheit_ohne_atest' => '#9b2c2c', + 'berufsschule' => '#dd6b20', + 'weiterbildung' => '#6b46c1', + 'persoenliche_gruende' => '#4a5568', + 'sonstiges' => '#718096', + ]; + + $color = $palette[$reason] ?? '#2d3748'; + if ($status === 'beantragt') { + return $color; + } + + return $color; +} + +function adminAbsenceEventTitle(array $row): string +{ + $employee = trim(($row['vorname'] ?? '') . ' ' . ($row['nachname'] ?? '')); + $reasonLabel = vacationAbsenceReasonLabel($row['absence_reason'] ?? 'urlaub'); + $statusLabel = adminAbsenceStatusLabel($row['status'] ?? ''); + if ($employee === '') { + return $reasonLabel . ' (' . $statusLabel . ')'; + } + + if ($statusLabel !== 'Genehmigt') { + return $employee . ' · ' . $reasonLabel . ' (' . $statusLabel . ')'; + } + + return $employee . ' · ' . $reasonLabel; +} + +$events = []; + +$vacStmt = $pdo->prepare(" + SELECT + v.id, + v.user_id, + v.start_date, + v.end_date, + v.days, + v.status, + v.comment_user, + v.absence_reason, + u.vorname, + u.nachname, + u.email + FROM vacations v + JOIN users u ON u.id = v.user_id + WHERE LOWER(TRIM(COALESCE(v.status, ''))) != 'abgelehnt' + ORDER BY v.start_date ASC, v.end_date ASC, u.nachname ASC, u.vorname ASC +"); +$vacStmt->execute(); +$vacations = $vacStmt->fetchAll(PDO::FETCH_ASSOC); + +foreach ($vacations as $row) { + $endInclusive = (new DateTime($row['end_date']))->modify('+1 day')->format('Y-m-d'); + $reason = vacationAbsenceNormalizeReason($row['absence_reason'] ?? 'urlaub'); + $status = trim((string)($row['status'] ?? '')); + $statusLabel = adminAbsenceStatusLabel($status); + + $events[] = [ + 'id' => 'vac_' . $row['id'], + 'title' => adminAbsenceEventTitle($row), + 'start' => $row['start_date'], + 'end' => $endInclusive, + 'allDay' => true, + 'backgroundColor' => adminAbsenceColor($reason, $status), + 'borderColor' => adminAbsenceColor($reason, $status), + 'extendedProps' => [ + 'type' => 'absence', + 'user_id' => $row['user_id'], + 'employee_name' => trim(($row['vorname'] ?? '') . ' ' . ($row['nachname'] ?? '')), + 'email' => $row['email'] ?? '', + 'status' => $status, + 'status_label' => $statusLabel, + 'reason' => $reason, + 'reason_label' => vacationAbsenceReasonLabel($reason), + 'days' => (int)($row['days'] ?? 0), + 'comment' => $row['comment_user'] ?? '', + ], + ]; +} + +$companyStmt = $pdo->prepare(" + SELECT id, start_date, end_date, description + FROM company_holidays + ORDER BY start_date ASC, end_date ASC +"); +$companyStmt->execute(); +$companyHolidays = $companyStmt->fetchAll(PDO::FETCH_ASSOC); + +foreach ($companyHolidays as $row) { + $endInclusive = (new DateTime($row['end_date']))->modify('+1 day')->format('Y-m-d'); + $events[] = [ + 'id' => 'com_' . $row['id'], + 'title' => $row['description'] ?: 'Betriebsurlaub', + 'start' => $row['start_date'], + 'end' => $endInclusive, + 'allDay' => true, + 'backgroundColor' => '#005f73', + 'borderColor' => '#005f73', + 'extendedProps' => [ + 'type' => 'company', + 'description' => $row['description'] ?: 'Betriebsurlaub', + ], + ]; +} + +include 'header.php'; +?> + +
+

Leitungskalender

+ + +
+ + +
+ Hinweis: Dieser Kalender zeigt alle Abwesenheiten aller Personen sowie Betriebsurlaub. +
+ +
+
+
+ Urlaub + Krankheit mit Attest + Krankheit ohne Attest + Berufsschule + Weiterbildung + Persönliche Gründe + Sonstiges + Betriebsurlaub +
+
+ +
+ + + + + + diff --git a/zeiterfassung/api/vacations.php b/zeiterfassung/api/vacations.php index 021ebdf..cf256a4 100644 --- a/zeiterfassung/api/vacations.php +++ b/zeiterfassung/api/vacations.php @@ -2,190 +2,414 @@ session_start(); require_once(__DIR__ . '/../inc/config.inc.php'); require_once(__DIR__ . '/../inc/functions.inc.php'); +require_once(__DIR__ . '/../inc/vacation_absence.inc.php'); ini_set('display_errors', '1'); ini_set('display_startup_errors', '1'); error_reporting(E_ALL); +if (!function_exists('vacationApiTableHasColumn')) { + function vacationApiTableHasColumn(PDO $pdo, string $table, string $column): bool + { + $stmt = $pdo->prepare( + "SELECT COUNT(*) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = :table_name + AND COLUMN_NAME = :column_name" + ); + $stmt->execute([ + 'table_name' => $table, + 'column_name' => $column, + ]); + return (int)$stmt->fetchColumn() > 0; + } +} + +if (!function_exists('vacationApiLower')) { + function vacationApiLower(?string $value): string + { + $value = trim((string)$value); + if ($value === '') { + return ''; + } + + if (function_exists('mb_strtolower')) { + return mb_strtolower($value, 'UTF-8'); + } + + return strtolower($value); + } +} + +if (!function_exists('vacationApiNormalizeAbsenceType')) { + function vacationApiNormalizeAbsenceType(?string $type): string + { + $type = vacationApiLower($type); + if ($type === '') { + return vacationAbsenceDefaultReason(); + } + + $type = strtr($type, [ + 'ä' => 'ae', + 'ö' => 'oe', + 'ü' => 'ue', + 'ß' => 'ss', + ]); + + $type = preg_replace('/[^a-z0-9]+/u', '_', $type); + $type = trim((string)$type, '_'); + + $map = [ + 'urlaub' => 'urlaub', + 'urlaubsantrag' => 'urlaub', + 'krankheit_mit_attest' => 'krankheit_mit_atest', + 'krankheit_mit_atest' => 'krankheit_mit_atest', + 'krank_mit_attest' => 'krankheit_mit_atest', + 'krankheit_ohne_attest' => 'krankheit_ohne_atest', + 'krankheit_ohne_atest' => 'krankheit_ohne_atest', + 'krank_ohne_attest' => 'krankheit_ohne_atest', + 'berufsschule' => 'berufsschule', + 'weiterbildung' => 'weiterbildung', + 'persoenliche_gruende' => 'persoenliche_gruende', + 'persoenliche_gruende_' => 'persoenliche_gruende', + 'persoenliche_gruende_mit' => 'persoenliche_gruende', + 'sonstiges' => 'sonstiges', + ]; + + return vacationAbsenceNormalizeReason($map[$type] ?? $type); + } +} + +if (!function_exists('vacationApiNormalizeStatus')) { + function vacationApiNormalizeStatus(?string $status): string + { + $status = vacationApiLower($status); + if ($status === '') { + return 'beantragt'; + } + + $status = preg_replace('/\s+/u', ' ', $status); + return $status; + } +} + +if (!function_exists('vacationApiTypeLabel')) { + function vacationApiTypeLabel(string $type): string + { + return vacationAbsenceReasonLabel($type); + } +} + +if (!function_exists('vacationApiStatusLabel')) { + function vacationApiStatusLabel(string $status): string + { + $labels = [ + 'genehmigt' => 'Genehmigt', + 'beantragt' => 'Beantragt', + 'abgelehnt' => 'Abgelehnt', + ]; + + return $labels[$status] ?? ucfirst($status); + } +} + +if (!function_exists('vacationApiStatusColor')) { + function vacationApiStatusColor(string $status): string + { + $colors = [ + 'genehmigt' => '#28a745', + 'beantragt' => '#f0ad4e', + 'abgelehnt' => '#6c757d', + ]; + + return $colors[$status] ?? '#17a2b8'; + } +} + +if (!function_exists('vacationApiTypeColor')) { + function vacationApiTypeColor(string $type): string + { + $colors = [ + 'urlaub' => '#2f9e44', + 'krankheit_mit_atest' => '#d9480f', + 'krankheit_ohne_atest' => '#c92a2a', + 'berufsschule' => '#1971c2', + 'weiterbildung' => '#5f3dc4', + 'persoenliche_gruende' => '#e67700', + 'sonstiges' => '#495057', + ]; + + return $colors[$type] ?? '#0d6efd'; + } +} + +if (!function_exists('vacationApiRequestedTypes')) { + function vacationApiRequestedTypes(?string $rawTypes, string $scope): ?array + { + $rawTypes = trim((string)$rawTypes); + if ($rawTypes === '') { + if ($scope === 'team') { + return ['urlaub']; + } + + return null; + } + + $normalized = vacationApiLower($rawTypes); + if (in_array($normalized, ['all', '*'], true)) { + return null; + } + + $types = []; + foreach (preg_split('/\s*,\s*/', $rawTypes) as $rawType) { + $normalizedType = vacationApiNormalizeAbsenceType($rawType); + if ($normalizedType !== '') { + $types[$normalizedType] = true; + } + } + + $types = array_keys($types); + sort($types); + + return $types; + } +} + +if (!function_exists('vacationApiStatusAllowed')) { + function vacationApiStatusAllowed(string $status, string $mode): bool + { + $mode = vacationApiLower($mode); + $status = vacationApiNormalizeStatus($status); + + switch ($mode) { + case 'approved': + return $status === 'genehmigt'; + case 'open': + return $status === 'beantragt' || $status === ''; + case 'active': + return $status === 'genehmigt' || $status === 'beantragt' || $status === ''; + case 'rejected': + return $status === 'abgelehnt'; + case 'all': + default: + return true; + } + } +} + $user = check_user(); $isAdmin = is_admin_user(); -$start = $_GET['start'] ?? null; -$end = $_GET['end'] ?? null; +$start = trim((string)($_GET['start'] ?? '')); +$end = trim((string)($_GET['end'] ?? '')); + +if ($start === '' || $end === '') { + http_response_code(400); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode(['error' => 'start and end required']); + exit; +} + $onlyApproved = isset($_GET['only_approved']) && ($_GET['only_approved'] == '1' || $_GET['only_approved'] === 'true'); $public = isset($_GET['public']) && ($_GET['public'] == '1' || $_GET['public'] === 'true'); $includeRejected = isset($_GET['include_rejected']) && ($_GET['include_rejected'] == '1' || $_GET['include_rejected'] === 'true'); $onlyPersonal = isset($_GET['only_personal']) && ($_GET['only_personal'] == '1' || $_GET['only_personal'] === 'true'); $publicAll = isset($_GET['public_all']) && ($_GET['public_all'] == '1' || $_GET['public_all'] === 'true'); -if (!$start || !$end) { - http_response_code(400); - echo json_encode(['error' => 'start and end required']); - exit; +$scope = strtolower(trim((string)($_GET['scope'] ?? ''))); +if ($scope === '') { + if ($onlyPersonal) { + $scope = 'personal'; + } elseif ($public || $publicAll) { + $scope = 'team'; + } elseif ($isAdmin) { + $scope = 'team'; + } else { + $scope = 'personal'; + } } -$events = []; -try { - $branch = 'unknown'; - $debugMode = isset($_GET['debug']) && ($_GET['debug'] == '1' || $_GET['debug'] === 'true'); - $showEmployeeNames = $isAdmin || $public; +if (!in_array($scope, ['personal', 'team', 'admin_all'], true)) { + $scope = 'personal'; +} - if ($onlyPersonal) { - $branch = 'onlyPersonal'; - if ($onlyApproved) { - $branch = 'onlyPersonal_onlyApproved'; - $stmt = $pdo->prepare("SELECT v.*, u.vorname, u.nachname FROM vacations v JOIN users u ON v.user_id = u.id WHERE v.user_id = ? AND v.start_date <= ? AND v.end_date >= ? AND LOWER(TRIM(v.status)) = 'genehmigt' ORDER BY v.start_date"); - $stmt->execute([$_SESSION['userid'], $end, $start]); - } else { - if ($includeRejected) { - $branch = 'onlyPersonal_includeRejected'; - $stmt = $pdo->prepare("SELECT v.*, u.vorname, u.nachname FROM vacations v JOIN users u ON v.user_id = u.id WHERE v.user_id = ? AND v.start_date <= ? AND v.end_date >= ? ORDER BY v.start_date"); - $stmt->execute([$_SESSION['userid'], $end, $start]); - } else { - $branch = 'onlyPersonal_excludeRejected'; - $stmt = $pdo->prepare("SELECT v.*, u.vorname, u.nachname FROM vacations v JOIN users u ON v.user_id = u.id WHERE v.user_id = ? AND v.start_date <= ? AND v.end_date >= ? AND (v.status IS NULL OR LOWER(TRIM(v.status)) != 'abgelehnt') ORDER BY v.start_date"); - $stmt->execute([$_SESSION['userid'], $end, $start]); - } - } - } elseif ($isAdmin) { - $branch = 'admin'; - if ($onlyApproved) { - $branch = 'admin_onlyApproved'; - $stmt = $pdo->prepare("SELECT v.*, u.vorname, u.nachname FROM vacations v JOIN users u ON v.user_id = u.id WHERE v.start_date <= ? AND v.end_date >= ? AND LOWER(TRIM(v.status)) = 'genehmigt' ORDER BY v.start_date"); - $stmt->execute([$end, $start]); - } else { - if ($includeRejected) { - $stmt = $pdo->prepare("SELECT v.*, u.vorname, u.nachname FROM vacations v JOIN users u ON v.user_id = u.id WHERE v.start_date <= ? AND v.end_date >= ? ORDER BY v.start_date"); - $stmt->execute([$end, $start]); - } else { - $stmt = $pdo->prepare("SELECT v.*, u.vorname, u.nachname FROM vacations v JOIN users u ON v.user_id = u.id WHERE v.start_date <= ? AND v.end_date >= ? AND (v.status IS NULL OR LOWER(TRIM(v.status)) IN ('genehmigt','beantragt')) ORDER BY v.start_date"); - $stmt->execute([$end, $start]); - } - } +if ($scope === 'admin_all' && !$isAdmin) { + $scope = 'personal'; +} + +$statusMode = strtolower(trim((string)($_GET['status_filter'] ?? ''))); +if ($statusMode === '') { + if ($onlyApproved) { + $statusMode = 'approved'; + } elseif ($includeRejected) { + $statusMode = 'all'; + } elseif ($scope === 'personal' || $scope === 'admin_all') { + $statusMode = 'all'; } else { - $branch = 'public_or_regular'; - if ($public && $onlyApproved) { - $branch = 'public_onlyApproved'; - $stmt = $pdo->prepare("SELECT v.*, u.vorname, u.nachname FROM vacations v JOIN users u ON v.user_id = u.id WHERE v.start_date <= ? AND v.end_date >= ? AND LOWER(TRIM(v.status)) = 'genehmigt' ORDER BY v.start_date"); - $stmt->execute([$end, $start]); - } elseif ($public && $publicAll) { - $branch = 'public_publicAll'; - $stmt = $pdo->prepare("SELECT v.*, u.vorname, u.nachname FROM vacations v JOIN users u ON v.user_id = u.id WHERE v.start_date <= ? AND v.end_date >= ? AND (v.status IS NULL OR LOWER(TRIM(v.status)) IN ('genehmigt','beantragt')) ORDER BY v.start_date"); - $stmt->execute([$end, $start]); - } else { - if ($onlyApproved) { - $stmt = $pdo->prepare("SELECT v.*, u.vorname, u.nachname FROM vacations v JOIN users u ON v.user_id = u.id WHERE v.user_id = ? AND v.start_date <= ? AND v.end_date >= ? AND LOWER(TRIM(v.status)) = 'genehmigt' ORDER BY v.start_date"); - $stmt->execute([$_SESSION['userid'], $end, $start]); - } else { - if ($includeRejected) { - $branch = 'regular_includeRejected'; - $stmt = $pdo->prepare("SELECT v.*, u.vorname, u.nachname FROM vacations v JOIN users u ON v.user_id = u.id WHERE v.user_id = ? AND v.start_date <= ? AND v.end_date >= ? ORDER BY v.start_date"); - $stmt->execute([$_SESSION['userid'], $end, $start]); - } else { - $branch = 'regular_excludeRejected'; - $stmt = $pdo->prepare("SELECT v.*, u.vorname, u.nachname FROM vacations v JOIN users u ON v.user_id = u.id WHERE v.user_id = ? AND v.start_date <= ? AND v.end_date >= ? AND (v.status IS NULL OR LOWER(TRIM(v.status)) != 'abgelehnt') ORDER BY v.start_date"); - $stmt->execute([$_SESSION['userid'], $end, $start]); - } - } - } + $statusMode = 'active'; + } +} + +$includeCompany = !isset($_GET['include_company']) || $_GET['include_company'] === '1' || $_GET['include_company'] === 'true'; +$requestedTypes = vacationApiRequestedTypes($_GET['absence_types'] ?? null, $scope); + +$hasAbsenceReasonColumn = vacationApiTableHasColumn($pdo, 'vacations', 'absence_reason'); +$absenceTypeSelect = $hasAbsenceReasonColumn ? 'v.absence_reason' : "'" . vacationAbsenceDefaultReason() . "'"; + +$selectColumns = " + SELECT + v.id, + v.user_id, + v.start_date, + v.end_date, + v.days, + v.status, + v.comment_user, + u.vorname, + u.nachname, + {$absenceTypeSelect} AS absence_type + FROM vacations v + JOIN users u ON v.user_id = u.id + WHERE v.start_date <= ? + AND v.end_date >= ? +"; + +$params = [$end, $start]; + +if ($scope === 'personal') { + $selectColumns .= " AND v.user_id = ?"; + $params[] = $_SESSION['userid']; +} elseif ($scope === 'team' || $scope === 'admin_all') { + // No further restriction here. Scope filters are applied in PHP so + // the query remains flexible for future admin and team views. +} + +$selectColumns .= " ORDER BY v.start_date ASC, v.id ASC"; + +$stmt = $pdo->prepare($selectColumns); +$stmt->execute($params); +$vacations = $stmt->fetchAll(PDO::FETCH_ASSOC); + +$events = []; + +foreach ($vacations as $v) { + $absenceType = vacationApiNormalizeAbsenceType($v['absence_type'] ?? ''); + $status = vacationApiNormalizeStatus($v['status'] ?? ''); + + if ($scope === 'team' && $absenceType !== 'urlaub') { + continue; + } + + if ($requestedTypes !== null && !in_array($absenceType, $requestedTypes, true)) { + continue; + } + + if (!vacationApiStatusAllowed($status, $statusMode)) { + continue; + } + + $employeeName = trim((string)($v['vorname'] ?? '') . ' ' . (string)($v['nachname'] ?? '')); + $typeLabel = vacationApiTypeLabel($absenceType); + $statusLabel = vacationApiStatusLabel($status); + + if ($scope === 'personal') { + $title = $typeLabel; + } elseif ($employeeName !== '') { + $title = $employeeName . ' - ' . $typeLabel; + } else { + $title = $typeLabel; + } + + if ($status !== '' && $status !== 'genehmigt') { + $title .= ' (' . $statusLabel . ')'; } try { - $vacations = $stmt->fetchAll(PDO::FETCH_ASSOC); - - if ($debugMode) { - $rawStatuses = array_map(function($r){ return $r['status'] ?? null; }, $vacations); - $meta = [ - 'branch' => $branch, - 'count' => count($vacations), - 'raw_statuses' => $rawStatuses - ]; - } - - foreach ($vacations as $v) { - if (isset($v['status'])) { - $normalized = preg_replace('/\s+/u', ' ', $v['status']); - $status = mb_strtolower(trim($normalized)); - } else { - $status = ''; - } - - if (!$isAdmin && !$includeRejected && mb_stripos($status, 'abgelehnt') !== false) { - continue; - } - - $isApproved = (mb_stripos($status, 'genehmigt') !== false); - $employeeName = trim(($v['vorname'] ?? '') . ' ' . ($v['nachname'] ?? '')); - if ($showEmployeeNames && $employeeName !== '') { - $title = $employeeName; - if ($v['status'] !== null && $v['status'] !== '') { - $title .= ' (' . $v['status'] . ')'; - } - } elseif ($isApproved) { - $title = 'Urlaub'; - } else { - $title = 'Urlaubsantrag'; - } - - try { - $endInclusive = (new DateTime($v['end_date']))->modify('+1 day')->format('Y-m-d'); - } catch (Exception $e) { - $endInclusive = $v['start_date']; - } - - $events[] = [ - 'id' => 'vac_' . $v['id'], - 'title' => $title, - 'start' => $v['start_date'], - 'end' => $endInclusive, - 'allDay' => true, - 'color' => ($isApproved) ? '#28a745' : '#ffc107', - 'extendedProps' => [ - 'type' => 'user', - 'user_id' => $v['user_id'], - 'employee_name' => $employeeName, - 'status' => $v['status'], - 'comment' => $v['comment_user'] ?? '' - ] - ]; - } - } catch (Exception $ex) { - header('Content-Type: application/json; charset=utf-8'); - $payload = ['error' => $ex->getMessage(), 'branch' => $branch, 'trace' => $ex->getTraceAsString()]; - echo json_encode($payload); - exit; + $endInclusive = (new DateTime($v['end_date']))->modify('+1 day')->format('Y-m-d'); + } catch (Exception $e) { + $endInclusive = $v['start_date']; } -} catch (Exception $ex) { - header('Content-Type: application/json; charset=utf-8'); - $payload = ['error' => $ex->getMessage(), 'branch' => $branch, 'trace' => $ex->getTraceAsString()]; - echo json_encode($payload); - exit; -} -$stmt = $pdo->prepare("SELECT * FROM company_holidays WHERE start_date <= ? AND end_date >= ? ORDER BY start_date"); -$stmt->execute([$end, $start]); -$holidays = $stmt->fetchAll(PDO::FETCH_ASSOC); + $backgroundColor = vacationApiTypeColor($absenceType); + if ($status === 'abgelehnt') { + $backgroundColor = vacationApiStatusColor($status); + } -foreach ($holidays as $h) { - $endInclusive = (new DateTime($h['end_date']))->modify('+1 day')->format('Y-m-d'); $events[] = [ - 'id' => 'com_' . $h['id'], - 'title' => $h['description'] ?: 'Betriebsurlaub', - 'start' => $h['start_date'], + 'id' => 'vac_' . $v['id'], + 'title' => $title, + 'start' => $v['start_date'], 'end' => $endInclusive, 'allDay' => true, - 'color' => '#007bff', + 'backgroundColor' => $backgroundColor, + 'borderColor' => $backgroundColor, + 'textColor' => '#ffffff', 'extendedProps' => [ - 'type' => 'company', - 'description' => $h['description'] - ] + 'type' => 'user', + 'user_id' => $v['user_id'], + 'employee_name' => $employeeName, + 'absence_type' => $absenceType, + 'absence_label' => $typeLabel, + 'status' => $v['status'], + 'status_label' => $statusLabel, + 'comment' => $v['comment_user'] ?? '', + 'scope' => $scope, + ], ]; } -header('Content-Type: application/json; charset=utf-8'); -if ($debugMode) { - echo json_encode(['events' => $events, 'meta' => $meta]); -} else { - echo json_encode($events); +if ($includeCompany) { + $stmt = $pdo->prepare("SELECT id, start_date, end_date, description, vertretung, vertretertelefon, vertreteradresse, vertreterurl FROM company_holidays WHERE start_date <= ? AND end_date >= ? ORDER BY start_date"); + $stmt->execute([$end, $start]); + $holidays = $stmt->fetchAll(PDO::FETCH_ASSOC); + + foreach ($holidays as $h) { + try { + $endInclusive = (new DateTime($h['end_date']))->modify('+1 day')->format('Y-m-d'); + } catch (Exception $e) { + $endInclusive = $h['start_date']; + } + + $description = trim((string)($h['description'] ?? '')); + $title = $description !== '' ? $description : 'Betriebsurlaub'; + + $events[] = [ + 'id' => 'com_' . $h['id'], + 'title' => $title, + 'start' => $h['start_date'], + 'end' => $endInclusive, + 'allDay' => true, + 'backgroundColor' => '#0b7285', + 'borderColor' => '#0b7285', + 'textColor' => '#ffffff', + 'extendedProps' => [ + 'type' => 'company', + 'description' => $description, + 'vertretung' => $h['vertretung'] ?? '', + 'vertretertelefon' => $h['vertretertelefon'] ?? '', + 'vertreteradresse' => $h['vertreteradresse'] ?? '', + 'vertreterurl' => $h['vertreterurl'] ?? '', + ], + ]; + } } + +header('Content-Type: application/json; charset=utf-8'); + +$json = json_encode( + $events, + JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE +); + +if ($json === false) { + http_response_code(500); + echo json_encode([ + 'error' => 'Kalenderdaten konnten nicht kodiert werden.', + 'json_error' => json_last_error_msg(), + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} + +echo $json; ?> diff --git a/zeiterfassung/approveVacation.php b/zeiterfassung/approveVacation.php index 933cc3a..78b1ed7 100644 --- a/zeiterfassung/approveVacation.php +++ b/zeiterfassung/approveVacation.php @@ -1,105 +1,108 @@ -prepare("UPDATE vacations SET status = 'genehmigt', approved_by = ?, approved_at = NOW() WHERE id = ?"); - $stmt->execute([$_SESSION['userid'], $id]); - } elseif ($action === 'reject') { - $stmt = $pdo->prepare("UPDATE vacations SET status = 'abgelehnt', approved_by = ?, approved_at = NOW() WHERE id = ?"); - $stmt->execute([$_SESSION['userid'], $id]); - } elseif ($action === 'delete' && is_admin_user()) { - $del = $pdo->prepare("DELETE FROM vacations WHERE id = ?"); - $del->execute([$id]); - } - - header('Location: approveVacation.php'); - exit(); -} - -include 'header.php'; - -// List pending and recent requests -$stmt = $pdo->prepare("SELECT v.*, u.vorname, u.nachname, u.email FROM vacations v JOIN users u ON v.user_id = u.id ORDER BY v.created_at DESC"); -$stmt->execute(); -$requests = $stmt->fetchAll(); - -?> - -
-

Urlaubsanträge - Genehmigung

- - - - - - - - - - - - - - - - - - - - - - - - - - -
MitarbeiterVonBisTageKommentarStatusAktion
- Beantragt'; - } elseif ($r['status'] === 'genehmigt') { - echo 'Genehmigt'; - } else { - echo 'Abgelehnt'; - } - ?> - - -
- - - -
- - - -
- - - -
- -
- - - -
-
- -
- - +prepare("UPDATE vacations SET status = 'genehmigt', approved_by = ?, approved_at = NOW() WHERE id = ?"); + $stmt->execute([$_SESSION['userid'], $id]); + } elseif ($action === 'reject') { + $stmt = $pdo->prepare("UPDATE vacations SET status = 'abgelehnt', approved_by = ?, approved_at = NOW() WHERE id = ?"); + $stmt->execute([$_SESSION['userid'], $id]); + } elseif ($action === 'delete' && is_admin_user()) { + $del = $pdo->prepare("DELETE FROM vacations WHERE id = ?"); + $del->execute([$id]); + } + + header('Location: approveVacation.php'); + exit(); +} + +include 'header.php'; + +// List pending and recent requests +$stmt = $pdo->prepare("SELECT v.*, u.vorname, u.nachname, u.email FROM vacations v JOIN users u ON v.user_id = u.id ORDER BY v.created_at DESC"); +$stmt->execute(); +$requests = $stmt->fetchAll(); + +?> + +
+

Abwesenheitsanträge - Genehmigung

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MitarbeiterGrundVonBisTageKommentarStatusAktion
+ Beantragt'; + } elseif ($r['status'] === 'genehmigt') { + echo 'Genehmigt'; + } else { + echo 'Abgelehnt'; + } + ?> + + +
+ + + +
+ + + +
+ + + +
+ +
+ + + +
+
+ +
+ + diff --git a/zeiterfassung/header.php b/zeiterfassung/header.php index 6141864..1f2e080 100644 --- a/zeiterfassung/header.php +++ b/zeiterfassung/header.php @@ -52,16 +52,17 @@ if (!isset($user)) {