Abwesenheitskalender erweitert
This commit is contained in:
@@ -22,7 +22,7 @@ if (!$user) {
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>Kurzüberblick:</strong> Die Administration steuert Anfragen, Inhalte, Einstellungen und Sonderbereiche. Die Zeiterfassung steuert Stempelungen, Korrekturen, Urlaube, Fehlbuchungen, PDF-Ausgaben und Benachrichtigungen.
|
||||
<strong>Kurzüberblick:</strong> Die Administration steuert Anfragen, Inhalte, Einstellungen und Sonderbereiche. Die Zeiterfassung steuert Stempelungen, Korrekturen, Abwesenheiten, Fehlbuchungen, PDF-Ausgaben und Benachrichtigungen.
|
||||
</div>
|
||||
|
||||
<h3>1. Administration</h3>
|
||||
@@ -46,7 +46,7 @@ if (!$user) {
|
||||
<p>Im Bereich <strong>Einstellungen</strong> werden zentrale Konfigurationen gepflegt, zum Beispiel Benachrichtigungsadressen und technische Grundeinstellungen. Änderungen dort sollten bewusst vorgenommen werden.</p>
|
||||
|
||||
<h4>Zeiterfassung aus der Admin-Oberfläche</h4>
|
||||
<p>Die Zeiterfassung ist aus dem Admin-Menü direkt erreichbar. Dort wechseln Administratoren in den operativen Bereich für Zeiten, Fehlbuchungen, Urlaub und Mitarbeiterverwaltung.</p>
|
||||
<p>Die Zeiterfassung ist aus dem Admin-Menü direkt erreichbar. Dort wechseln Administratoren in den operativen Bereich für Zeiten, Fehlbuchungen, Abwesenheiten und Mitarbeiterverwaltung.</p>
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -61,11 +61,11 @@ if (!$user) {
|
||||
<h4>Fehlbuchungen</h4>
|
||||
<p>Der Bereich <strong>Fehlbuchungen</strong> zeigt unvollständige oder fehlerhafte KOMMEN/GEHEN-Folgen an. Mitarbeiter sehen dort ihre eigenen problematischen Tage und können diese korrigieren.</p>
|
||||
|
||||
<h4>Urlaubsantrag</h4>
|
||||
<p>Über <strong>Urlaubsantrag</strong> wird Urlaub eingereicht. Der Antrag wird anschließend über die Genehmigungsfunktionen der Admins geprüft.</p>
|
||||
<h4>Abwesenheitsantrag</h4>
|
||||
<p>Über <strong>Abwesenheitsantrag</strong> werden Urlaube und weitere Abwesenheitsgründe eingereicht. Der Antrag wird anschließend über die Genehmigungsfunktionen der Admins geprüft.</p>
|
||||
|
||||
<h4>Mein Urlaubskalender</h4>
|
||||
<p>Im eigenen Urlaubskalender sind persönliche Urlaubszeiten sichtbar. So kann jeder Mitarbeiter seine eigenen Anträge und genehmigten Zeiten prüfen.</p>
|
||||
<h4>Mein Abwesenheitskalender</h4>
|
||||
<p>Im eigenen Kalender sind die persönlichen Abwesenheiten sichtbar. So kann jeder Mitarbeiter seine eigenen Anträge und genehmigten Zeiten prüfen.</p>
|
||||
|
||||
<h4>Team-Urlaubskalender</h4>
|
||||
<p>Der Team-Kalender zeigt genehmigte Urlaubseinträge des Teams sowie Betriebsurlaub. Damit lassen sich Überschneidungen und Abwesenheiten leichter erkennen.</p>
|
||||
@@ -89,11 +89,14 @@ if (!$user) {
|
||||
<h4>Mitarbeiterverwaltung</h4>
|
||||
<p>In der Mitarbeiterverwaltung werden Mitarbeiter angelegt und gepflegt. Dort werden unter anderem E-Mail, Rollen, Zeiterfassungsberechtigung, Admin-Status und Kartenzuordnungen verwaltet.</p>
|
||||
|
||||
<h4>Urlaubsübersicht</h4>
|
||||
<p>Die Urlaubsübersicht dient zur Kontrolle aller Urlaubseinträge. Sie ist besonders für Planung und Rückfragen hilfreich.</p>
|
||||
<h4>Abwesenheitsübersicht</h4>
|
||||
<p>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.</p>
|
||||
|
||||
<h4>Urlaubsanträge genehmigen</h4>
|
||||
<p>Im Bereich <strong>Urlaubsanträge genehmigen</strong> prüfen Admins eingereichte Urlaube und können diese annehmen oder ablehnen.</p>
|
||||
<h4>Abwesenheiten genehmigen</h4>
|
||||
<p>Im Bereich <strong>Abwesenheiten genehmigen</strong> prüfen Admins eingereichte Abwesenheiten und können diese annehmen oder ablehnen.</p>
|
||||
|
||||
<h4>Leitungskalender</h4>
|
||||
<p>Der Leitungskalender zeigt alle Abwesenheitstermine über alle Personen hinweg. Damit lassen sich Urlaub, Krankheit, Weiterbildung und weitere Gründe zentral koordinieren.</p>
|
||||
|
||||
<h4>Betriebsurlaub</h4>
|
||||
<p>Unter <strong>Betriebsurlaub</strong> werden zentrale Schließzeiten der Praxis gepflegt. Diese Einträge erscheinen im Urlaubskontext und können mit Vertreterinformationen hinterlegt werden.</p>
|
||||
@@ -118,13 +121,13 @@ if (!$user) {
|
||||
<p>Eigene fehlerhafte Tage können in der Zeiterfassung angepasst werden. Größere Korrekturen oder Sammelkorrekturen werden durch Admins vorgenommen.</p>
|
||||
|
||||
<h4>Wo sehe ich meinen Urlaub?</h4>
|
||||
<p>Im Bereich <strong>Mein Urlaubskalender</strong>. Dort sind die eigenen Urlaubszeiträume sichtbar.</p>
|
||||
<p>Im Bereich <strong>Mein Abwesenheitskalender</strong>. Dort sind die eigenen Abwesenheitszeiträume sichtbar.</p>
|
||||
|
||||
<h4>Wo sehe ich, wann Kollegen im Urlaub sind?</h4>
|
||||
<p>Im <strong>Team-Urlaubskalender</strong>. Dort werden freigegebene Urlaube und Betriebsurlaub angezeigt.</p>
|
||||
|
||||
<h4>Was bedeutet Betriebsurlaub?</h4>
|
||||
<p>Betriebsurlaub sind zentrale Schließzeiten der Praxis. Diese werden administrativ gepflegt und im Urlaubskalender sichtbar gemacht.</p>
|
||||
<p>Betriebsurlaub sind zentrale Schließzeiten der Praxis. Diese werden administrativ gepflegt und im Abwesenheitskalender sichtbar gemacht.</p>
|
||||
|
||||
<h4>An wen wende ich mich bei falschen Zeiten, wenn ich sie nicht selbst korrigieren kann?</h4>
|
||||
<p>Dann sollte ein Admin oder Vorgesetzter informiert werden. Admins können einzelne Tage bearbeiten oder automatisch fehlende Ausstempelungen ergänzen.</p>
|
||||
@@ -151,6 +154,9 @@ if (!$user) {
|
||||
<h4>Wo pflege ich Vertreterdaten beim Betriebsurlaub?</h4>
|
||||
<p>Im Bereich <strong>Betriebsurlaub</strong>. Dort werden Beschreibung, Vertretung, Telefonnummer, Adresse und URL gepflegt.</p>
|
||||
|
||||
<h4>Welche Abwesenheiten zählen auf den Urlaubsanspruch?</h4>
|
||||
<p>Nur <strong>Urlaub</strong> zählt auf den Urlaubsanspruch. Krankheit, Berufsschule, Weiterbildung, persönliche Gründe und Sonstiges werden separat ausgewertet.</p>
|
||||
|
||||
<h4>Wo finde ich den schnellsten Rückweg zwischen Admin und Zeiterfassung?</h4>
|
||||
<p>Es gibt direkte Menüeinträge zwischen beiden Bereichen. In der Zeiterfassung führt <strong>Zur Admin-Oberfläche</strong> zurück in die Verwaltung.</p>
|
||||
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once('inc/config.inc.php');
|
||||
require_once('inc/functions.inc.php');
|
||||
require_once __DIR__ . '/inc/vacation_absence.inc.php';
|
||||
|
||||
$user = check_user();
|
||||
if (!is_admin_user()) {
|
||||
die('Zugriff verweigert. Nur Chefs dürfen den Leitungskalender sehen.');
|
||||
}
|
||||
|
||||
$schemaWarning = '';
|
||||
try {
|
||||
vacationAbsenceEnsureSchema($pdo);
|
||||
} catch (Throwable $e) {
|
||||
$schemaWarning = 'Das Abwesenheitsschema konnte nicht automatisch aktualisiert werden: ' . $e->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';
|
||||
?>
|
||||
|
||||
<div class="container">
|
||||
<h2>Leitungskalender</h2>
|
||||
|
||||
<?php if (!empty($schemaWarning)): ?>
|
||||
<div class="alert alert-warning"><?php echo htmlspecialchars($schemaWarning); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>Hinweis:</strong> Dieser Kalender zeigt alle Abwesenheiten aller Personen sowie Betriebsurlaub.
|
||||
</div>
|
||||
|
||||
<div id="calendar"></div>
|
||||
<br>
|
||||
<div class="d-flex flex-wrap" style="gap: 0.5rem;">
|
||||
<span class="badge badge-success">Urlaub</span>
|
||||
<span class="badge badge-danger">Krankheit mit Attest</span>
|
||||
<span class="badge badge-dark">Krankheit ohne Attest</span>
|
||||
<span class="badge badge-warning">Berufsschule</span>
|
||||
<span class="badge badge-primary">Weiterbildung</span>
|
||||
<span class="badge badge-secondary">Persönliche Gründe</span>
|
||||
<span class="badge badge-light">Sonstiges</span>
|
||||
<span class="badge badge-info">Betriebsurlaub</span>
|
||||
</div>
|
||||
<br>
|
||||
<div id="eventDetails" style="display:none;">
|
||||
<h4>Details</h4>
|
||||
<div id="detailsContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<link href='https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.css' rel='stylesheet' />
|
||||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.js'></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var calendarEl = document.getElementById('calendar');
|
||||
var events = <?php echo json_encode($events, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
|
||||
|
||||
function formatAllDayRange(start, end) {
|
||||
var startDate = new Date(start);
|
||||
var endDate = end ? new Date(end) : null;
|
||||
var startLabel = startDate.toLocaleDateString('de-DE');
|
||||
if (!endDate) {
|
||||
return startLabel;
|
||||
}
|
||||
|
||||
var inclusiveEnd = new Date(endDate.getTime() - 24 * 60 * 60 * 1000);
|
||||
return startLabel + ' - ' + inclusiveEnd.toLocaleDateString('de-DE');
|
||||
}
|
||||
|
||||
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
initialView: 'dayGridMonth',
|
||||
firstDay: 1,
|
||||
height: 700,
|
||||
events: events,
|
||||
eventClick: function(info) {
|
||||
var ev = info.event;
|
||||
var props = ev.extendedProps || {};
|
||||
var html = '<strong>' + ev.title + '</strong><br>' + formatAllDayRange(ev.start, ev.end) + '<br>';
|
||||
|
||||
if (props.type === 'absence') {
|
||||
html += 'Mitarbeiter: ' + (props.employee_name || '') + '<br>';
|
||||
html += 'Grund: ' + (props.reason_label || '') + '<br>';
|
||||
html += 'Status: ' + (props.status_label || '') + '<br>';
|
||||
html += 'Tage: ' + (props.days || 0) + '<br>';
|
||||
if (props.comment) {
|
||||
html += 'Kommentar: ' + props.comment + '<br>';
|
||||
}
|
||||
} else if (props.type === 'company') {
|
||||
html += 'Beschreibung: ' + (props.description || 'Betriebsurlaub') + '<br>';
|
||||
}
|
||||
|
||||
document.getElementById('detailsContent').innerHTML = html;
|
||||
document.getElementById('eventDetails').style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include 'footer.php'; ?>
|
||||
+378
-154
@@ -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;
|
||||
?>
|
||||
|
||||
+108
-105
@@ -1,105 +1,108 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once("inc/config.inc.php");
|
||||
require_once("inc/functions.inc.php");
|
||||
|
||||
$user = check_user();
|
||||
if (!is_admin_user()) {
|
||||
die('Zugriff verweigert. Nur Chefs dürfen Anträge genehmigen.');
|
||||
}
|
||||
|
||||
// Handle approve/reject actions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['id']) && isset($_POST['action'])) {
|
||||
$id = (int)$_POST['id'];
|
||||
$action = $_POST['action'];
|
||||
|
||||
if ($action === 'approve') {
|
||||
$stmt = $pdo->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();
|
||||
|
||||
?>
|
||||
|
||||
<div class="container">
|
||||
<h2>Urlaubsanträge - Genehmigung</h2>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mitarbeiter</th>
|
||||
<th>Von</th>
|
||||
<th>Bis</th>
|
||||
<th>Tage</th>
|
||||
<th>Kommentar</th>
|
||||
<th>Status</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($requests as $r): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($r['vorname'] . ' ' . $r['nachname']); ?></td>
|
||||
<td><?php echo $r['start_date']; ?></td>
|
||||
<td><?php echo $r['end_date']; ?></td>
|
||||
<td><?php echo $r['days']; ?></td>
|
||||
<td><?php echo htmlspecialchars($r['comment_user']); ?></td>
|
||||
<td>
|
||||
<?php
|
||||
if ($r['status'] === 'beantragt' || $r['status'] === null) {
|
||||
echo '<span class="badge badge-warning">Beantragt</span>';
|
||||
} elseif ($r['status'] === 'genehmigt') {
|
||||
echo '<span class="badge badge-success">Genehmigt</span>';
|
||||
} else {
|
||||
echo '<span class="badge badge-danger">Abgelehnt</span>';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($r['status'] !== 'genehmigt'): ?>
|
||||
<form method="post" style="display:inline-block; margin-right:6px;">
|
||||
<input type="hidden" name="id" value="<?php echo $r['id']; ?>">
|
||||
<input type="hidden" name="action" value="approve">
|
||||
<button class="btn btn-sm btn-success" type="submit">Genehmigen</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($r['status'] !== 'abgelehnt'): ?>
|
||||
<form method="post" style="display:inline-block;">
|
||||
<input type="hidden" name="id" value="<?php echo $r['id']; ?>">
|
||||
<input type="hidden" name="action" value="reject">
|
||||
<button class="btn btn-sm btn-danger" type="submit">Ablehnen</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<form method="post" style="display:inline-block; margin-left:6px;" onsubmit="return confirm('Wirklich löschen?');">
|
||||
<input type="hidden" name="id" value="<?php echo $r['id']; ?>">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<button class="btn btn-sm btn-outline-danger" type="submit">Löschen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<?php include 'footer.php';
|
||||
|
||||
?>
|
||||
<?php
|
||||
session_start();
|
||||
require_once("inc/config.inc.php");
|
||||
require_once("inc/functions.inc.php");
|
||||
require_once("inc/vacation_absence.inc.php");
|
||||
|
||||
$user = check_user();
|
||||
if (!is_admin_user()) {
|
||||
die('Zugriff verweigert. Nur Chefs dürfen Anträge genehmigen.');
|
||||
}
|
||||
|
||||
vacationAbsenceEnsureSchema($pdo);
|
||||
|
||||
// Handle approve/reject actions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['id']) && isset($_POST['action'])) {
|
||||
$id = (int)$_POST['id'];
|
||||
$action = $_POST['action'];
|
||||
|
||||
if ($action === 'approve') {
|
||||
$stmt = $pdo->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();
|
||||
|
||||
?>
|
||||
|
||||
<div class="container">
|
||||
<h2>Abwesenheitsanträge - Genehmigung</h2>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mitarbeiter</th>
|
||||
<th>Grund</th>
|
||||
<th>Von</th>
|
||||
<th>Bis</th>
|
||||
<th>Tage</th>
|
||||
<th>Kommentar</th>
|
||||
<th>Status</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($requests as $r): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($r['vorname'] . ' ' . $r['nachname']); ?></td>
|
||||
<td><?php echo htmlspecialchars(vacationAbsenceReasonLabel($r['absence_reason'] ?? 'urlaub')); ?></td>
|
||||
<td><?php echo htmlspecialchars((string)$r['start_date']); ?></td>
|
||||
<td><?php echo htmlspecialchars((string)$r['end_date']); ?></td>
|
||||
<td><?php echo (int)$r['days']; ?></td>
|
||||
<td><?php echo htmlspecialchars((string)($r['comment_user'] ?? '')); ?></td>
|
||||
<td>
|
||||
<?php
|
||||
if ($r['status'] === 'beantragt' || $r['status'] === null) {
|
||||
echo '<span class="badge badge-warning">Beantragt</span>';
|
||||
} elseif ($r['status'] === 'genehmigt') {
|
||||
echo '<span class="badge badge-success">Genehmigt</span>';
|
||||
} else {
|
||||
echo '<span class="badge badge-danger">Abgelehnt</span>';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($r['status'] !== 'genehmigt'): ?>
|
||||
<form method="post" style="display:inline-block; margin-right:6px;">
|
||||
<input type="hidden" name="id" value="<?php echo (int)$r['id']; ?>">
|
||||
<input type="hidden" name="action" value="approve">
|
||||
<button class="btn btn-sm btn-success" type="submit">Genehmigen</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($r['status'] !== 'abgelehnt'): ?>
|
||||
<form method="post" style="display:inline-block;">
|
||||
<input type="hidden" name="id" value="<?php echo (int)$r['id']; ?>">
|
||||
<input type="hidden" name="action" value="reject">
|
||||
<button class="btn btn-sm btn-danger" type="submit">Ablehnen</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
<form method="post" style="display:inline-block; margin-left:6px;" onsubmit="return confirm('Wirklich löschen?');">
|
||||
<input type="hidden" name="id" value="<?php echo (int)$r['id']; ?>">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<button class="btn btn-sm btn-outline-danger" type="submit">Löschen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<?php include 'footer.php'; ?>
|
||||
|
||||
@@ -52,16 +52,17 @@ if (!isset($user)) {
|
||||
<!-- Urlaub Dropdown -->
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="urlaubDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Urlaub
|
||||
Abwesenheit
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="urlaubDropdown">
|
||||
<a class="dropdown-item" href="urlaubsantrag.php">Urlaubsantrag</a>
|
||||
<a class="dropdown-item" href="my_vacations_calendar.php">Mein Urlaubskalender</a>
|
||||
<a class="dropdown-item" href="vacations_calendar_all.php">Team Urlaubskalender</a>
|
||||
<a class="dropdown-item" href="urlaubsantrag.php">Abwesenheitsantrag</a>
|
||||
<a class="dropdown-item" href="my_vacations_calendar.php">Mein Abwesenheitskalender</a>
|
||||
<a class="dropdown-item" href="vacations_calendar_all.php">Team-Urlaubskalender</a>
|
||||
<?php if (is_admin_user()) : ?>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="vacations_overview.php">Urlaubsübersicht</a>
|
||||
<a class="dropdown-item" href="approveVacation.php">Urlaubsanträge genehmigen</a>
|
||||
<a class="dropdown-item" href="admin_absence_calendar.php">Leitungskalender</a>
|
||||
<a class="dropdown-item" href="vacations_overview.php">Abwesenheitsübersicht</a>
|
||||
<a class="dropdown-item" href="approveVacation.php">Abwesenheiten genehmigen</a>
|
||||
<a class="dropdown-item" href="company_holidays.php">Betriebsurlaub</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
if (!function_exists('vacationAbsenceTableExists')) {
|
||||
function vacationAbsenceTableExists(PDO $pdo, string $table): bool
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT COUNT(*)
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = :table_name"
|
||||
);
|
||||
$stmt->execute(['table_name' => $table]);
|
||||
return (int)$stmt->fetchColumn() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('vacationAbsenceTableHasColumn')) {
|
||||
function vacationAbsenceTableHasColumn(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('vacationAbsenceReasonOptions')) {
|
||||
function vacationAbsenceReasonOptions(): array
|
||||
{
|
||||
return [
|
||||
'urlaub' => 'Urlaub',
|
||||
'krankheit_mit_atest' => 'Krankheit mit Attest',
|
||||
'krankheit_ohne_atest' => 'Krankheit ohne Attest',
|
||||
'berufsschule' => 'Berufsschule',
|
||||
'weiterbildung' => 'Weiterbildung',
|
||||
'persoenliche_gruende' => 'Persoenliche Gründe',
|
||||
'sonstiges' => 'Sonstiges',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('vacationAbsenceDefaultReason')) {
|
||||
function vacationAbsenceDefaultReason(): string
|
||||
{
|
||||
return 'urlaub';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('vacationAbsenceNormalizeReason')) {
|
||||
function vacationAbsenceNormalizeReason($reason): string
|
||||
{
|
||||
$reason = trim((string)$reason);
|
||||
$options = vacationAbsenceReasonOptions();
|
||||
|
||||
return array_key_exists($reason, $options) ? $reason : vacationAbsenceDefaultReason();
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('vacationAbsenceReasonLabel')) {
|
||||
function vacationAbsenceReasonLabel($reason): string
|
||||
{
|
||||
$reason = vacationAbsenceNormalizeReason($reason);
|
||||
$options = vacationAbsenceReasonOptions();
|
||||
|
||||
return $options[$reason] ?? $options[vacationAbsenceDefaultReason()];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('vacationAbsenceCountsAgainstEntitlement')) {
|
||||
function vacationAbsenceCountsAgainstEntitlement($reason): bool
|
||||
{
|
||||
return vacationAbsenceNormalizeReason($reason) === 'urlaub';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('vacationAbsenceEnsureSchema')) {
|
||||
function vacationAbsenceEnsureSchema(PDO $pdo): void
|
||||
{
|
||||
if (!vacationAbsenceTableExists($pdo, 'vacations')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!vacationAbsenceTableHasColumn($pdo, 'vacations', 'absence_reason')) {
|
||||
$pdo->exec("ALTER TABLE vacations ADD COLUMN absence_reason VARCHAR(50) NOT NULL DEFAULT 'urlaub'");
|
||||
}
|
||||
|
||||
$pdo->exec("
|
||||
UPDATE vacations
|
||||
SET absence_reason = 'urlaub'
|
||||
WHERE absence_reason IS NULL OR TRIM(absence_reason) = ''
|
||||
");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +1,92 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once('inc/config.inc.php');
|
||||
require_once('inc/functions.inc.php');
|
||||
|
||||
$user = check_user();
|
||||
// personal calendar available to any logged-in user
|
||||
|
||||
include 'header.php';
|
||||
?>
|
||||
|
||||
<div class="container">
|
||||
<h2>Mein Urlaubskalender</h2>
|
||||
<div id="calendar"></div>
|
||||
<br>
|
||||
<div>
|
||||
<span class="badge badge-success">genehmigt</span>
|
||||
<span class="badge badge-warning">beantragt</span>
|
||||
<span class="badge badge-primary">Betriebsurlaub</span>
|
||||
</div>
|
||||
<br>
|
||||
<div id="eventDetails" style="display:none;">
|
||||
<h4>Details</h4>
|
||||
<div id="detailsContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<link href='https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.css' rel='stylesheet' />
|
||||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.js'></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var calendarEl = document.getElementById('calendar');
|
||||
|
||||
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
initialView: 'dayGridMonth',
|
||||
firstDay: 1,
|
||||
height: 650,
|
||||
events: function(info, successCallback, failureCallback) {
|
||||
var url = 'api/vacations.php?start=' + info.startStr + '&end=' + info.endStr + '&only_personal=1';
|
||||
fetch(url).then(function(res){ return res.json(); }).then(function(data){ successCallback(data); }).catch(function(err){ failureCallback(err); });
|
||||
},
|
||||
eventClick: function(info) {
|
||||
var ev = info.event;
|
||||
var props = ev.extendedProps;
|
||||
var html = '<strong>' + ev.title + '</strong><br>' + ev.start.toLocaleDateString() + ' - ' + (new Date(ev.end).toLocaleDateString()) + '<br>';
|
||||
if (props.type === 'user') {
|
||||
html += 'Status: ' + (props.status || '') + '<br>';
|
||||
html += 'Kommentar: ' + (props.comment || '') + '<br>';
|
||||
} else if (props.type === 'company') {
|
||||
html += 'Beschreibung: ' + (props.description || '') + '<br>';
|
||||
}
|
||||
document.getElementById('detailsContent').innerHTML = html;
|
||||
document.getElementById('eventDetails').style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include 'footer.php'; ?>
|
||||
<?php
|
||||
session_start();
|
||||
require_once('inc/config.inc.php');
|
||||
require_once('inc/functions.inc.php');
|
||||
|
||||
$user = check_user();
|
||||
|
||||
include 'header.php';
|
||||
?>
|
||||
|
||||
<div class="container">
|
||||
<h2>Mein Abwesenheitskalender</h2>
|
||||
<p>Hier werden alle persönlichen Abwesenheiten angezeigt, inklusive Urlaub, Krankheit, Schule und Weiterbildung. Betriebsurlaub wird ebenfalls mit eingeblendet.</p>
|
||||
|
||||
<div id="calendar"></div>
|
||||
<br>
|
||||
<div class="mb-3">
|
||||
<strong>Abwesenheitsarten:</strong><br>
|
||||
<span class="badge badge-success">Urlaub</span>
|
||||
<span class="badge badge-danger">Krankheit mit Attest</span>
|
||||
<span class="badge badge-dark">Krankheit ohne Attest</span>
|
||||
<span class="badge badge-primary">Berufsschule</span>
|
||||
<span class="badge badge-info">Weiterbildung</span>
|
||||
<span class="badge badge-warning">Persönliche Gründe</span>
|
||||
<span class="badge badge-secondary">Sonstiges</span>
|
||||
<span class="badge badge-primary">Betriebsurlaub</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<strong>Status:</strong><br>
|
||||
<span class="badge badge-success">Genehmigt</span>
|
||||
<span class="badge badge-warning">Beantragt</span>
|
||||
<span class="badge badge-secondary">Abgelehnt</span>
|
||||
</div>
|
||||
<div id="eventDetails" style="display:none;">
|
||||
<h4>Details</h4>
|
||||
<div id="detailsContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<link href='https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.css' rel='stylesheet' />
|
||||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.js'></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var calendarEl = document.getElementById('calendar');
|
||||
|
||||
function formatAllDayRange(start, end) {
|
||||
var startLabel = start.toLocaleDateString('de-DE');
|
||||
if (!end) {
|
||||
return startLabel;
|
||||
}
|
||||
|
||||
var inclusiveEnd = new Date(end.getTime() - 24 * 60 * 60 * 1000);
|
||||
return startLabel + ' - ' + inclusiveEnd.toLocaleDateString('de-DE');
|
||||
}
|
||||
|
||||
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
initialView: 'dayGridMonth',
|
||||
firstDay: 1,
|
||||
height: 650,
|
||||
events: function(info, successCallback, failureCallback) {
|
||||
var url = 'api/vacations.php?start=' + info.startStr + '&end=' + info.endStr + '&scope=personal&include_company=1&status_filter=all';
|
||||
fetch(url)
|
||||
.then(function(res){ return res.json(); })
|
||||
.then(function(data){ successCallback(data); })
|
||||
.catch(function(err){ failureCallback(err); });
|
||||
},
|
||||
eventClick: function(info) {
|
||||
var ev = info.event;
|
||||
var props = ev.extendedProps || {};
|
||||
var html = '<strong>' + ev.title + '</strong><br>' +
|
||||
formatAllDayRange(ev.start, ev.end) + '<br>';
|
||||
|
||||
if (props.type === 'user') {
|
||||
html += 'Abwesenheitsgrund: ' + (props.absence_label || props.absence_type || '') + '<br>';
|
||||
html += 'Status: ' + (props.status_label || props.status || '') + '<br>';
|
||||
if (props.comment) {
|
||||
html += 'Kommentar: ' + props.comment + '<br>';
|
||||
}
|
||||
} else if (props.type === 'company') {
|
||||
html += 'Beschreibung: ' + (props.description || '') + '<br>';
|
||||
}
|
||||
|
||||
document.getElementById('detailsContent').innerHTML = html;
|
||||
document.getElementById('eventDetails').style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include 'footer.php'; ?>
|
||||
|
||||
+125
-121
@@ -1,121 +1,125 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once("inc/config.inc.php");
|
||||
require_once("inc/functions.inc.php");
|
||||
|
||||
$user = check_user();
|
||||
|
||||
if (!isset($_SESSION['userid'])) {
|
||||
die("Kein Benutzer angemeldet.");
|
||||
}
|
||||
|
||||
if ($_SESSION['admin'] != 1) {
|
||||
die("Kein Zugriff.");
|
||||
}
|
||||
|
||||
$admin_id = $_SESSION['userid'];
|
||||
$message = "";
|
||||
|
||||
/* ===== Antrag bearbeiten ===== */
|
||||
if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST['vacation_id'])) {
|
||||
|
||||
$vacation_id = (int)$_POST['vacation_id'];
|
||||
$action = $_POST['action'];
|
||||
$comment_admin = trim($_POST['comment_admin']);
|
||||
|
||||
if ($action == "genehmigen") {
|
||||
$status = "genehmigt";
|
||||
} elseif ($action == "ablehnen") {
|
||||
$status = "abgelehnt";
|
||||
} else {
|
||||
die("Ungültige Aktion.");
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE vacations
|
||||
SET status = ?,
|
||||
approved_by = ?,
|
||||
approved_at = NOW()
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$status, $admin_id, $vacation_id]);
|
||||
|
||||
$message = "Antrag erfolgreich aktualisiert.";
|
||||
}
|
||||
|
||||
/* ===== Offene Anträge laden ===== */
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT v.*, u.vorname, u.nachname
|
||||
FROM vacations v
|
||||
JOIN users u ON u.id = v.user_id
|
||||
WHERE v.status = 'beantragt'
|
||||
ORDER BY v.start_date ASC
|
||||
");
|
||||
$stmt->execute();
|
||||
$antraege = $stmt->fetchAll();
|
||||
?>
|
||||
|
||||
<?php include 'header.php'; ?>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
|
||||
<h2>Urlaubsanträge genehmigen</h2>
|
||||
|
||||
<?php if ($message): ?>
|
||||
<div class="alert alert-success"><?php echo $message; ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (count($antraege) == 0): ?>
|
||||
<div class="alert alert-info">Keine offenen Anträge.</div>
|
||||
<?php else: ?>
|
||||
|
||||
<table class="table table-bordered table-striped">
|
||||
<tr>
|
||||
<th>Mitarbeiter</th>
|
||||
<th>Von</th>
|
||||
<th>Bis</th>
|
||||
<th>Tage</th>
|
||||
<th>Kommentar</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
|
||||
<?php foreach ($antraege as $a): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($a['vorname'] . " " . $a['nachname']); ?></td>
|
||||
<td><?php echo $a['start_date']; ?></td>
|
||||
<td><?php echo $a['end_date']; ?></td>
|
||||
<td><?php echo $a['days']; ?></td>
|
||||
<td><?php echo htmlspecialchars($a['comment_user']); ?></td>
|
||||
<td>
|
||||
|
||||
<form method="post" style="display:inline;">
|
||||
<input type="hidden" name="vacation_id" value="<?php echo $a['id']; ?>">
|
||||
<input type="hidden" name="action" value="genehmigen">
|
||||
<button type="submit" class="btn btn-success btn-sm">
|
||||
Genehmigen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="post" style="display:inline;">
|
||||
<input type="hidden" name="vacation_id" value="<?php echo $a['id']; ?>">
|
||||
<input type="hidden" name="action" value="ablehnen">
|
||||
<button type="submit" class="btn btn-danger btn-sm">
|
||||
Ablehnen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
||||
</table>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include 'footer.php'; ?>
|
||||
<?php
|
||||
session_start();
|
||||
require_once("inc/config.inc.php");
|
||||
require_once("inc/functions.inc.php");
|
||||
require_once("inc/vacation_absence.inc.php");
|
||||
|
||||
$user = check_user();
|
||||
vacationAbsenceEnsureSchema($pdo);
|
||||
|
||||
if (!isset($_SESSION['userid'])) {
|
||||
die("Kein Benutzer angemeldet.");
|
||||
}
|
||||
|
||||
if ($_SESSION['admin'] != 1) {
|
||||
die("Kein Zugriff.");
|
||||
}
|
||||
|
||||
$admin_id = $_SESSION['userid'];
|
||||
$message = "";
|
||||
|
||||
/* ===== Antrag bearbeiten ===== */
|
||||
if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST['vacation_id'])) {
|
||||
|
||||
$vacation_id = (int)$_POST['vacation_id'];
|
||||
$action = $_POST['action'];
|
||||
$comment_admin = trim((string)($_POST['comment_admin'] ?? ''));
|
||||
|
||||
if ($action == "genehmigen") {
|
||||
$status = "genehmigt";
|
||||
} elseif ($action == "ablehnen") {
|
||||
$status = "abgelehnt";
|
||||
} else {
|
||||
die("Ungültige Aktion.");
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("
|
||||
UPDATE vacations
|
||||
SET status = ?,
|
||||
approved_by = ?,
|
||||
approved_at = NOW()
|
||||
WHERE id = ?
|
||||
");
|
||||
$stmt->execute([$status, $admin_id, $vacation_id]);
|
||||
|
||||
$message = "Antrag erfolgreich aktualisiert.";
|
||||
}
|
||||
|
||||
/* ===== Offene Anträge laden ===== */
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT v.*, u.vorname, u.nachname
|
||||
FROM vacations v
|
||||
JOIN users u ON u.id = v.user_id
|
||||
WHERE v.status = 'beantragt'
|
||||
ORDER BY v.start_date ASC
|
||||
");
|
||||
$stmt->execute();
|
||||
$antraege = $stmt->fetchAll();
|
||||
?>
|
||||
|
||||
<?php include 'header.php'; ?>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
|
||||
<h2>Abwesenheitsanträge genehmigen</h2>
|
||||
|
||||
<?php if ($message): ?>
|
||||
<div class="alert alert-success"><?php echo htmlspecialchars($message); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (count($antraege) == 0): ?>
|
||||
<div class="alert alert-info">Keine offenen Anträge.</div>
|
||||
<?php else: ?>
|
||||
|
||||
<table class="table table-bordered table-striped">
|
||||
<tr>
|
||||
<th>Mitarbeiter</th>
|
||||
<th>Grund</th>
|
||||
<th>Von</th>
|
||||
<th>Bis</th>
|
||||
<th>Tage</th>
|
||||
<th>Kommentar</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
|
||||
<?php foreach ($antraege as $a): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($a['vorname'] . " " . $a['nachname']); ?></td>
|
||||
<td><?php echo htmlspecialchars(vacationAbsenceReasonLabel($a['absence_reason'] ?? 'urlaub')); ?></td>
|
||||
<td><?php echo htmlspecialchars((string)$a['start_date']); ?></td>
|
||||
<td><?php echo htmlspecialchars((string)$a['end_date']); ?></td>
|
||||
<td><?php echo (int)$a['days']; ?></td>
|
||||
<td><?php echo htmlspecialchars((string)($a['comment_user'] ?? '')); ?></td>
|
||||
<td>
|
||||
|
||||
<form method="post" style="display:inline;">
|
||||
<input type="hidden" name="vacation_id" value="<?php echo (int)$a['id']; ?>">
|
||||
<input type="hidden" name="action" value="genehmigen">
|
||||
<button type="submit" class="btn btn-success btn-sm">
|
||||
Genehmigen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="post" style="display:inline;">
|
||||
<input type="hidden" name="vacation_id" value="<?php echo (int)$a['id']; ?>">
|
||||
<input type="hidden" name="action" value="ablehnen">
|
||||
<button type="submit" class="btn btn-danger btn-sm">
|
||||
Ablehnen
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
||||
</table>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include 'footer.php'; ?>
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
session_start();
|
||||
require_once("inc/config.inc.php");
|
||||
require_once("inc/functions.inc.php");
|
||||
require_once("inc/vacation_absence.inc.php");
|
||||
|
||||
$user = check_user();
|
||||
vacationAbsenceEnsureSchema($pdo);
|
||||
|
||||
if (!isset($_SESSION['userid'])) {
|
||||
die("Kein Benutzer angemeldet.");
|
||||
@@ -14,6 +16,10 @@ $canManageTeamVacations = can_manage_team_vacations();
|
||||
$message = "";
|
||||
$error = "";
|
||||
$selected_user_id = $user_id;
|
||||
$selected_absence_reason = vacationAbsenceDefaultReason();
|
||||
$start_date = '';
|
||||
$end_date = '';
|
||||
$comment = '';
|
||||
|
||||
$selectableUsers = [];
|
||||
if ($canManageTeamVacations) {
|
||||
@@ -49,6 +55,7 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
$start_date = trim((string)($_POST['start_date'] ?? ''));
|
||||
$end_date = trim((string)($_POST['end_date'] ?? ''));
|
||||
$comment = trim((string)($_POST['comment'] ?? ''));
|
||||
$selected_absence_reason = vacationAbsenceNormalizeReason($_POST['absence_reason'] ?? vacationAbsenceDefaultReason());
|
||||
$selected_user_id = $canManageTeamVacations ? (int)($_POST['user_id'] ?? $user_id) : $user_id;
|
||||
|
||||
$selectedUser = null;
|
||||
@@ -74,7 +81,7 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
$error = "Bitte beide Datumsfelder ausfuellen.";
|
||||
} elseif ($error === "" && $start_date > $end_date) {
|
||||
$error = "Enddatum liegt vor dem Startdatum.";
|
||||
} elseif ($error === "" && $start_date < date("Y-m-d")) {
|
||||
} elseif ($error === "" && vacationAbsenceCountsAgainstEntitlement($selected_absence_reason) && $start_date < date("Y-m-d")) {
|
||||
$error = "Urlaub kann nicht in der Vergangenheit beantragt werden.";
|
||||
} elseif ($error === "") {
|
||||
$stmt = $pdo->prepare("
|
||||
@@ -97,15 +104,20 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
$days = calculateWorkingDays($start_date, $end_date);
|
||||
|
||||
$insert = $pdo->prepare("
|
||||
INSERT INTO vacations (user_id, start_date, end_date, days, comment_user)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
INSERT INTO vacations (user_id, start_date, end_date, days, comment_user, absence_reason)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
");
|
||||
$insert->execute([$selected_user_id, $start_date, $end_date, $days, $comment]);
|
||||
$insert->execute([$selected_user_id, $start_date, $end_date, $days, $comment, $selected_absence_reason]);
|
||||
|
||||
$reasonLabel = vacationAbsenceReasonLabel($selected_absence_reason);
|
||||
if ($selected_user_id !== $user_id && $selectedUser) {
|
||||
$message = "Urlaub fuer " . $selectedUser['vorname'] . " " . $selectedUser['nachname'] . " erfolgreich eingereicht ($days Werktage).";
|
||||
$message = "Abwesenheit fuer " . $selectedUser['vorname'] . " " . $selectedUser['nachname'] . " erfolgreich eingetragen ($days Werktage, Grund: " . $reasonLabel . ").";
|
||||
} else {
|
||||
$message = "Urlaubsantrag erfolgreich eingereicht ($days Werktage).";
|
||||
if ($selected_absence_reason === 'urlaub') {
|
||||
$message = "Urlaubsantrag erfolgreich eingereicht ($days Werktage).";
|
||||
} else {
|
||||
$message = "Abwesenheitsantrag erfolgreich eingereicht ($days Werktage, Grund: " . $reasonLabel . ").";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,7 +130,7 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
|
||||
<h2>Urlaubsantrag</h2>
|
||||
<h2>Abwesenheitsantrag</h2>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger"><?php echo htmlspecialchars($error); ?></div>
|
||||
@@ -144,32 +156,44 @@ if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Abwesenheitsgrund:</label>
|
||||
<select name="absence_reason" class="form-control" required>
|
||||
<?php foreach (vacationAbsenceReasonOptions() as $reasonKey => $reasonLabel): ?>
|
||||
<option value="<?php echo htmlspecialchars($reasonKey); ?>" <?php echo ($selected_absence_reason === $reasonKey) ? 'selected' : ''; ?>>
|
||||
<?php echo htmlspecialchars($reasonLabel); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<small class="form-text text-muted">Nur Urlaub wird auf das Urlaubskontingent angerechnet.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Von:</label>
|
||||
<input type="date" name="start_date" class="form-control" required>
|
||||
<input type="date" name="start_date" class="form-control" value="<?php echo htmlspecialchars($start_date); ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Bis:</label>
|
||||
<input type="date" name="end_date" class="form-control" required>
|
||||
<input type="date" name="end_date" class="form-control" value="<?php echo htmlspecialchars($end_date); ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Kommentar (optional):</label>
|
||||
<textarea name="comment" class="form-control"></textarea>
|
||||
<textarea name="comment" class="form-control"><?php echo htmlspecialchars($comment); ?></textarea>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
<?php echo $canManageTeamVacations ? 'Urlaub eintragen' : 'Urlaub beantragen'; ?>
|
||||
<?php echo $canManageTeamVacations ? 'Abwesenheit eintragen' : 'Abwesenheit beantragen'; ?>
|
||||
</button>
|
||||
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4><?php echo $canManageTeamVacations ? 'Urlaubseintraege' : 'Meine Antraege'; ?></h4>
|
||||
<h4><?php echo $canManageTeamVacations ? 'Abwesenheitseintraege' : 'Meine Antraege'; ?></h4>
|
||||
|
||||
<?php
|
||||
$listSql = "
|
||||
@@ -196,6 +220,7 @@ $antraege = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
<?php if ($canManageTeamVacations): ?>
|
||||
<th>Mitarbeiter</th>
|
||||
<?php endif; ?>
|
||||
<th>Grund</th>
|
||||
<th>Von</th>
|
||||
<th>Bis</th>
|
||||
<th>Tage</th>
|
||||
@@ -208,6 +233,7 @@ $antraege = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
<?php if ($canManageTeamVacations): ?>
|
||||
<td><?php echo htmlspecialchars(trim($a['vorname'] . ' ' . $a['nachname'])); ?></td>
|
||||
<?php endif; ?>
|
||||
<td><?php echo htmlspecialchars(vacationAbsenceReasonLabel($a['absence_reason'] ?? 'urlaub')); ?></td>
|
||||
<td><?php echo htmlspecialchars((string)$a['start_date']); ?></td>
|
||||
<td><?php echo htmlspecialchars((string)$a['end_date']); ?></td>
|
||||
<td><?php echo (int)$a['days']; ?></td>
|
||||
|
||||
@@ -1,64 +1,90 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once('inc/config.inc.php');
|
||||
require_once('inc/functions.inc.php');
|
||||
|
||||
$user = check_user();
|
||||
if (!is_admin_user()) {
|
||||
die('Zugriff verweigert. Nur Chefs dürfen den Kalender sehen.');
|
||||
}
|
||||
|
||||
include 'header.php';
|
||||
?>
|
||||
|
||||
<div class="container">
|
||||
<h2>Team Urlaubskalender</h2>
|
||||
<div id="calendar"></div>
|
||||
<br>
|
||||
<div>
|
||||
<span class="badge badge-success">genehmigt</span>
|
||||
<span class="badge badge-warning">beantragt</span>
|
||||
<span class="badge badge-primary">Betriebsurlaub</span>
|
||||
</div>
|
||||
<br>
|
||||
<div id="eventDetails" style="display:none;">
|
||||
<h4>Details</h4>
|
||||
<div id="detailsContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<link href='https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.css' rel='stylesheet' />
|
||||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.js'></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var calendarEl = document.getElementById('calendar');
|
||||
|
||||
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
initialView: 'dayGridMonth',
|
||||
firstDay: 1,
|
||||
height: 650,
|
||||
events: function(info, successCallback, failureCallback) {
|
||||
var url = 'api/vacations.php?start=' + info.startStr + '&end=' + info.endStr;
|
||||
fetch(url).then(function(res){ return res.json(); }).then(function(data){ successCallback(data); }).catch(function(err){ failureCallback(err); });
|
||||
},
|
||||
<?php
|
||||
session_start();
|
||||
require_once('inc/config.inc.php');
|
||||
require_once('inc/functions.inc.php');
|
||||
|
||||
$user = check_user();
|
||||
if (!is_admin_user()) {
|
||||
die('Zugriff verweigert. Nur Chefs dürfen den Kalender sehen.');
|
||||
}
|
||||
|
||||
include 'header.php';
|
||||
?>
|
||||
|
||||
<div class="container">
|
||||
<h2>Team Urlaubskalender</h2>
|
||||
<p>Admin-Sicht auf den Teamkalender. Angezeigt werden nur Urlaub von Personen sowie Betriebsurlaub.</p>
|
||||
|
||||
<div id="calendar"></div>
|
||||
<br>
|
||||
<div class="mb-3">
|
||||
<strong>Anzeige:</strong><br>
|
||||
<span class="badge badge-success">Urlaub</span>
|
||||
<span class="badge badge-primary">Betriebsurlaub</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<strong>Status:</strong><br>
|
||||
<span class="badge badge-success">Genehmigt</span>
|
||||
<span class="badge badge-warning">Beantragt</span>
|
||||
<span class="badge badge-secondary">Abgelehnt</span>
|
||||
</div>
|
||||
<div id="eventDetails" style="display:none;">
|
||||
<h4>Details</h4>
|
||||
<div id="detailsContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<link href='https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.css' rel='stylesheet' />
|
||||
<script src='https://cdn.jsdelivr.net/npm/fullcalendar@5.11.3/main.min.js'></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var calendarEl = document.getElementById('calendar');
|
||||
|
||||
function formatAllDayRange(start, end) {
|
||||
var startLabel = start.toLocaleDateString('de-DE');
|
||||
if (!end) {
|
||||
return startLabel;
|
||||
}
|
||||
|
||||
var inclusiveEnd = new Date(end.getTime() - 24 * 60 * 60 * 1000);
|
||||
return startLabel + ' - ' + inclusiveEnd.toLocaleDateString('de-DE');
|
||||
}
|
||||
|
||||
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
initialView: 'dayGridMonth',
|
||||
firstDay: 1,
|
||||
height: 650,
|
||||
events: function(info, successCallback, failureCallback) {
|
||||
var url = 'api/vacations.php?start=' + info.startStr + '&end=' + info.endStr + '&scope=team&include_company=1&status_filter=active';
|
||||
fetch(url)
|
||||
.then(function(res){ return res.json(); })
|
||||
.then(function(data){ successCallback(data); })
|
||||
.catch(function(err){ failureCallback(err); });
|
||||
},
|
||||
eventClick: function(info) {
|
||||
var ev = info.event;
|
||||
var props = ev.extendedProps;
|
||||
var html = '<strong>' + ev.title + '</strong><br>' + ev.start.toLocaleDateString() + ' - ' + (new Date(ev.end).toLocaleDateString()) + '<br>';
|
||||
var props = ev.extendedProps || {};
|
||||
var html = '<strong>' + ev.title + '</strong><br>' +
|
||||
formatAllDayRange(ev.start, ev.end) + '<br>';
|
||||
|
||||
if (props.type === 'user') {
|
||||
html += 'Mitarbeiter: ' + (props.employee_name || '') + '<br>';
|
||||
html += 'Status: ' + (props.status || '') + '<br>';
|
||||
html += 'Kommentar: ' + (props.comment || '') + '<br>';
|
||||
html += 'Abwesenheitsgrund: ' + (props.absence_label || props.absence_type || '') + '<br>';
|
||||
html += 'Status: ' + (props.status_label || props.status || '') + '<br>';
|
||||
if (props.comment) {
|
||||
html += 'Kommentar: ' + props.comment + '<br>';
|
||||
}
|
||||
} else if (props.type === 'company') {
|
||||
html += 'Beschreibung: ' + (props.description || '') + '<br>';
|
||||
}
|
||||
document.getElementById('detailsContent').innerHTML = html;
|
||||
document.getElementById('eventDetails').style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include 'footer.php'; ?>
|
||||
|
||||
document.getElementById('detailsContent').innerHTML = html;
|
||||
document.getElementById('eventDetails').style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php include 'footer.php'; ?>
|
||||
|
||||
@@ -10,14 +10,21 @@ include 'header.php';
|
||||
|
||||
<div class="container">
|
||||
<h2>Team Urlaubskalender</h2>
|
||||
<p>Dieser Kalender zeigt nur Urlaub von Personen sowie Betriebsurlaub. Andere Abwesenheitsarten werden hier bewusst ausgeblendet.</p>
|
||||
|
||||
<div id="calendar"></div>
|
||||
<br>
|
||||
<div>
|
||||
<span class="badge badge-success">genehmigt</span>
|
||||
<span class="badge badge-warning">beantragt</span>
|
||||
<div class="mb-3">
|
||||
<strong>Anzeige:</strong><br>
|
||||
<span class="badge badge-success">Urlaub</span>
|
||||
<span class="badge badge-primary">Betriebsurlaub</span>
|
||||
</div>
|
||||
<br>
|
||||
<div class="mb-3">
|
||||
<strong>Status:</strong><br>
|
||||
<span class="badge badge-success">Genehmigt</span>
|
||||
<span class="badge badge-warning">Beantragt</span>
|
||||
<span class="badge badge-secondary">Abgelehnt</span>
|
||||
</div>
|
||||
<div id="eventDetails" style="display:none;">
|
||||
<h4>Details</h4>
|
||||
<div id="detailsContent"></div>
|
||||
@@ -30,27 +37,44 @@ include 'header.php';
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var calendarEl = document.getElementById('calendar');
|
||||
|
||||
function formatAllDayRange(start, end) {
|
||||
var startLabel = start.toLocaleDateString('de-DE');
|
||||
if (!end) {
|
||||
return startLabel;
|
||||
}
|
||||
|
||||
var inclusiveEnd = new Date(end.getTime() - 24 * 60 * 60 * 1000);
|
||||
return startLabel + ' - ' + inclusiveEnd.toLocaleDateString('de-DE');
|
||||
}
|
||||
|
||||
var calendar = new FullCalendar.Calendar(calendarEl, {
|
||||
initialView: 'dayGridMonth',
|
||||
firstDay: 1,
|
||||
height: 650,
|
||||
events: function(info, successCallback, failureCallback) {
|
||||
var url = 'api/vacations.php?start=' + info.startStr + '&end=' + info.endStr + '&public=1&public_all=1';
|
||||
fetch(url).then(function(res){ return res.json(); }).then(function(data){ successCallback(data); }).catch(function(err){ failureCallback(err); });
|
||||
var url = 'api/vacations.php?start=' + info.startStr + '&end=' + info.endStr + '&scope=team&include_company=1&status_filter=active';
|
||||
fetch(url)
|
||||
.then(function(res){ return res.json(); })
|
||||
.then(function(data){ successCallback(data); })
|
||||
.catch(function(err){ failureCallback(err); });
|
||||
},
|
||||
eventClick: function(info) {
|
||||
var ev = info.event;
|
||||
var props = ev.extendedProps;
|
||||
var html = '<strong>' + ev.title + '</strong><br>' + ev.start.toLocaleDateString() + ' - ' + (new Date(ev.end).toLocaleDateString()) + '<br>';
|
||||
var props = ev.extendedProps || {};
|
||||
var html = '<strong>' + ev.title + '</strong><br>' +
|
||||
formatAllDayRange(ev.start, ev.end) + '<br>';
|
||||
|
||||
if (props.type === 'user') {
|
||||
html += 'Mitarbeiter: ' + (props.employee_name || ev.title || '') + '<br>';
|
||||
html += 'Status: ' + (props.status || '') + '<br>';
|
||||
html += 'Mitarbeiter: ' + (props.employee_name || '') + '<br>';
|
||||
html += 'Abwesenheitsgrund: ' + (props.absence_label || props.absence_type || '') + '<br>';
|
||||
html += 'Status: ' + (props.status_label || props.status || '') + '<br>';
|
||||
if (props.comment) {
|
||||
html += 'Kommentar: ' + props.comment + '<br>';
|
||||
}
|
||||
} else if (props.type === 'company') {
|
||||
html += 'Beschreibung: ' + (props.description || '') + '<br>';
|
||||
}
|
||||
|
||||
document.getElementById('detailsContent').innerHTML = html;
|
||||
document.getElementById('eventDetails').style.display = 'block';
|
||||
}
|
||||
|
||||
@@ -1,84 +1,198 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once('inc/config.inc.php');
|
||||
require_once('inc/functions.inc.php');
|
||||
|
||||
$user = check_user();
|
||||
if (!is_admin_user()) {
|
||||
die('Zugriff verweigert. Nur Chefs dürfen die Urlaubsübersicht sehen.');
|
||||
}
|
||||
|
||||
include 'header.php';
|
||||
|
||||
// Jahr für Auswertung
|
||||
$year = date('Y');
|
||||
|
||||
// Lade alle Mitarbeiter
|
||||
$stmt = $pdo->prepare("SELECT id, vorname, nachname, email, urlaubstage FROM users ORDER BY nachname, vorname");
|
||||
$stmt->execute();
|
||||
$users = $stmt->fetchAll();
|
||||
|
||||
?>
|
||||
|
||||
<div class="container">
|
||||
<h2>Urlaubsübersicht (<?php echo $year; ?>)</h2>
|
||||
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mitarbeiter</th>
|
||||
<th>Email</th>
|
||||
<th>Anspruch</th>
|
||||
<th>Genutzt (<?php echo $year; ?>)</th>
|
||||
<th>Verbleibend</th>
|
||||
<th>Ausstehend</th>
|
||||
<th>Bevorstehende Urlaube</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users as $u):
|
||||
$uid = $u['id'];
|
||||
|
||||
// Genutzte Tage (genehmigt) im Jahr
|
||||
$s = $pdo->prepare("SELECT IFNULL(SUM(days),0) AS used FROM vacations WHERE user_id = ? AND status = 'genehmigt' AND YEAR(start_date) = ?");
|
||||
$s->execute([$uid, $year]);
|
||||
$used = (int)$s->fetchColumn();
|
||||
|
||||
// Ausstehende Anträge
|
||||
$p = $pdo->prepare("SELECT COUNT(*) FROM vacations WHERE user_id = ? AND status = 'beantragt'");
|
||||
$p->execute([$uid]);
|
||||
$pending = (int)$p->fetchColumn();
|
||||
|
||||
// Bevorstehende Urlaube (nächste 5)
|
||||
$n = $pdo->prepare("SELECT start_date, end_date, days, status FROM vacations WHERE user_id = ? AND end_date >= CURDATE() ORDER BY start_date LIMIT 5");
|
||||
$n->execute([$uid]);
|
||||
$upcoming = $n->fetchAll();
|
||||
|
||||
$entitlement = isset($u['urlaubstage']) ? (int)$u['urlaubstage'] : 0;
|
||||
$remaining = $entitlement - $used;
|
||||
?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($u['vorname'] . ' ' . $u['nachname']); ?></td>
|
||||
<td><?php echo htmlspecialchars($u['email']); ?></td>
|
||||
<td><?php echo $entitlement; ?></td>
|
||||
<td><?php echo $used; ?></td>
|
||||
<td><?php echo $remaining; ?></td>
|
||||
<td><?php echo $pending; ?></td>
|
||||
<td>
|
||||
<?php if (count($upcoming) == 0) { echo '-'; } else {
|
||||
foreach ($upcoming as $up) {
|
||||
echo htmlspecialchars($up['start_date'] . ' → ' . $up['end_date'] . ' (' . $up['days'] . 'd) ' . ' [' . $up['status'] . ']');
|
||||
echo '<br>';
|
||||
}
|
||||
} ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>Hinweis: Ansprüche werden aus dem Feld <strong>users.urlaubstage</strong> gelesen. Falls dieses Feld leer ist, bitte in der Nutzerverwaltung pflegen.</p>
|
||||
|
||||
</div>
|
||||
|
||||
<?php include 'footer.php'; ?>
|
||||
<?php
|
||||
session_start();
|
||||
require_once('inc/config.inc.php');
|
||||
require_once('inc/functions.inc.php');
|
||||
require_once __DIR__ . '/inc/vacation_absence.inc.php';
|
||||
|
||||
$user = check_user();
|
||||
if (!is_admin_user()) {
|
||||
die('Zugriff verweigert. Nur Chefs dürfen die Abwesenheitsübersicht sehen.');
|
||||
}
|
||||
|
||||
$schemaWarning = '';
|
||||
try {
|
||||
vacationAbsenceEnsureSchema($pdo);
|
||||
} catch (Throwable $e) {
|
||||
$schemaWarning = 'Das Abwesenheitsschema konnte nicht automatisch aktualisiert werden: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
include 'header.php';
|
||||
|
||||
// Jahr für Auswertung
|
||||
$year = date('Y');
|
||||
$yearStart = $year . '-01-01';
|
||||
$nextYearStart = (string)((int)$year + 1) . '-01-01';
|
||||
|
||||
$reasonOptions = vacationAbsenceReasonOptions();
|
||||
$reasonKeys = array_keys($reasonOptions);
|
||||
|
||||
function vacationOverviewStatusLabel(?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);
|
||||
}
|
||||
|
||||
// Lade alle Mitarbeiter
|
||||
$stmt = $pdo->prepare("SELECT id, vorname, nachname, email, urlaubstage FROM users ORDER BY nachname, vorname");
|
||||
$stmt->execute();
|
||||
$users = $stmt->fetchAll();
|
||||
|
||||
$statsStmt = $pdo->prepare("
|
||||
SELECT
|
||||
user_id,
|
||||
COALESCE(SUM(CASE WHEN LOWER(TRIM(COALESCE(absence_reason, 'urlaub'))) = 'urlaub' AND LOWER(TRIM(COALESCE(status, ''))) = 'genehmigt' THEN COALESCE(days, 0) ELSE 0 END), 0) AS urlaub_used_days,
|
||||
COALESCE(SUM(CASE WHEN LOWER(TRIM(COALESCE(status, ''))) != 'abgelehnt' THEN COALESCE(days, 0) ELSE 0 END), 0) AS total_absence_days,
|
||||
COALESCE(SUM(CASE WHEN LOWER(TRIM(COALESCE(status, ''))) = 'beantragt' OR TRIM(COALESCE(status, '')) = '' THEN 1 ELSE 0 END), 0) AS pending_count,
|
||||
COALESCE(SUM(CASE WHEN LOWER(TRIM(COALESCE(absence_reason, 'urlaub'))) = 'urlaub' AND LOWER(TRIM(COALESCE(status, ''))) != 'abgelehnt' THEN COALESCE(days, 0) ELSE 0 END), 0) AS urlaub_total_days,
|
||||
COALESCE(SUM(CASE WHEN LOWER(TRIM(COALESCE(absence_reason, 'urlaub'))) = 'krankheit_mit_atest' AND LOWER(TRIM(COALESCE(status, ''))) != 'abgelehnt' THEN COALESCE(days, 0) ELSE 0 END), 0) AS krankheit_mit_atest_days,
|
||||
COALESCE(SUM(CASE WHEN LOWER(TRIM(COALESCE(absence_reason, 'urlaub'))) = 'krankheit_ohne_atest' AND LOWER(TRIM(COALESCE(status, ''))) != 'abgelehnt' THEN COALESCE(days, 0) ELSE 0 END), 0) AS krankheit_ohne_atest_days,
|
||||
COALESCE(SUM(CASE WHEN LOWER(TRIM(COALESCE(absence_reason, 'urlaub'))) = 'berufsschule' AND LOWER(TRIM(COALESCE(status, ''))) != 'abgelehnt' THEN COALESCE(days, 0) ELSE 0 END), 0) AS berufsschule_days,
|
||||
COALESCE(SUM(CASE WHEN LOWER(TRIM(COALESCE(absence_reason, 'urlaub'))) = 'weiterbildung' AND LOWER(TRIM(COALESCE(status, ''))) != 'abgelehnt' THEN COALESCE(days, 0) ELSE 0 END), 0) AS weiterbildung_days,
|
||||
COALESCE(SUM(CASE WHEN LOWER(TRIM(COALESCE(absence_reason, 'urlaub'))) = 'persoenliche_gruende' AND LOWER(TRIM(COALESCE(status, ''))) != 'abgelehnt' THEN COALESCE(days, 0) ELSE 0 END), 0) AS persoenliche_gruende_days,
|
||||
COALESCE(SUM(CASE WHEN LOWER(TRIM(COALESCE(absence_reason, 'urlaub'))) = 'sonstiges' AND LOWER(TRIM(COALESCE(status, ''))) != 'abgelehnt' THEN COALESCE(days, 0) ELSE 0 END), 0) AS sonstiges_days
|
||||
FROM vacations
|
||||
WHERE start_date >= :year_start
|
||||
AND start_date < :next_year_start
|
||||
GROUP BY user_id
|
||||
");
|
||||
$statsStmt->execute([
|
||||
'year_start' => $yearStart,
|
||||
'next_year_start' => $nextYearStart,
|
||||
]);
|
||||
$statsRows = $statsStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$statsByUser = [];
|
||||
foreach ($statsRows as $row) {
|
||||
$statsByUser[(int)$row['user_id']] = $row;
|
||||
}
|
||||
|
||||
$upcomingStmt = $pdo->prepare("
|
||||
SELECT
|
||||
v.user_id,
|
||||
v.start_date,
|
||||
v.end_date,
|
||||
v.days,
|
||||
v.status,
|
||||
v.absence_reason,
|
||||
u.vorname,
|
||||
u.nachname
|
||||
FROM vacations v
|
||||
JOIN users u ON u.id = v.user_id
|
||||
WHERE v.end_date >= CURDATE()
|
||||
AND LOWER(TRIM(COALESCE(v.status, ''))) != 'abgelehnt'
|
||||
ORDER BY v.start_date ASC, v.end_date ASC
|
||||
");
|
||||
$upcomingStmt->execute();
|
||||
$upcomingRows = $upcomingStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$upcomingByUser = [];
|
||||
foreach ($upcomingRows as $row) {
|
||||
$uid = (int)$row['user_id'];
|
||||
if (!isset($upcomingByUser[$uid])) {
|
||||
$upcomingByUser[$uid] = [];
|
||||
}
|
||||
if (count($upcomingByUser[$uid]) < 5) {
|
||||
$upcomingByUser[$uid][] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
function vacationOverviewReasonValue(array $statsByUser, int $userId, string $key): int
|
||||
{
|
||||
$map = [
|
||||
'urlaub' => 'urlaub_total_days',
|
||||
'krankheit_mit_atest' => 'krankheit_mit_atest_days',
|
||||
'krankheit_ohne_atest' => 'krankheit_ohne_atest_days',
|
||||
'berufsschule' => 'berufsschule_days',
|
||||
'weiterbildung' => 'weiterbildung_days',
|
||||
'persoenliche_gruende' => 'persoenliche_gruende_days',
|
||||
'sonstiges' => 'sonstiges_days',
|
||||
];
|
||||
|
||||
$field = $map[$key] ?? null;
|
||||
if ($field === null || !isset($statsByUser[$userId])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int)($statsByUser[$userId][$field] ?? 0);
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
<div class="container">
|
||||
<h2>Abwesenheitsübersicht (<?php echo $year; ?>)</h2>
|
||||
|
||||
<?php if (!empty($schemaWarning)): ?>
|
||||
<div class="alert alert-warning"><?php echo htmlspecialchars($schemaWarning); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>Hinweis:</strong> Nur der Grund <em>Urlaub</em> zählt auf den Urlaubsanspruch. Alle anderen Abwesenheitsgründe werden hier zusätzlich pro Jahr ausgewertet.
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mitarbeiter</th>
|
||||
<th>Email</th>
|
||||
<th>Anspruch</th>
|
||||
<th>Urlaub genutzt (<?php echo $year; ?>)</th>
|
||||
<th>Verbleibend</th>
|
||||
<th>Alle Abwesenheiten</th>
|
||||
<th>Offene Anträge</th>
|
||||
<?php foreach ($reasonKeys as $reasonKey): ?>
|
||||
<th><?php echo htmlspecialchars(vacationAbsenceReasonLabel($reasonKey)); ?></th>
|
||||
<?php endforeach; ?>
|
||||
<th>Bevorstehende Einträge</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users as $u):
|
||||
$uid = $u['id'];
|
||||
|
||||
$entitlement = isset($u['urlaubstage']) ? (int)$u['urlaubstage'] : 0;
|
||||
$used = (int)($statsByUser[$uid]['urlaub_used_days'] ?? 0);
|
||||
$remaining = $entitlement - $used;
|
||||
$totalAbsences = (int)($statsByUser[$uid]['total_absence_days'] ?? 0);
|
||||
$pending = (int)($statsByUser[$uid]['pending_count'] ?? 0);
|
||||
$upcoming = $upcomingByUser[$uid] ?? [];
|
||||
?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($u['vorname'] . ' ' . $u['nachname']); ?></td>
|
||||
<td><?php echo htmlspecialchars($u['email']); ?></td>
|
||||
<td><?php echo $entitlement; ?></td>
|
||||
<td><?php echo $used; ?></td>
|
||||
<td><?php echo $remaining; ?></td>
|
||||
<td><?php echo $totalAbsences; ?></td>
|
||||
<td><?php echo $pending; ?></td>
|
||||
<?php foreach ($reasonKeys as $reasonKey): ?>
|
||||
<td><?php echo vacationOverviewReasonValue($statsByUser, (int)$uid, $reasonKey); ?></td>
|
||||
<?php endforeach; ?>
|
||||
<td>
|
||||
<?php if (count($upcoming) == 0) { echo '-'; } else {
|
||||
foreach ($upcoming as $up) {
|
||||
$reasonLabel = vacationAbsenceReasonLabel($up['absence_reason'] ?? 'urlaub');
|
||||
$statusLabel = vacationOverviewStatusLabel($up['status'] ?? null);
|
||||
echo htmlspecialchars($up['start_date'] . ' → ' . $up['end_date'] . ' (' . $up['days'] . 'd) ' . ' [' . $reasonLabel . ', ' . $statusLabel . ']');
|
||||
echo '<br>';
|
||||
}
|
||||
} ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p>Hinweis: Der Urlaubsanspruch wird weiterhin aus <strong>users.urlaubstage</strong> gelesen. Die zusätzlichen Spalten zeigen die Abwesenheiten je Grund pro Jahr.</p>
|
||||
|
||||
</div>
|
||||
|
||||
<?php include 'footer.php'; ?>
|
||||
|
||||
Reference in New Issue
Block a user