Abwesenheitskalender erweitert

This commit is contained in:
2026-04-01 23:59:28 +02:00
parent 6360af272a
commit aae89a45a8
12 changed files with 1416 additions and 622 deletions
+378 -154
View File
@@ -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;
?>