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 = 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'); $scope = strtolower(trim((string)($_GET['scope'] ?? ''))); if ($scope === '') { if ($onlyPersonal) { $scope = 'personal'; } elseif ($public || $publicAll) { $scope = 'team'; } elseif ($isAdmin) { $scope = 'team'; } else { $scope = 'personal'; } } if (!in_array($scope, ['personal', 'team', 'admin_all'], true)) { $scope = 'personal'; } 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 { $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 { $endInclusive = (new DateTime($v['end_date']))->modify('+1 day')->format('Y-m-d'); } catch (Exception $e) { $endInclusive = $v['start_date']; } $backgroundColor = vacationApiTypeColor($absenceType); if ($status === 'abgelehnt') { $backgroundColor = vacationApiStatusColor($status); } $events[] = [ 'id' => 'vac_' . $v['id'], 'title' => $title, 'start' => $v['start_date'], 'end' => $endInclusive, 'allDay' => true, 'backgroundColor' => $backgroundColor, 'borderColor' => $backgroundColor, 'textColor' => '#ffffff', 'extendedProps' => [ '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, ], ]; } 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; ?>