Compare commits

...

20 Commits

Author SHA1 Message Date
clemens fd320ba0c6 Praxis Webseite Update 2026-04-02 01:30:14 +02:00
clemens aae89a45a8 Abwesenheitskalender erweitert 2026-04-01 23:59:28 +02:00
clemens 6360af272a Impfverwaltung anpassen 2026-03-30 21:52:10 +02:00
clemens 091702c2a2 Menü und Hilfe 2026-03-30 21:24:55 +02:00
clemens c9b0026f52 Menüanpassung und Sonderzeichen 2026-03-30 21:04:39 +02:00
clemens bb422005d0 PDF Ausgabe erweitert 2026-03-30 20:54:41 +02:00
clemens 098c2d4275 Betriebsurlaub 2026-03-30 20:48:55 +02:00
clemens 7388b5b379 Schließung aller offnen Fehler 2026-03-30 20:46:08 +02:00
clemens 016753293c Merge branch 'main' of https://git.ctb-it.de/clemens/praxis-creutzburg-web 2026-03-30 20:37:17 +02:00
clemens 874e8a04c0 Zeiterfassung Mail anpassung 2026-03-30 20:35:13 +02:00
clemens 0084516414 zeiterfassung 2026-03-30 20:34:27 +02:00
clemens e22dbc980c Anpassung Ladezeit Impfen + Urlaubsplaner 2026-03-30 08:44:45 +02:00
clemens 8470e90f56 ftp Einstellungen 2026-03-29 22:27:22 +02:00
clemens 26666aef30 anpassung anfragen Seite 2026-03-24 15:39:18 +01:00
clemens 3fee4eefe2 Anpassung Startseite 2026-03-24 15:36:32 +01:00
clemens 6dd0ac86b2 Impfworkflow + Patientensuche repariert 2026-03-24 14:57:21 +01:00
clemens 211ce11e06 Abgleich mit Live-Daten 2026-03-24 14:45:06 +01:00
clemens 00077aa09a Merge branch 'main' of https://git.ctb-it.de/clemens/praxis-creutzburg-web 2026-03-23 17:14:11 +01:00
clemens 4b4c1f74df impfwarteliste angepasst 2026-03-23 17:14:09 +01:00
clemens f5ffaf297d Stellenausschreibung rausgenommen 2026-03-23 17:02:57 +01:00
150 changed files with 20498 additions and 16324 deletions
+5
View File
@@ -0,0 +1,5 @@
*.woff binary
*.woff2 binary
*.ttf binary
*.eot binary
*.otf binary
+6
View File
@@ -25,3 +25,9 @@
/app/Config/database.php /app/Config/database.php
/vendors/* /vendors/*
# Local editor/deploy configuration
/.vscode/ftp-sync.json
/.vscode/sftp.json
.vscode/ftp-sync.json
.vscode/sftp.json
+27
View File
@@ -0,0 +1,27 @@
{
"remotePath": "./",
"host": "your-host",
"username": "your-username",
"password": "your-password",
"port": 21,
"secure": true,
"protocol": "ftp",
"uploadOnSave": false,
"passive": false,
"debug": false,
"privateKeyPath": null,
"passphrase": null,
"agent": null,
"allow": [],
"ignore": [
"\\.vscode",
"\\.git",
"\\.DS_Store"
],
"generatedFiles": {
"extensionsToInclude": [
""
],
"path": ""
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"sqltools.connections": [
{
"mysqlOptions": {
"authProtocol": "default",
"enableSsl": "Disabled"
},
"ssh": "Disabled",
"previewLimit": 50,
"server": "mysql2fda.netcup.net",
"port": 3306,
"driver": "MySQL",
"name": "Praxis Creutzburg",
"database": "k25330_pracreutz",
"username": "k25330_pracreutz"
}
]
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "Project Deployment",
"host": "your-host",
"protocol": "ftp",
"port": 21,
"username": "your-username",
"password": "your-password",
"remotePath": "/",
"secure": true,
"uploadOnSave": false,
"useTempFile": false,
"openSsh": false
}
+24
View File
@@ -0,0 +1,24 @@
CREATE TABLE IF NOT EXISTS time_error_notification_state (
employee_id INT NOT NULL,
cycle_started_on DATE NOT NULL,
first_error_date DATE NOT NULL,
last_notification_stage VARCHAR(50) DEFAULT NULL,
last_notification_sent_at DATETIME DEFAULT NULL,
employee_day_1_sent_at DATETIME DEFAULT NULL,
employee_day_3_sent_at DATETIME DEFAULT NULL,
admin_day_7_sent_at DATETIME DEFAULT NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (employee_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS time_error_notifications (
id INT NOT NULL AUTO_INCREMENT,
employee_id INT NOT NULL,
cycle_started_on DATE NOT NULL,
notification_stage VARCHAR(50) NOT NULL,
recipient_email VARCHAR(255) NOT NULL,
sent_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uniq_time_error_notification (employee_id, cycle_started_on, notification_stage, recipient_email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+74 -37
View File
@@ -1,16 +1,19 @@
<?php <?php
// admin/bootstrap.php session_start();
ob_start(); // fängt zufälligen Output ab, verhindert "headers already sent" Folgeschäden
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
require_once(__DIR__ . "/../inc/config.inc.php"); // WICHTIG: Pfade aus /admin heraus korrekt auflösen
require_once(__DIR__ . "/../inc/functions.inc.php"); require_once __DIR__ . "/../inc/config.inc.php";
require_once __DIR__ . "/../inc/functions.inc.php";
require_once __DIR__ . "/../inc/company_holiday_sync.inc.php";
// Login prüfen
$user = check_admin_user();
include __DIR__ . "/templates/header.inc.php";
$user = check_admin_user(); $user = check_admin_user();
$internUserId = (int)$_SESSION['auth']['id']; $internUserId = (int)$_SESSION['auth']['id'];
include("templates/header.inc.php");
if (!$user) { echo "<div class='container main-container'><h3>Erst anmelden: <a href=login.php>Login</a></h3><br>"; if (!$user) { echo "<div class='container main-container'><h3>Erst anmelden: <a href=login.php>Login</a></h3><br>";
include("templates/footer.inc.php"); include("templates/footer.inc.php");
@@ -18,22 +21,21 @@ include("templates/footer.inc.php");
?> ?>
<script src="/admin/js/tinymce/tinymce.min.js" referrerpolicy="origin"></script> <script src="/admin/js/tinymce/tinymce.min.js" referrerpolicy="origin"></script>
<div class="container main-container"> <div class="container main-container">
<?php <div style="display:flex; justify-content:space-between; align-items:flex-start; gap:20px; flex-wrap:wrap;">
##test2 <div>
echo '<div style="float: right; width: 200px; ">'; <h2>Administration - Anfragen</h2>
echo "<form action='". $_SERVER['PHP_SELF'] . "' id='formbenutzersuche' method=POST>"; Hallo <?php echo htmlentities($user['vorname']); ?>,<br>
echo '<input type="hidden" name="aktion" value="benutzersuche" />'; Herzlich Willkommen im internen Bereich!<br><br>
echo '<input type="hidden" name="userid_input" id="userid_input" />'; </div>
echo '<label>Benutzersuche Anfragen:</label> <div style="width:200px;">
<input type="text" id="user_input" name="skill_input" width="48"/>'; <form action="<?php echo htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8'); ?>" id="formbenutzersuche" method="POST">
//echo '<input type="submit" class="btn btn-primary" id="submitbox" value="" />'; <input type="hidden" name="aktion" value="benutzersuche" />
echo "</form>"; <input type="hidden" name="userid_input" id="userid_input" />
echo '</div>'; <label>Benutzersuche Anfragen:</label>
?> <input type="text" id="user_input" name="skill_input" width="48" />
<h2>Administration - Anfragen</h2> </form>
</div>
Hallo <?php echo htmlentities($user['vorname']); ?>,<br> </div>
Herzlich Willkommen im internen Bereich!<br><br>
@@ -41,13 +43,24 @@ Herzlich Willkommen im internen Bereich!<br><br>
<?php <?php
$aktion = $_POST["aktion"] ?? $_GET["aktion"] ?? '';
$artRequest = $_POST["art"] ?? $_GET["art"] ?? "1";
if ($aktion !== '') {
$_POST["aktion"] = $aktion;
}
if ($artRequest !== '') {
$_POST["art"] = $artRequest;
}
if(!check_worker()){ if(!check_worker()){
echo "<div class='container main-container'><h3>Erst anmelden: <a href=login.php>Login</a></h3><br>"; echo "<div class='container main-container'><h3>Erst anmelden: <a href=login.php>Login</a></h3><br>";
echo $_SESSION['userid']; echo $_SESSION['userid'];
}else{ }else{
if (($_POST["aktion"] ?? '') == "1") { if ($aktion == "1") {
echo "<header><h2>Anfragen bearbeiten</h2></header>"; echo "<header><h2>Anfragen bearbeiten</h2></header>";
@@ -93,7 +106,7 @@ if(!check_worker()){
$art = $_POST["art"] ?? "1"; $art = $artRequest;
// Default // Default
$sql = " $sql = "
@@ -623,12 +636,12 @@ if(!check_worker()){
echo "Start: <input class='form-control' name='Starttime[]' type='date' value='" . htmlspecialchars($start, ENT_QUOTES, 'UTF-8') . "'> echo "Start: <input class='form-control' name='Starttime[]' type='date' value='" . htmlspecialchars($start, ENT_QUOTES, 'UTF-8') . "'>
Ende: <input class='form-control' name='Endetime[]' type='date' value='" . htmlspecialchars($ende, ENT_QUOTES, 'UTF-8') . "'><br> Ende: <input class='form-control' name='Endetime[]' type='date' value='" . htmlspecialchars($ende, ENT_QUOTES, 'UTF-8') . "'><br>
Vertretung: <input class='form-control' name='vertretung[]' type='text' value='" . htmlspecialchars($vertretung, ENT_QUOTES, 'UTF-8') . "'>"; Vertretung: <input class='form-control' name='vertretung[]' type='text' required value='" . htmlspecialchars($vertretung, ENT_QUOTES, 'UTF-8') . "'>";
echo "<br>Vertretung Telefon: <input class='form-control' name='vertretertelefon[]' type='text' value='" . htmlspecialchars($vertretertelefon, ENT_QUOTES, 'UTF-8') . "'> echo "<br>Vertretung Telefon: <input class='form-control' name='vertretertelefon[]' type='text' required value='" . htmlspecialchars($vertretertelefon, ENT_QUOTES, 'UTF-8') . "'>
<br>Vertretung Adresse: <input class='form-control' name='vertreteradresse[]' type='text' value='" . htmlspecialchars($vertreteradresse, ENT_QUOTES, 'UTF-8') . "'>"; <br>Vertretung Adresse: <input class='form-control' name='vertreteradresse[]' type='text' required value='" . htmlspecialchars($vertreteradresse, ENT_QUOTES, 'UTF-8') . "'>";
echo "<br>Vertretung Webseite: <input class='form-control' name='vertreterurl[]' type='text' value='" . htmlspecialchars($vertreterurl, ENT_QUOTES, 'UTF-8') . "'>"; echo "<br>Vertretung Webseite: <input class='form-control' name='vertreterurl[]' type='text' required value='" . htmlspecialchars($vertreterurl, ENT_QUOTES, 'UTF-8') . "'>";
echo "<input name='urlaubid[]' type='hidden' value='" . $urlaubid . "'><br>"; echo "<input name='urlaubid[]' type='hidden' value='" . $urlaubid . "'><br>";
} }
@@ -655,10 +668,19 @@ if(!check_worker()){
}else if (($_POST["aktion"] ?? '') == "5") { }else if (($_POST["aktion"] ?? '') == "5") {
// Termine in DB speichern. // Termine in DB speichern.
$i =0; $i =0;
$pdo->beginTransaction();
try {
foreach ($_POST['Starttime'] as $Starttime) { foreach ($_POST['Starttime'] as $Starttime) {
//echo $datum . "<br>";
if($_POST["Starttime"][$i] != "0000-00-00"){ if($_POST["Starttime"][$i] != "0000-00-00"){
//echo $_POST["urlaubid"][$i] . "<br>"; $vertretung = trim((string)($_POST['vertretung'][$i] ?? ''));
$vertretertelefon = trim((string)($_POST['vertretertelefon'][$i] ?? ''));
$vertreteradresse = trim((string)($_POST['vertreteradresse'][$i] ?? ''));
$vertreterurl = trim((string)($_POST['vertreterurl'][$i] ?? ''));
if ($vertretung === '' || $vertretertelefon === '' || $vertreteradresse === '' || $vertreterurl === '') {
throw new RuntimeException("Bitte alle Vertreterinformationen fuer jeden Urlaubseintrag vollstaendig ausfuellen.");
}
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
INSERT INTO urlaub INSERT INTO urlaub
(urlaubid, vertretung, start, ende, vertretertelefon, vertreteradresse, vertreterurl) (urlaubid, vertretung, start, ende, vertretertelefon, vertreteradresse, vertreterurl)
@@ -675,20 +697,35 @@ if(!check_worker()){
$ok = $stmt->execute([ $ok = $stmt->execute([
':urlaubid' => (int)$_POST['urlaubid'][$i], // 0 = INSERT, >0 = UPDATE ':urlaubid' => (int)$_POST['urlaubid'][$i], // 0 = INSERT, >0 = UPDATE
':vertretung' => $_POST['vertretung'][$i], ':vertretung' => $vertretung,
':start' => $_POST['Starttime'][$i], ':start' => $_POST['Starttime'][$i],
':ende' => $_POST['Endetime'][$i], ':ende' => $_POST['Endetime'][$i],
':telefon' => $_POST['vertretertelefon'][$i], ':telefon' => $vertretertelefon,
':adresse' => $_POST['vertreteradresse'][$i], ':adresse' => $vertreteradresse,
':url' => $_POST['vertreterurl'][$i], ':url' => $vertreterurl,
]); ]);
if (!$ok) { if (!$ok) {
throw new RuntimeException("Fehler beim Eintragen in der Datenbank."); throw new RuntimeException("Fehler beim Eintragen in der Datenbank.");
} }
$urlaubId = (int)$_POST['urlaubid'][$i];
if ($urlaubId <= 0) {
$urlaubId = (int)$pdo->lastInsertId();
}
if ($urlaubId > 0) {
vacationSyncCompanyHolidayFromUrlaub($pdo, $urlaubId, $internUserId);
}
} }
$i++; $i++;
} }
$pdo->commit();
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $e;
}
echo "Einträge wurden in der Datenbank gespeichert!<br><br>"; echo "Einträge wurden in der Datenbank gespeichert!<br><br>";
}else if (($_POST["aktion"] ?? '') == "6") { }else if (($_POST["aktion"] ?? '') == "6") {
@@ -2025,7 +2062,7 @@ if(!check_worker()){
var div = document.getElementById('neuerUrlaub'); var div = document.getElementById('neuerUrlaub');
div.innerHTML += "Start: <input name=Starttime[] type=date class='form-control' > Ende: <input name=Endetime[] type=date class='form-control' >Vertretung: <input type=text name=vertretung[] weight=100 class='form-control'> Vertretung Telefon: <input type=text name=vertretertelefon[] weight=100 class='form-control'> Vertretung Adresse: <input type=text name=vertreteradresse[] weight=100 class='form-control'> Vertretung Webseite: <input type=text name=vertreterurl[] weight=100 class='form-control'> <input name=urlaubid[] type=hidden value='0'> <br>"; div.innerHTML += "Start: <input name=Starttime[] type=date class='form-control' > Ende: <input name=Endetime[] type=date class='form-control' >Vertretung: <input type=text name=vertretung[] weight=100 class='form-control' required> Vertretung Telefon: <input type=text name=vertretertelefon[] weight=100 class='form-control' required> Vertretung Adresse: <input type=text name=vertreteradresse[] weight=100 class='form-control' required> Vertretung Webseite: <input type=text name=vertreterurl[] weight=100 class='form-control' required> <input name=urlaubid[] type=hidden value='0'> <br>";
//Public: <select name=aktiv[] id='aktiv' required ><option value='1' >Ja</option> <option value='0'>Nein</option></select> //Public: <select name=aktiv[] id='aktiv' required ><option value='1' >Ja</option> <option value='0'>Nein</option></select>
//document.getElementById('neueTermine').innerHTML = div; //document.getElementById('neueTermine').innerHTML = div;
+288 -84
View File
@@ -4,6 +4,7 @@ session_start();
require_once __DIR__ . "/../inc/config.inc.php"; require_once __DIR__ . "/../inc/config.inc.php";
require_once __DIR__ . "/../inc/functions.inc.php"; require_once __DIR__ . "/../inc/functions.inc.php";
require_once __DIR__ . "/../inc/functions.impfen.inc.php"; require_once __DIR__ . "/../inc/functions.impfen.inc.php";
require_once __DIR__ . "/../inc/impfworkflow_notifications.inc.php";
$user = check_admin_user(); $user = check_admin_user();
include __DIR__ . "/templates/header.inc.php"; include __DIR__ . "/templates/header.inc.php";
@@ -78,11 +79,87 @@ function ensureWorkflowTables(PDO $pdo): void
impfWorkflowEnsureTables($pdo); impfWorkflowEnsureTables($pdo);
} }
function workflowDeleteWaitlistEntry(PDO $pdo, int $warteid): void
{
if ($warteid <= 0) {
return;
}
$stDeleteMap = $pdo->prepare("DELETE FROM warteliste_zeitraum WHERE warteid = :wid");
$stDeleteMap->execute(['wid' => $warteid]);
$stDelete = $pdo->prepare("DELETE FROM warteliste WHERE warteid = :wid");
$stDelete->execute(['wid' => $warteid]);
}
function workflowLoadWaitlistEntry(PDO $pdo, int $warteid): ?array
{
if ($warteid <= 0) {
return null;
}
$stWait = $pdo->prepare("SELECT w.warteid, w.userid, w.checked, p.vorname, p.nachname
FROM warteliste w
LEFT JOIN persons p ON p.person_id = w.userid
WHERE w.warteid = :wid
LIMIT 1");
$stWait->execute(['wid' => $warteid]);
$row = $stWait->fetch(PDO::FETCH_ASSOC);
return $row ?: null;
}
function workflowCountWaitersForPlan(PDO $pdo, int $impfstoffId, int $planId): int
{
return impfWorkflowNotificationCountWaitersForPlan($pdo, $impfstoffId, $planId);
}
function workflowLoadWaitRowsForPlan(PDO $pdo, int $impfstoffId, int $planId): array
{
$stW = $pdo->prepare("SELECT w.warteid, w.userid, w.hash, w.impfart, w.Impfaufklaerung, w.WeitereFragen, w.letzteimpfung, w.date_created
FROM warteliste w
WHERE w.checked = 1
AND (w.impfstoff = :iid OR w.impfstoff = 0)
AND EXISTS (
SELECT 1
FROM warteliste_zeitraum wz
WHERE wz.warteid = w.warteid
AND wz.zeitraum_id = :zid
)
ORDER BY w.date_created ASC, w.warteid ASC");
$stW->execute([
'iid' => $impfstoffId,
'zid' => $planId,
]);
return $stW->fetchAll(PDO::FETCH_ASSOC);
}
function workflowCountStrictWaitersForPlan(PDO $pdo, int $impfstoffId, int $planId): int
{
$stW = $pdo->prepare("SELECT COUNT(DISTINCT w.userid)
FROM warteliste w
WHERE w.checked = 1
AND (w.impfstoff = :iid OR w.impfstoff = 0)
AND EXISTS (
SELECT 1
FROM warteliste_zeitraum wz
WHERE wz.warteid = w.warteid
AND wz.zeitraum_id = :zid
)");
$stW->execute([
'iid' => $impfstoffId,
'zid' => $planId,
]);
return (int)$stW->fetchColumn();
}
function workflowAddWartelisteEntry( function workflowAddWartelisteEntry(
PDO $pdo, PDO $pdo,
int $personId, int $personId,
int $impfstoffId, int $impfstoffId,
int $planId, $planIds,
int $impfart, int $impfart,
?string $letzteImpfung, ?string $letzteImpfung,
int $checked int $checked
@@ -92,7 +169,7 @@ function workflowAddWartelisteEntry(
} }
$impfstoffId = max(0, $impfstoffId); $impfstoffId = max(0, $impfstoffId);
$planId = max(0, $planId); $planIds = impfNormalizeZeitraumIds($planIds);
$impfart = ($impfart >= 1 && $impfart <= 4) ? $impfart : 1; $impfart = ($impfart >= 1 && $impfart <= 4) ? $impfart : 1;
if ($impfart > 1 && !$letzteImpfung) { if ($impfart > 1 && !$letzteImpfung) {
$impfart = 1; $impfart = 1;
@@ -112,10 +189,12 @@ function workflowAddWartelisteEntry(
$impfstoffName = 'ohne Vorgabe'; $impfstoffName = 'ohne Vorgabe';
$zeitraum = 'Flexibel'; $zeitraum = 'Flexibel';
if ($planId > 0) { if (!empty($planIds)) {
$zeitraumLabels = [];
foreach ($planIds as $planId) {
$plan = impfLoadZeitraumById($pdo, $planId, true); $plan = impfLoadZeitraumById($pdo, $planId, true);
if (!$plan) { if (!$plan) {
return [false, "Das ausgewaehlte Zeitfenster ist nicht mehr verfuegbar."]; return [false, "Mindestens ein ausgewaehltes Zeitfenster ist nicht mehr verfuegbar."];
} }
$zugeordneteImpfstoffe = $plan['impfstoff_id_list'] ?? []; $zugeordneteImpfstoffe = $plan['impfstoff_id_list'] ?? [];
@@ -125,25 +204,24 @@ function workflowAddWartelisteEntry(
if ($impfstoffId <= 0) { if ($impfstoffId <= 0) {
if (count($zugeordneteImpfstoffe) !== 1) { if (count($zugeordneteImpfstoffe) !== 1) {
return [false, "Bitte einen Impfstoff auswaehlen, der dem Zeitfenster zugeordnet ist."]; return [false, "Bitte einen Impfstoff auswaehlen, der allen Zeitfenstern eindeutig zugeordnet ist."];
} }
$impfstoffId = (int)$zugeordneteImpfstoffe[0]; $currentImpfstoffId = (int)$zugeordneteImpfstoffe[0];
if ($impfstoffValue > 0 && $impfstoffValue !== $currentImpfstoffId) {
return [false, "Die ausgewaehlten Zeitfenster gehoeren zu unterschiedlichen Impfstoffen."];
}
$impfstoffValue = $currentImpfstoffId;
} }
$stImpfstoff = $pdo->prepare("SELECT impfid, impfname $zeitraumLabels[] = workflowPlanLabel($plan);
FROM impfstoff
WHERE impfid = :iid
LIMIT 1");
$stImpfstoff->execute(['iid' => $impfstoffId]);
$impfstoff = $stImpfstoff->fetch(PDO::FETCH_ASSOC);
if (!$impfstoff) {
return [false, "Der ausgewaehlte Impfstoff wurde nicht gefunden."];
} }
$impfstoffValue = $impfstoffId; if ($impfstoffId <= 0) {
$impfstoffName = (string)$impfstoff['impfname']; $impfstoffId = $impfstoffValue;
$zeitraum = workflowPlanLabel($plan); }
} elseif ($impfstoffId > 0) { }
if ($impfstoffId > 0) {
$stImpfstoff = $pdo->prepare("SELECT impfid, impfname $stImpfstoff = $pdo->prepare("SELECT impfid, impfname
FROM impfstoff FROM impfstoff
WHERE impfid = :iid WHERE impfid = :iid
@@ -158,6 +236,10 @@ function workflowAddWartelisteEntry(
$impfstoffValue = 0; $impfstoffValue = 0;
} }
if (!empty($planIds)) {
$zeitraum = implode(' | ', $zeitraumLabels);
}
$stDup = $pdo->prepare("SELECT warteid $stDup = $pdo->prepare("SELECT warteid
FROM warteliste FROM warteliste
WHERE userid = :uid WHERE userid = :uid
@@ -172,7 +254,7 @@ function workflowAddWartelisteEntry(
$hash = md5('admin-warte-' . $personId . '-' . microtime(true) . '-' . random_int(1000, 9999)); $hash = md5('admin-warte-' . $personId . '-' . microtime(true) . '-' . random_int(1000, 9999));
$checkedValue = ($checked === 0) ? 0 : 1; $checkedValue = ($checked === 0) ? 0 : 1;
$letzteValue = ($impfart === 1) ? null : ($letzteImpfung ?: null); $letzteValue = ($impfart === 1) ? null : ($letzteImpfung ?: null);
$zeitraumIdValue = ($planId > 0) ? $planId : null; $zeitraumIdValue = !empty($planIds) ? (int)$planIds[0] : null;
$stInsert = $pdo->prepare("INSERT INTO warteliste $stInsert = $pdo->prepare("INSERT INTO warteliste
(userid, checked, hash, impfenangebot, impfstoff, Patientenart, Impfaufklaerung, WeitereFragen, impfart, impfenmit, letzteimpfung, impfenzeitraum, zeitraum_id, date_created) (userid, checked, hash, impfenangebot, impfstoff, Patientenart, Impfaufklaerung, WeitereFragen, impfart, impfenmit, letzteimpfung, impfenzeitraum, zeitraum_id, date_created)
@@ -190,6 +272,11 @@ function workflowAddWartelisteEntry(
'zeitraum_id' => $zeitraumIdValue, 'zeitraum_id' => $zeitraumIdValue,
]); ]);
$warteid = (int)$pdo->lastInsertId();
if (!empty($planIds)) {
impfSetWartelistenZeitraeume($pdo, $warteid, $planIds);
}
$personName = trim((string)$person['vorname'] . ' ' . (string)$person['nachname']); $personName = trim((string)$person['vorname'] . ' ' . (string)$person['nachname']);
return [true, "Wartelistenplatz fuer {$personName} ({$impfstoffName}) gespeichert."]; return [true, "Wartelistenplatz fuer {$personName} ({$impfstoffName}) gespeichert."];
} }
@@ -235,16 +322,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} else { } else {
$dosen = (int)$rule['dosen_pro_flasche']; $dosen = (int)$rule['dosen_pro_flasche'];
$stCount = $pdo->prepare("SELECT COUNT(DISTINCT userid) $wartende = workflowCountWaitersForPlan($pdo, $impfstoffId, $planId);
FROM warteliste
WHERE checked = 1
AND (impfstoff = :iid OR impfstoff = 0)
AND (zeitraum_id = :zid OR zeitraum_id IS NULL)");
$stCount->execute([
'iid' => $impfstoffId,
'zid' => $planId,
]);
$wartende = (int)$stCount->fetchColumn();
if ($wartende < $dosen) { if ($wartende < $dosen) {
$error = "Nicht genug bestätigte Warteteilnehmer: {$wartende} von {$dosen}."; $error = "Nicht genug bestätigte Warteteilnehmer: {$wartende} von {$dosen}.";
@@ -286,16 +364,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
]); ]);
$timeid = (int)$pdo->lastInsertId(); $timeid = (int)$pdo->lastInsertId();
$stW = $pdo->prepare("SELECT warteid, userid, hash, impfart, Impfaufklaerung, WeitereFragen, letzteimpfung $warteRowsRaw = workflowLoadWaitRowsForPlan($pdo, $impfstoffId, $planId);
FROM warteliste
WHERE checked = 1
AND (impfstoff = :iid OR impfstoff = 0)
AND (zeitraum_id = :zid OR zeitraum_id IS NULL)
ORDER BY date_created ASC, warteid ASC");
$stW->bindValue(':iid', $impfstoffId, PDO::PARAM_INT);
$stW->bindValue(':zid', $planId, PDO::PARAM_INT);
$stW->execute();
$warteRowsRaw = $stW->fetchAll(PDO::FETCH_ASSOC);
$warteRows = []; $warteRows = [];
$seenUserIds = []; $seenUserIds = [];
foreach ($warteRowsRaw as $warteRow) { foreach ($warteRowsRaw as $warteRow) {
@@ -333,6 +402,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
]); ]);
$terminIds[] = (int)$pdo->lastInsertId(); $terminIds[] = (int)$pdo->lastInsertId();
$stDelW->execute(['wid' => (int)$w['warteid']]); $stDelW->execute(['wid' => (int)$w['warteid']]);
$pdo->prepare("DELETE FROM warteliste_zeitraum WHERE warteid = :wid")
->execute(['wid' => (int)$w['warteid']]);
} }
$stReduce = $pdo->prepare("UPDATE timeslots SET impfdosen = GREATEST(impfdosen - :cnt, 0) WHERE timeid = :timeid"); $stReduce = $pdo->prepare("UPDATE timeslots SET impfdosen = GREATEST(impfdosen - :cnt, 0) WHERE timeid = :timeid");
@@ -344,6 +415,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
SendMailMessageVorlage($pdo, '1', $tid, $mailTemplateId); SendMailMessageVorlage($pdo, '1', $tid, $mailTemplateId);
} }
impfWorkflowNotificationProcess($pdo);
$message = count($terminIds) . " Terminanfragen wurden erstellt und versendet."; $message = count($terminIds) . " Terminanfragen wurden erstellt und versendet.";
} catch (Throwable $e) { } catch (Throwable $e) {
if ($pdo->inTransaction()) { if ($pdo->inTransaction()) {
@@ -363,19 +435,23 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} elseif ($aktion === 'add_waitlist_existing') { } elseif ($aktion === 'add_waitlist_existing') {
$personId = (int)($_POST['wl_person_id'] ?? 0); $personId = (int)($_POST['wl_person_id'] ?? 0);
$impfstoffId = (int)($_POST['wl_impfstoff_id'] ?? 0); $impfstoffId = (int)($_POST['wl_impfstoff_id'] ?? 0);
$planId = (int)($_POST['wl_plan_id'] ?? 0); $planIds = impfNormalizeZeitraumIds($_POST['wl_plan_ids'] ?? ($_POST['wl_plan_id'] ?? []));
[$ok, $msg] = workflowAddWartelisteEntry( [$ok, $msg] = workflowAddWartelisteEntry(
$pdo, $pdo,
$personId, $personId,
$impfstoffId, $impfstoffId,
$planId, $planIds,
1, 1,
null, null,
1 1
); );
if ($ok) { if ($ok) {
$notificationEvents = impfWorkflowNotificationProcess($pdo);
$message = $msg; $message = $msg;
if (!empty($notificationEvents)) {
$message .= ' ' . count($notificationEvents) . " Impfworkflow-Benachrichtigung(en) wurden versendet.";
}
} else { } else {
$error = $msg; $error = $msg;
} }
@@ -391,7 +467,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$patientenart = ((int)($_POST['new_patientenart'] ?? 0) === 1) ? 1 : 0; $patientenart = ((int)($_POST['new_patientenart'] ?? 0) === 1) ? 1 : 0;
$impfstoffId = (int)($_POST['new_impfstoff_id'] ?? 0); $impfstoffId = (int)($_POST['new_impfstoff_id'] ?? 0);
$planId = (int)($_POST['new_plan_id'] ?? 0); $planIds = impfNormalizeZeitraumIds($_POST['new_plan_ids'] ?? ($_POST['new_plan_id'] ?? []));
if ($vorname === '' || $nachname === '' || $geburtstag === '') { if ($vorname === '' || $nachname === '' || $geburtstag === '') {
$error = "Für neue Patienten sind Vorname, Nachname und Geburtstag erforderlich."; $error = "Für neue Patienten sind Vorname, Nachname und Geburtstag erforderlich.";
@@ -414,13 +490,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$pdo, $pdo,
$personId, $personId,
$impfstoffId, $impfstoffId,
$planId, $planIds,
1, 1,
null, null,
1 1
); );
if ($ok) { if ($ok) {
$notificationEvents = impfWorkflowNotificationProcess($pdo);
$message = $msg; $message = $msg;
if (!empty($notificationEvents)) {
$message .= ' ' . count($notificationEvents) . " Impfworkflow-Benachrichtigung(en) wurden versendet.";
}
} else { } else {
$error = $msg; $error = $msg;
} }
@@ -529,27 +609,88 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($warteid <= 0) { if ($warteid <= 0) {
$error = "Ungültiger Wartelisten-Eintrag."; $error = "Ungültiger Wartelisten-Eintrag.";
} else { } else {
$stDelete = $pdo->prepare("DELETE FROM warteliste WHERE warteid = :wid"); $stDelete = $pdo->prepare("SELECT warteid FROM warteliste WHERE warteid = :wid");
$stDelete->execute(['wid' => $warteid]); $stDelete->execute(['wid' => $warteid]);
if ($stDelete->rowCount() > 0) { $exists = (bool)$stDelete->fetch(PDO::FETCH_ASSOC);
workflowDeleteWaitlistEntry($pdo, $warteid);
if ($exists) {
impfWorkflowNotificationProcess($pdo);
$message = "Wartelisten-Eintrag wurde gelöscht."; $message = "Wartelisten-Eintrag wurde gelöscht.";
} else { } else {
$error = "Wartelisten-Eintrag nicht gefunden."; $error = "Wartelisten-Eintrag nicht gefunden.";
} }
} }
} elseif ($aktion === 'confirm_waitlist') {
$warteid = (int)($_POST['warteid'] ?? 0);
if ($warteid <= 0) {
$error = "Ungültiger Wartelisten-Eintrag.";
} else {
$waitRow = workflowLoadWaitlistEntry($pdo, $warteid);
if (!$waitRow) {
$error = "Wartelisten-Eintrag nicht gefunden.";
} elseif ((int)$waitRow['checked'] >= 1) {
$error = "Der Wartelisten-Eintrag ist bereits bestätigt.";
} else {
$stUpdate = $pdo->prepare("UPDATE warteliste
SET checked = 1
WHERE warteid = :wid
AND checked < 1");
$stUpdate->execute(['wid' => $warteid]);
if ($stUpdate->rowCount() < 1) {
$error = "Wartelisten-Eintrag konnte nicht bestätigt werden.";
} else {
SendMailMessageVorlage($pdo, '2', $warteid, '9');
$notificationEvents = impfWorkflowNotificationProcess($pdo);
$personName = trim((string)($waitRow['vorname'] ?? '') . ' ' . (string)($waitRow['nachname'] ?? ''));
$message = "Wartelisten-Eintrag für " . trim($personName) . " wurde bestätigt.";
if (!empty($notificationEvents)) {
$message .= ' ' . count($notificationEvents) . " Impfworkflow-Benachrichtigung(en) wurden versendet.";
}
}
}
}
} elseif ($aktion === 'cancel_waitlist') {
$warteid = (int)($_POST['warteid'] ?? 0);
if ($warteid <= 0) {
$error = "Ungültiger Wartelisten-Eintrag.";
} else {
$waitRow = workflowLoadWaitlistEntry($pdo, $warteid);
if (!$waitRow) {
$error = "Wartelisten-Eintrag nicht gefunden.";
} else {
SendMailMessageVorlage($pdo, '2', $warteid, '10');
workflowDeleteWaitlistEntry($pdo, $warteid);
$notificationEvents = impfWorkflowNotificationProcess($pdo);
$personName = trim((string)($waitRow['vorname'] ?? '') . ' ' . (string)($waitRow['nachname'] ?? ''));
$message = "Wartelisten-Eintrag für " . trim($personName) . " wurde abgesagt.";
if (!empty($notificationEvents)) {
$message .= ' ' . count($notificationEvents) . " Impfworkflow-Benachrichtigung(en) wurden versendet.";
}
}
}
} }
} }
$rules = []; $rules = [];
$plans = []; $plans = [];
$configuredImpfstoffe = []; $configuredImpfstoffe = [];
$configuredImpfstoffNames = [];
$eligible = []; $eligible = [];
$personResults = []; $personResults = [];
$waitRows = []; $waitRows = [];
$upcomingRows = []; $upcomingRows = [];
$eventOverview = []; $eventOverview = [];
$planWaitCounts = [];
try { try {
$needsPlanData = in_array($view, ['teilnehmer', 'event-create'], true);
$needsEligibilityData = ($view === 'event-create');
$needsPersonSearch = ($view === 'teilnehmer' && $personSearch !== '');
$needsWaitRows = ($view === 'warteliste');
$needsUpcomingRows = ($view === 'event-teilnehmer');
if ($needsPlanData) {
$stRules = $pdo->prepare("SELECT r.impfstoff_id, r.dosen_pro_flasche, i.impfname, $stRules = $pdo->prepare("SELECT r.impfstoff_id, r.dosen_pro_flasche, i.impfname,
COALESCE((SELECT COUNT(DISTINCT w.userid) FROM warteliste w WHERE w.checked = 1 AND (w.impfstoff = r.impfstoff_id OR w.impfstoff = 0)),0) AS wartende COALESCE((SELECT COUNT(DISTINCT w.userid) FROM warteliste w WHERE w.checked = 1 AND (w.impfstoff = r.impfstoff_id OR w.impfstoff = 0)),0) AS wartende
FROM impfstoff_workflow r FROM impfstoff_workflow r
@@ -570,16 +711,41 @@ try {
foreach ($rules as $r) { foreach ($rules as $r) {
$iid = (int)$r['impfstoff_id']; $iid = (int)$r['impfstoff_id'];
$dosen = (int)$r['dosen_pro_flasche']; $configuredImpfstoffNames[$iid] = (string)$r['impfname'];
if ($dosen > 0 && isset($planExistsForImpfstoff[$iid])) { if (!isset($planExistsForImpfstoff[$iid])) {
continue;
}
$configuredImpfstoffe[] = $r; $configuredImpfstoffe[] = $r;
if ((int)$r['wartende'] >= $dosen) { if (!$needsEligibilityData) {
continue;
}
$dosen = (int)$r['dosen_pro_flasche'];
if ($dosen <= 0) {
continue;
}
$hasEligiblePlan = false;
foreach ($plans as $plan) {
if (!in_array($iid, $plan['impfstoff_id_list'] ?? [], true)) {
continue;
}
$planId = (int)$plan['zeitraum_id'];
$planWaitCounts[$iid][$planId] = workflowCountStrictWaitersForPlan($pdo, $iid, $planId);
if ($planWaitCounts[$iid][$planId] >= $dosen) {
$hasEligiblePlan = true;
}
}
if ($hasEligiblePlan) {
$eligible[] = $r; $eligible[] = $r;
} }
} }
} }
if ($personSearch !== '') { if ($needsPersonSearch) {
$searchLike = '%' . $personSearch . '%'; $searchLike = '%' . $personSearch . '%';
$searchExactId = ctype_digit($personSearch) ? (int)$personSearch : -1; $searchExactId = ctype_digit($personSearch) ? (int)$personSearch : -1;
$stPersons = $pdo->prepare("SELECT person_id, vorname, nachname, geburtstag, email, tele, ort, plz, strasse $stPersons = $pdo->prepare("SELECT person_id, vorname, nachname, geburtstag, email, tele, ort, plz, strasse
@@ -598,6 +764,7 @@ try {
$personResults = $stPersons->fetchAll(PDO::FETCH_ASSOC); $personResults = $stPersons->fetchAll(PDO::FETCH_ASSOC);
} }
if ($needsWaitRows) {
$stWait = $pdo->prepare("SELECT w.warteid, w.userid, w.checked, w.impfstoff, w.impfart, w.impfenzeitraum, w.zeitraum_id, w.letzteimpfung, w.date_created, $stWait = $pdo->prepare("SELECT w.warteid, w.userid, w.checked, w.impfstoff, w.impfart, w.impfenzeitraum, w.zeitraum_id, w.letzteimpfung, w.date_created,
p.vorname, p.nachname, p.geburtstag, p.email, p.tele, p.vorname, p.nachname, p.geburtstag, p.email, p.tele,
i.impfname i.impfname
@@ -610,17 +777,22 @@ try {
$stWait->execute(); $stWait->execute();
$waitRows = $stWait->fetchAll(PDO::FETCH_ASSOC); $waitRows = $stWait->fetchAll(PDO::FETCH_ASSOC);
$waitIds = array_map(static function (array $waitRow): int {
return (int)($waitRow['warteid'] ?? 0);
}, $waitRows);
$waitLabelsById = impfGetWartelistenZeitraeumeLabelsMap($pdo, $waitIds, false);
foreach ($waitRows as &$waitRow) { foreach ($waitRows as &$waitRow) {
$zeitraumId = (int)($waitRow['zeitraum_id'] ?? 0); $warteid = (int)($waitRow['warteid'] ?? 0);
if ($zeitraumId > 0) { $waitRow['zeitraum_labels'] = $waitLabelsById[$warteid] ?? [];
$zeitraum = impfLoadZeitraumById($pdo, $zeitraumId, true); if (!empty($waitRow['zeitraum_labels'])) {
if ($zeitraum) { $waitRow['impfenzeitraum'] = implode(' | ', $waitRow['zeitraum_labels']);
$waitRow['impfenzeitraum'] = $zeitraum['label'];
}
} }
} }
unset($waitRow); unset($waitRow);
}
if ($needsUpcomingRows) {
$stUpcoming = $pdo->prepare("SELECT ts.timeid, ts.date, ts.start, ts.ende, ts.impfdosen, $stUpcoming = $pdo->prepare("SELECT ts.timeid, ts.date, ts.start, ts.ende, ts.impfdosen,
i.impfname, o.anzeigename, o.adresse, i.impfname, o.anzeigename, o.adresse,
it.terminid, it.checked, it.behandelt, it.impfart, it.terminid, it.checked, it.behandelt, it.impfart,
@@ -665,6 +837,7 @@ try {
]; ];
} }
} }
}
} catch (Throwable $e) { } catch (Throwable $e) {
$error = trim($error . ' Fehler beim Laden der Impfverwaltung: ' . $e->getMessage()); $error = trim($error . ' Fehler beim Laden der Impfverwaltung: ' . $e->getMessage());
} }
@@ -728,7 +901,13 @@ try {
<?php echo esc((string)$w['tele']); ?> <?php echo esc((string)$w['tele']); ?>
</td> </td>
<td><?php echo esc((string)($w['impfname'] ?: 'Unbekannt')); ?></td> <td><?php echo esc((string)($w['impfname'] ?: 'Unbekannt')); ?></td>
<td><?php echo esc((string)$w['impfenzeitraum']); ?></td> <td>
<?php if (!empty($w['zeitraum_labels'])): ?>
<?php echo implode('<br>', array_map('esc', $w['zeitraum_labels'])); ?>
<?php else: ?>
<?php echo esc(impfLimitLabelLength((string)$w['impfenzeitraum'], 50)); ?>
<?php endif; ?>
</td>
<td> <td>
<?php echo esc(workflowImpfartName((int)$w['impfart'])); ?> <?php echo esc(workflowImpfartName((int)$w['impfart'])); ?>
<?php if (!empty($w['letzteimpfung'])): ?> <?php if (!empty($w['letzteimpfung'])): ?>
@@ -738,7 +917,19 @@ try {
<td><?php echo esc(workflowWarteStatus((int)$w['checked'])); ?></td> <td><?php echo esc(workflowWarteStatus((int)$w['checked'])); ?></td>
<td><?php echo esc((string)$w['date_created']); ?></td> <td><?php echo esc((string)$w['date_created']); ?></td>
<td> <td>
<form method="post" onsubmit="return confirm('Eintrag wirklich löschen?');"> <?php if ((int)$w['checked'] === 0): ?>
<form method="post" style="display:inline-block; margin-right:6px;" onsubmit="return confirm('Eintrag wirklich als bestätigt markieren?');">
<input type="hidden" name="aktion" value="confirm_waitlist">
<input type="hidden" name="warteid" value="<?php echo (int)$w['warteid']; ?>">
<button class="btn btn-success btn-xs" type="submit">Bestätigen</button>
</form>
<?php endif; ?>
<form method="post" style="display:inline-block; margin-right:6px;" onsubmit="return confirm('Wartelisten-Eintrag wirklich absagen?');">
<input type="hidden" name="aktion" value="cancel_waitlist">
<input type="hidden" name="warteid" value="<?php echo (int)$w['warteid']; ?>">
<button class="btn btn-warning btn-xs" type="submit">Absagen</button>
</form>
<form method="post" style="display:inline-block;" onsubmit="return confirm('Eintrag wirklich löschen?');">
<input type="hidden" name="aktion" value="delete_waitlist"> <input type="hidden" name="aktion" value="delete_waitlist">
<input type="hidden" name="warteid" value="<?php echo (int)$w['warteid']; ?>"> <input type="hidden" name="warteid" value="<?php echo (int)$w['warteid']; ?>">
<button class="btn btn-danger btn-xs" type="submit">Löschen</button> <button class="btn btn-danger btn-xs" type="submit">Löschen</button>
@@ -774,10 +965,11 @@ try {
<?php endif; ?> <?php endif; ?>
<?php endif; ?> <?php endif; ?>
<form method="post" class="form-inline"> <form method="post">
<input type="hidden" name="aktion" value="add_waitlist_existing"> <input type="hidden" name="aktion" value="add_waitlist_existing">
<input type="hidden" name="person_search" value="<?php echo esc($personSearch); ?>"> <input type="hidden" name="person_search" value="<?php echo esc($personSearch); ?>">
<div class="form-group" style="display:block; margin-bottom:10px;">
<label>Patient</label> <label>Patient</label>
<select class="form-control" name="wl_person_id" required <?php echo empty($personResults) ? 'disabled' : ''; ?>> <select class="form-control" name="wl_person_id" required <?php echo empty($personResults) ? 'disabled' : ''; ?>>
<option value="">Bitte wählen</option> <option value="">Bitte wählen</option>
@@ -790,8 +982,10 @@ try {
<option value="<?php echo $pid; ?>" <?php echo $selected; ?>><?php echo esc($personLabel); ?></option> <option value="<?php echo $pid; ?>" <?php echo $selected; ?>><?php echo esc($personLabel); ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div>
<label style="margin-left:10px;">Impfstoff</label> <div class="form-group" style="display:block; margin-bottom:10px;">
<label>Impfstoff</label>
<select class="form-control" name="wl_impfstoff_id" id="existing_impfstoff"> <select class="form-control" name="wl_impfstoff_id" id="existing_impfstoff">
<option value="0" <?php echo ((int)($_POST['wl_impfstoff_id'] ?? 0) === 0) ? 'selected' : ''; ?>>Keine Vorgabe</option> <option value="0" <?php echo ((int)($_POST['wl_impfstoff_id'] ?? 0) === 0) ? 'selected' : ''; ?>>Keine Vorgabe</option>
<?php foreach ($configuredImpfstoffe as $r): ?> <?php foreach ($configuredImpfstoffe as $r): ?>
@@ -804,22 +998,25 @@ try {
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div>
<label style="margin-left:10px;">Zeitfenster</label> <div class="form-group" style="display:block;">
<select class="form-control" name="wl_plan_id" id="existing_plan"> <label>Zeitfenster</label>
<option value="0" <?php echo ((int)($_POST['wl_plan_id'] ?? 0) === 0) ? 'selected' : ''; ?>>Ohne Zeitfenster</option> <?php $selectedExistingPlanIds = impfNormalizeZeitraumIds($_POST['wl_plan_ids'] ?? ($_POST['wl_plan_id'] ?? [])); ?>
<select class="form-control" name="wl_plan_ids[]" id="existing_plan" multiple size="6">
<?php foreach ($plans as $p): ?> <?php foreach ($plans as $p): ?>
<?php <?php
$planId = (int)$p['zeitraum_id']; $planId = (int)$p['zeitraum_id'];
$selected = ((int)($_POST['wl_plan_id'] ?? 0) === $planId) ? 'selected' : ''; $selected = in_array($planId, $selectedExistingPlanIds, true) ? 'selected' : '';
$impfstoffeCsv = implode(',', $p['impfstoff_id_list']); $impfstoffeCsv = implode(',', $p['impfstoff_id_list']);
$impfstoffeText = empty($p['impfstoff_name_list']) ? 'ohne Impfstoff' : implode(', ', $p['impfstoff_name_list']); $optionLabel = workflowPlanLabel($p);
?> ?>
<option value="<?php echo $planId; ?>" data-impfstoffe="<?php echo esc($impfstoffeCsv); ?>" <?php echo $selected; ?>> <option value="<?php echo $planId; ?>" data-impfstoffe="<?php echo esc($impfstoffeCsv); ?>" <?php echo $selected; ?>>
<?php echo esc(workflowPlanLabel($p) . ' | Impfstoffe: ' . $impfstoffeText); ?> <?php echo esc($optionLabel); ?>
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div>
<div class="workflow-action-row"> <div class="workflow-action-row">
<button class="btn btn-primary" type="submit" <?php echo empty($personResults) ? 'disabled' : ''; ?>> <button class="btn btn-primary" type="submit" <?php echo empty($personResults) ? 'disabled' : ''; ?>>
Zur Warteliste hinzufügen Zur Warteliste hinzufügen
@@ -897,17 +1094,17 @@ try {
</div> </div>
<div class="col-sm-3"> <div class="col-sm-3">
<label>Zeitfenster</label> <label>Zeitfenster</label>
<select class="form-control" name="new_plan_id" id="new_plan"> <?php $selectedNewPlanIds = impfNormalizeZeitraumIds($_POST['new_plan_ids'] ?? ($_POST['new_plan_id'] ?? [])); ?>
<option value="0" <?php echo ((int)($_POST['new_plan_id'] ?? 0) === 0) ? 'selected' : ''; ?>>Ohne Zeitfenster</option> <select class="form-control" name="new_plan_ids[]" id="new_plan" multiple size="6">
<?php foreach ($plans as $p): ?> <?php foreach ($plans as $p): ?>
<?php <?php
$planId = (int)$p['zeitraum_id']; $planId = (int)$p['zeitraum_id'];
$selected = ((int)($_POST['new_plan_id'] ?? 0) === $planId) ? 'selected' : ''; $selected = in_array($planId, $selectedNewPlanIds, true) ? 'selected' : '';
$impfstoffeCsv = implode(',', $p['impfstoff_id_list']); $impfstoffeCsv = implode(',', $p['impfstoff_id_list']);
$impfstoffeText = empty($p['impfstoff_name_list']) ? 'ohne Impfstoff' : implode(', ', $p['impfstoff_name_list']); $optionLabel = workflowPlanLabel($p);
?> ?>
<option value="<?php echo $planId; ?>" data-impfstoffe="<?php echo esc($impfstoffeCsv); ?>" <?php echo $selected; ?>> <option value="<?php echo $planId; ?>" data-impfstoffe="<?php echo esc($impfstoffeCsv); ?>" <?php echo $selected; ?>>
<?php echo esc(workflowPlanLabel($p) . ' | Impfstoffe: ' . $impfstoffeText); ?> <?php echo esc($optionLabel); ?>
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
@@ -935,7 +1132,7 @@ try {
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><strong>Konkretes Impfevent erstellen</strong></div> <div class="panel-heading"><strong>Konkretes Impfevent erstellen</strong></div>
<div class="panel-body"> <div class="panel-body">
<p>Es werden nur Impfstoffe angeboten, bei denen die bestätigte Warteliste mindestens eine Flasche füllt.</p> <p>Es werden nur Impfstoffe und Zeiträume angeboten, bei denen die bestätigte Warteliste mindestens eine Flasche füllt und die Wartenden diesen Zeitraum auch ausdrücklich gewählt haben.</p>
<form method="post" class="form-inline" style="margin-bottom:14px;"> <form method="post" class="form-inline" style="margin-bottom:14px;">
<input type="hidden" name="aktion" value="create_event"> <input type="hidden" name="aktion" value="create_event">
@@ -951,14 +1148,18 @@ try {
<select class="form-control" name="plan_id" id="event_plan" required> <select class="form-control" name="plan_id" id="event_plan" required>
<option value="">Bitte wählen</option> <option value="">Bitte wählen</option>
<?php foreach ($plans as $p): ?> <?php foreach ($plans as $p): ?>
<?php foreach (($p['impfstoff_id_list'] ?? []) as $planImpfstoffId): ?>
<?php <?php
$impfstoffeCsv = implode(',', $p['impfstoff_id_list']); $waiterCount = (int)($planWaitCounts[(int)$planImpfstoffId][(int)$p['zeitraum_id']] ?? 0);
$impfstoffeText = empty($p['impfstoff_name_list']) ? 'ohne Impfstoff' : implode(', ', $p['impfstoff_name_list']); if ($waiterCount <= 0) {
continue;
}
?> ?>
<option value="<?php echo (int)$p['zeitraum_id']; ?>" data-impfstoffe="<?php echo esc($impfstoffeCsv); ?>"> <option value="<?php echo (int)$p['zeitraum_id']; ?>" data-impfstoffe="<?php echo esc((string)$planImpfstoffId); ?>">
<?php echo esc(workflowPlanLabel($p) . ' | Impfstoffe: ' . $impfstoffeText); ?> <?php echo esc(workflowPlanLabel($p) . ' | Impfstoff: ' . ($configuredImpfstoffNames[(int)$planImpfstoffId] ?? 'Unbekannt') . ' | Wartende: ' . $waiterCount); ?>
</option> </option>
<?php endforeach; ?> <?php endforeach; ?>
<?php endforeach; ?>
</select> </select>
<label style="margin-left:10px;">Konkretes Datum</label> <label style="margin-left:10px;">Konkretes Datum</label>
@@ -1061,15 +1262,18 @@ try {
function filterPlans() { function filterPlans() {
var val = impfstoff.value; var val = impfstoff.value;
var needsFilter = (val !== "" && val !== "0"); var needsFilter = (val !== "" && val !== "0");
var selectedValid = false; var selectedValues = [];
for (var s = 0; s < plan.options.length; s++) {
if (plan.options[s].selected && plan.options[s].value !== '') {
selectedValues.push(plan.options[s].value);
}
}
var selectedValid = 0;
for (var i = 0; i < plan.options.length; i++) { for (var i = 0; i < plan.options.length; i++) {
var opt = plan.options[i]; var opt = plan.options[i];
var optionImpfstoffe = opt.getAttribute('data-impfstoffe'); var optionImpfstoffe = opt.getAttribute('data-impfstoffe');
if (!optionImpfstoffe) { if (!optionImpfstoffe) {
opt.hidden = false; opt.hidden = false;
if (opt.value === plan.value) {
selectedValid = true;
}
continue; continue;
} }
var ids = optionImpfstoffe.split(','); var ids = optionImpfstoffe.split(',');
@@ -1083,11 +1287,13 @@ try {
} }
} }
opt.hidden = !visible; opt.hidden = !visible;
if (visible && opt.value === plan.value) { if (!visible) {
selectedValid = true; opt.selected = false;
} else if (selectedValues.indexOf(opt.value) !== -1) {
selectedValid++;
} }
} }
if (!selectedValid) { if (selectedValues.length > 0 && selectedValid === 0) {
plan.value = ''; plan.value = '';
} }
} }
@@ -1103,5 +1309,3 @@ try {
</div> </div>
<?php include __DIR__ . "/templates/footer.inc.php"; ?> <?php include __DIR__ . "/templates/footer.inc.php"; ?>
+6 -1
View File
@@ -289,6 +289,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($aktion === 'add_zeitraum') { if ($aktion === 'add_zeitraum') {
$bezeichnung = trim((string)($_POST['bezeichnung'] ?? '')); $bezeichnung = trim((string)($_POST['bezeichnung'] ?? ''));
if (function_exists('mb_substr')) {
$bezeichnung = mb_substr($bezeichnung, 0, 50, 'UTF-8');
} else {
$bezeichnung = substr($bezeichnung, 0, 50);
}
$wochentag = (int)($_POST['wochentag'] ?? 0); $wochentag = (int)($_POST['wochentag'] ?? 0);
$start = trim((string)($_POST['start'] ?? '')); $start = trim((string)($_POST['start'] ?? ''));
$ende = trim((string)($_POST['ende'] ?? '')); $ende = trim((string)($_POST['ende'] ?? ''));
@@ -608,7 +613,7 @@ $rules = $stRules->fetchAll(PDO::FETCH_ASSOC);
<form method="post" class="form-inline" style="margin-bottom:12px;"> <form method="post" class="form-inline" style="margin-bottom:12px;">
<input type="hidden" name="aktion" value="add_zeitraum"> <input type="hidden" name="aktion" value="add_zeitraum">
<label>Bezeichnung</label> <label>Bezeichnung</label>
<input class="form-control" type="text" name="bezeichnung" placeholder="z. B. Mittwoch Vormittag"> <input class="form-control" type="text" name="bezeichnung" maxlength="50" placeholder="z. B. Mittwoch Vormittag">
<label style="margin-left:10px;">Wochentag</label> <label style="margin-left:10px;">Wochentag</label>
<select class="form-control" name="wochentag" required> <select class="form-control" name="wochentag" required>
<option value="1">Montag</option> <option value="1">Montag</option>
+1 -1
View File
@@ -1,7 +1,7 @@
$(function() { $(function() {
$("#user_input").autocomplete({ $("#user_input").autocomplete({
source: "inc/suchepatient.php", source: "../inc/suchepatient.php",
minLength: 3, minLength: 3,
select: function( event, ui ) { select: function( event, ui ) {
event.preventDefault(); event.preventDefault();
+61 -17
View File
@@ -1,17 +1,19 @@
<?php <?php
session_start(); session_start();
// WICHTIG: Pfade aus /admin heraus korrekt auflösen // WICHTIG: Pfade aus /admin heraus korrekt auflösen
require_once __DIR__ . "/../inc/config.inc.php"; require_once __DIR__ . "/../inc/config.inc.php";
require_once __DIR__ . "/../inc/functions.inc.php"; require_once __DIR__ . "/../inc/functions.inc.php";
require_once __DIR__ . "/../inc/impfworkflow_notifications.inc.php";
// Login prüfen // Login prüfen
$user = check_admin_user(); $user = check_admin_user();
include __DIR__ . "/templates/header.inc.php"; include __DIR__ . "/templates/header.inc.php";
$user = check_admin_user(); $user = check_admin_user();
$internUserId = (int)$_SESSION['auth']['id']; $internUserId = (int)$_SESSION['auth']['id'];
$activeTab = 'data';
if (!$user) { echo "<div class='container main-container'><h3>Erst anmelden: <a href=login.php>Login</a></h3><br>"; if (!$user) { echo "<div class='container main-container'><h3>Erst anmelden: <a href=login.php>Login</a></h3><br>";
@@ -22,11 +24,12 @@ if(isset($_GET['save'])) {
$save = $_GET['save']; $save = $_GET['save'];
if($save == 'personal_data') { if($save == 'personal_data') {
$activeTab = 'data';
$vorname = trim($_POST['vorname']); $vorname = trim($_POST['vorname']);
$nachname = trim($_POST['nachname']); $nachname = trim($_POST['nachname']);
if($vorname == "" || $nachname == "") { if($vorname == "" || $nachname == "") {
$error_msg = "Bitte Vor- und Nachname ausfüllen."; $error_msg = "Bitte Vor- und Nachname ausfüllen.";
} else { } else {
$statement = $pdo->prepare("UPDATE users SET vorname = :vorname, nachname = :nachname, updated_at=NOW() WHERE id = :userid"); $statement = $pdo->prepare("UPDATE users SET vorname = :vorname, nachname = :nachname, updated_at=NOW() WHERE id = :userid");
$result = $statement->execute(array('vorname' => $vorname, 'nachname'=> $nachname, 'userid' => $user['id'] )); $result = $statement->execute(array('vorname' => $vorname, 'nachname'=> $nachname, 'userid' => $user['id'] ));
@@ -34,14 +37,15 @@ if(isset($_GET['save'])) {
$success_msg = "Daten erfolgreich gespeichert."; $success_msg = "Daten erfolgreich gespeichert.";
} }
} else if($save == 'email') { } else if($save == 'email') {
$activeTab = 'email';
$passwort = $_POST['passwort']; $passwort = $_POST['passwort'];
$email = trim($_POST['email']); $email = trim($_POST['email']);
$email2 = trim($_POST['email2']); $email2 = trim($_POST['email2']);
if($email != $email2) { if($email != $email2) {
$error_msg = "Die eingegebenen E-Mail-Adressen stimmten nicht überein."; $error_msg = "Die eingegebenen E-Mail-Adressen stimmten nicht überein.";
} else if(!filter_var($email, FILTER_VALIDATE_EMAIL)) { } else if(!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$error_msg = "Bitte eine gültige E-Mail-Adresse eingeben."; $error_msg = "Bitte eine gültige E-Mail-Adresse eingeben.";
} else if(!password_verify($passwort, $user['passwort'])) { } else if(!password_verify($passwort, $user['passwort'])) {
$error_msg = "Bitte korrektes Passwort eingeben."; $error_msg = "Bitte korrektes Passwort eingeben.";
} else { } else {
@@ -51,13 +55,31 @@ if(isset($_GET['save'])) {
$success_msg = "E-Mail-Adresse erfolgreich gespeichert."; $success_msg = "E-Mail-Adresse erfolgreich gespeichert.";
} }
} else if($save == 'impfworkflow_notification') {
$activeTab = 'impfworkflow';
$benachrichtigungEmail = trim((string)($_POST['benachrichtigung_email'] ?? ''));
if ($benachrichtigungEmail !== '' && !filter_var($benachrichtigungEmail, FILTER_VALIDATE_EMAIL)) {
$error_msg = "Bitte eine gueltige E-Mail-Adresse fuer die Impfworkflow-Benachrichtigung eingeben.";
} else {
try {
impfWorkflowNotificationSetEmail($pdo, $benachrichtigungEmail);
$success_msg = ($benachrichtigungEmail !== '')
? "Impfworkflow-Benachrichtigungsadresse gespeichert."
: "Impfworkflow-Benachrichtigungsadresse geloescht.";
} catch (Throwable $e) {
$error_msg = "Die Impfworkflow-Benachrichtigungsadresse konnte nicht gespeichert werden: " . $e->getMessage();
}
}
} else if($save == 'passwort') { } else if($save == 'passwort') {
$activeTab = 'passwort';
$passwortAlt = $_POST['passwortAlt']; $passwortAlt = $_POST['passwortAlt'];
$passwortNeu = trim($_POST['passwortNeu']); $passwortNeu = trim($_POST['passwortNeu']);
$passwortNeu2 = trim($_POST['passwortNeu2']); $passwortNeu2 = trim($_POST['passwortNeu2']);
if($passwortNeu != $passwortNeu2) { if($passwortNeu != $passwortNeu2) {
$error_msg = "Die eingegebenen Passwörter stimmten nicht überein."; $error_msg = "Die eingegebenen Passwörter stimmten nicht überein.";
} else if($passwortNeu == "") { } else if($passwortNeu == "") {
$error_msg = "Das Passwort darf nicht leer sein."; $error_msg = "Das Passwort darf nicht leer sein.";
} else if(!password_verify($passwortAlt, $user['passwort'])) { } else if(!password_verify($passwortAlt, $user['passwort'])) {
@@ -107,14 +129,15 @@ endif;
<!-- Nav tabs --> <!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist"> <ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#data" aria-controls="home" role="tab" data-toggle="tab">Persönliche Daten</a></li> <li role="presentation" class="<?php echo ($activeTab === 'data') ? 'active' : ''; ?>"><a href="#data" aria-controls="home" role="tab" data-toggle="tab">Persönliche Daten</a></li>
<li role="presentation"><a href="#email" aria-controls="profile" role="tab" data-toggle="tab">E-Mail</a></li> <li role="presentation" class="<?php echo ($activeTab === 'email') ? 'active' : ''; ?>"><a href="#email" aria-controls="profile" role="tab" data-toggle="tab">E-Mail</a></li>
<li role="presentation"><a href="#passwort" aria-controls="messages" role="tab" data-toggle="tab">Passwort</a></li> <li role="presentation" class="<?php echo ($activeTab === 'impfworkflow') ? 'active' : ''; ?>"><a href="#impfworkflow" aria-controls="impfworkflow" role="tab" data-toggle="tab">Impfworkflow</a></li>
<li role="presentation" class="<?php echo ($activeTab === 'passwort') ? 'active' : ''; ?>"><a href="#passwort" aria-controls="messages" role="tab" data-toggle="tab">Passwort</a></li>
</ul> </ul>
<!-- Persönliche Daten--> <!-- Persönliche Daten-->
<div class="tab-content"> <div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="data"> <div role="tabpanel" class="tab-pane <?php echo ($activeTab === 'data') ? 'active' : ''; ?>" id="data">
<br> <br>
<form action="?save=personal_data" method="post" class="form-horizontal"> <form action="?save=personal_data" method="post" class="form-horizontal">
<div class="form-group"> <div class="form-group">
@@ -139,10 +162,10 @@ endif;
</form> </form>
</div> </div>
<!-- Änderung der E-Mail-Adresse --> <!-- Änderung der E-Mail-Adresse -->
<div role="tabpanel" class="tab-pane" id="email"> <div role="tabpanel" class="tab-pane <?php echo ($activeTab === 'email') ? 'active' : ''; ?>" id="email">
<br> <br>
<p>Zum Änderen deiner E-Mail-Adresse gib bitte dein aktuelles Passwort sowie die neue E-Mail-Adresse ein.</p> <p>Zum Änderen deiner E-Mail-Adresse gib bitte dein aktuelles Passwort sowie die neue E-Mail-Adresse ein.</p>
<form action="?save=email" method="post" class="form-horizontal"> <form action="?save=email" method="post" class="form-horizontal">
<div class="form-group"> <div class="form-group">
<label for="inputPasswort" class="col-sm-2 control-label">Passwort</label> <label for="inputPasswort" class="col-sm-2 control-label">Passwort</label>
@@ -174,10 +197,31 @@ endif;
</form> </form>
</div> </div>
<!-- Änderung des Passworts --> <div role="tabpanel" class="tab-pane <?php echo ($activeTab === 'impfworkflow') ? 'active' : ''; ?>" id="impfworkflow">
<div role="tabpanel" class="tab-pane" id="passwort">
<br> <br>
<p>Zum Änderen deines Passworts gib bitte dein aktuelles Passwort sowie das neue Passwort ein.</p> <p>Hier hinterlegst du die E-Mail-Adresse, an die spaeter Impfworkflow-Benachrichtigungen gesendet werden sollen.</p>
<?php $currentNotificationEmail = impfWorkflowNotificationGetEmail($pdo); ?>
<form action="?save=impfworkflow_notification" method="post" class="form-horizontal">
<div class="form-group">
<label for="inputImpfworkflowMail" class="col-sm-2 control-label">Benachrichtigungs-E-Mail</label>
<div class="col-sm-10">
<input class="form-control" id="inputImpfworkflowMail" name="benachrichtigung_email" type="email" value="<?php echo htmlentities($currentNotificationEmail); ?>" placeholder="benachrichtigung@praxis.de">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<p class="help-block">Leer lassen, um Benachrichtigungen zu deaktivieren.</p>
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</div>
</form>
</div>
<!-- Änderung des Passworts -->
<div role="tabpanel" class="tab-pane <?php echo ($activeTab === 'passwort') ? 'active' : ''; ?>" id="passwort">
<br>
<p>Zum Änderen deines Passworts gib bitte dein aktuelles Passwort sowie das neue Passwort ein.</p>
<form action="?save=passwort" method="post" class="form-horizontal"> <form action="?save=passwort" method="post" class="form-horizontal">
<div class="form-group"> <div class="form-group">
<label for="inputPasswort" class="col-sm-2 control-label">Altes Passwort</label> <label for="inputPasswort" class="col-sm-2 control-label">Altes Passwort</label>
+36
View File
@@ -17,6 +17,7 @@ FROM (
UNION ALL SELECT 'impfstoff_wochenplan' UNION ALL SELECT 'impfstoff_wochenplan'
UNION ALL SELECT 'impf_zeitraum' UNION ALL SELECT 'impf_zeitraum'
UNION ALL SELECT 'impf_zeitraum_impfstoff' UNION ALL SELECT 'impf_zeitraum_impfstoff'
UNION ALL SELECT 'warteliste_zeitraum'
UNION ALL SELECT 'warteliste' UNION ALL SELECT 'warteliste'
) t ) t
LEFT JOIN information_schema.tables it LEFT JOIN information_schema.tables it
@@ -51,6 +52,9 @@ FROM (
UNION ALL SELECT 'impf_zeitraum', 'impfortid' UNION ALL SELECT 'impf_zeitraum', 'impfortid'
UNION ALL SELECT 'impf_zeitraum_impfstoff', 'zeitraum_id' UNION ALL SELECT 'impf_zeitraum_impfstoff', 'zeitraum_id'
UNION ALL SELECT 'impf_zeitraum_impfstoff', 'impfstoff_id' UNION ALL SELECT 'impf_zeitraum_impfstoff', 'impfstoff_id'
UNION ALL SELECT 'warteliste_zeitraum', 'warteid'
UNION ALL SELECT 'warteliste_zeitraum', 'zeitraum_id'
UNION ALL SELECT 'warteliste_zeitraum', 'created_at'
UNION ALL SELECT 'warteliste', 'warteid' UNION ALL SELECT 'warteliste', 'warteid'
UNION ALL SELECT 'warteliste', 'userid' UNION ALL SELECT 'warteliste', 'userid'
UNION ALL SELECT 'warteliste', 'impfenzeitraum' UNION ALL SELECT 'warteliste', 'impfenzeitraum'
@@ -71,6 +75,7 @@ SELECT
END AS status END AS status
FROM ( FROM (
SELECT 'warteliste' AS table_name, 'idx_warteliste_zeitraum' AS index_name SELECT 'warteliste' AS table_name, 'idx_warteliste_zeitraum' AS index_name
UNION ALL SELECT 'warteliste_zeitraum', 'idx_warteliste_zeitraum_zeitraum'
UNION ALL SELECT 'impfstoff_wochenplan', 'idx_impfstoff_wochenplan_impfstoff' UNION ALL SELECT 'impfstoff_wochenplan', 'idx_impfstoff_wochenplan_impfstoff'
UNION ALL SELECT 'impfstoff_wochenplan', 'idx_impfstoff_wochenplan_wochentag' UNION ALL SELECT 'impfstoff_wochenplan', 'idx_impfstoff_wochenplan_wochentag'
UNION ALL SELECT 'impf_zeitraum', 'idx_impf_zeitraum_wochentag' UNION ALL SELECT 'impf_zeitraum', 'idx_impf_zeitraum_wochentag'
@@ -102,6 +107,24 @@ SELECT
ELSE 'TABLE_MISSING' ELSE 'TABLE_MISSING'
END AS status; END AS status;
SELECT
'meta' AS check_type,
'impf_workflow_meta.legacy_warteliste_zeitraeume_migrated' AS object_name,
CASE
WHEN EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = 'impf_workflow_meta'
) THEN COALESCE((
SELECT CONCAT('VALUE=', meta_value)
FROM impf_workflow_meta
WHERE meta_key = 'legacy_warteliste_zeitraeume_migrated'
LIMIT 1
), 'MISSING')
ELSE 'TABLE_MISSING'
END AS status;
SELECT SELECT
'data' AS check_type, 'data' AS check_type,
'impfstoff_wochenplan rows' AS object_name, 'impfstoff_wochenplan rows' AS object_name,
@@ -154,3 +177,16 @@ SELECT
) THEN CAST((SELECT COUNT(*) FROM warteliste WHERE zeitraum_id IS NOT NULL) AS CHAR) ) THEN CAST((SELECT COUNT(*) FROM warteliste WHERE zeitraum_id IS NOT NULL) AS CHAR)
ELSE 'COLUMN_MISSING' ELSE 'COLUMN_MISSING'
END AS status; END AS status;
SELECT
'data' AS check_type,
'warteliste_zeitraum rows' AS object_name,
CASE
WHEN EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = 'warteliste_zeitraum'
) THEN CAST((SELECT COUNT(*) FROM warteliste_zeitraum) AS CHAR)
ELSE 'TABLE_MISSING'
END AS status;
+51
View File
@@ -51,6 +51,14 @@ CREATE TABLE IF NOT EXISTS `impf_zeitraum_impfstoff` (
INDEX `idx_impf_zeitraum_impfstoff_impfstoff` (`impfstoff_id`) INDEX `idx_impf_zeitraum_impfstoff_impfstoff` (`impfstoff_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `warteliste_zeitraum` (
`warteid` INT NOT NULL,
`zeitraum_id` INT NOT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`warteid`, `zeitraum_id`),
INDEX `idx_warteliste_zeitraum_zeitraum` (`zeitraum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP PROCEDURE IF EXISTS `migrate_praxis_schema_20260320`; DROP PROCEDURE IF EXISTS `migrate_praxis_schema_20260320`;
DELIMITER $$ DELIMITER $$
CREATE PROCEDURE `migrate_praxis_schema_20260320`() CREATE PROCEDURE `migrate_praxis_schema_20260320`()
@@ -60,6 +68,8 @@ BEGIN
DECLARE v_warteliste_exists INT DEFAULT 0; DECLARE v_warteliste_exists INT DEFAULT 0;
DECLARE v_zeitraum_id_exists INT DEFAULT 0; DECLARE v_zeitraum_id_exists INT DEFAULT 0;
DECLARE v_warteliste_index_exists INT DEFAULT 0; DECLARE v_warteliste_index_exists INT DEFAULT 0;
DECLARE v_warteliste_zeitraum_exists INT DEFAULT 0;
DECLARE v_warteliste_zeitraum_index_exists INT DEFAULT 0;
DECLARE v_legacy_plan_exists INT DEFAULT 0; DECLARE v_legacy_plan_exists INT DEFAULT 0;
SELECT COUNT(*) SELECT COUNT(*)
@@ -114,6 +124,35 @@ BEGIN
END IF; END IF;
END IF; END IF;
SELECT COUNT(*)
INTO v_warteliste_zeitraum_exists
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = 'warteliste_zeitraum';
IF v_warteliste_zeitraum_exists = 0 THEN
CREATE TABLE `warteliste_zeitraum` (
`warteid` INT NOT NULL,
`zeitraum_id` INT NOT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`warteid`, `zeitraum_id`),
INDEX `idx_warteliste_zeitraum_zeitraum` (`zeitraum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SET v_warteliste_zeitraum_exists = 1;
ELSE
SELECT COUNT(*)
INTO v_warteliste_zeitraum_index_exists
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'warteliste_zeitraum'
AND index_name = 'idx_warteliste_zeitraum_zeitraum';
IF v_warteliste_zeitraum_index_exists = 0 THEN
ALTER TABLE `warteliste_zeitraum`
ADD INDEX `idx_warteliste_zeitraum_zeitraum` (`zeitraum_id`);
END IF;
END IF;
SELECT COUNT(*) SELECT COUNT(*)
INTO v_legacy_plan_exists INTO v_legacy_plan_exists
FROM information_schema.tables FROM information_schema.tables
@@ -159,6 +198,18 @@ BEGIN
VALUES ('legacy_wochenplan_migrated', '1') AS `incoming` VALUES ('legacy_wochenplan_migrated', '1') AS `incoming`
ON DUPLICATE KEY UPDATE `meta_value` = `incoming`.`meta_value`; ON DUPLICATE KEY UPDATE `meta_value` = `incoming`.`meta_value`;
END IF; END IF;
IF v_warteliste_exists > 0 AND v_warteliste_zeitraum_exists > 0 THEN
INSERT IGNORE INTO `warteliste_zeitraum` (`warteid`, `zeitraum_id`)
SELECT `warteid`, `zeitraum_id`
FROM `warteliste`
WHERE `zeitraum_id` IS NOT NULL
AND `zeitraum_id` > 0;
INSERT INTO `impf_workflow_meta` (`meta_key`, `meta_value`)
VALUES ('legacy_warteliste_zeitraeume_migrated', '1') AS `incoming`
ON DUPLICATE KEY UPDATE `meta_value` = `incoming`.`meta_value`;
END IF;
END $$ END $$
DELIMITER ; DELIMITER ;
@@ -0,0 +1,99 @@
-- Migration fuer Mehrfach-Zeitfenster in der Impfwarteliste.
-- Idempotent: kann mehrfach ausgefuehrt werden.
-- Bestehende Tabellen werden nicht neu aufgebaut, sondern nur erweitert.
CREATE TABLE IF NOT EXISTS `impf_workflow_meta` (
`meta_key` VARCHAR(100) NOT NULL,
`meta_value` VARCHAR(255) NOT NULL DEFAULT '',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`meta_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP PROCEDURE IF EXISTS `migrate_warteliste_multi_zeitfenster_20260322`;
DELIMITER $$
CREATE PROCEDURE `migrate_warteliste_multi_zeitfenster_20260322`()
BEGIN
DECLARE v_warteliste_exists INT DEFAULT 0;
DECLARE v_zeitraum_id_exists INT DEFAULT 0;
DECLARE v_warteliste_index_exists INT DEFAULT 0;
DECLARE v_warteliste_zeitraum_exists INT DEFAULT 0;
DECLARE v_warteliste_zeitraum_index_exists INT DEFAULT 0;
SELECT COUNT(*)
INTO v_warteliste_exists
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = 'warteliste';
IF v_warteliste_exists = 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Tabelle warteliste wurde nicht gefunden.';
END IF;
SELECT COUNT(*)
INTO v_zeitraum_id_exists
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = 'warteliste'
AND column_name = 'zeitraum_id';
IF v_zeitraum_id_exists = 0 THEN
ALTER TABLE `warteliste`
ADD COLUMN `zeitraum_id` INT NULL AFTER `impfenzeitraum`;
END IF;
SELECT COUNT(*)
INTO v_warteliste_index_exists
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'warteliste'
AND index_name = 'idx_warteliste_zeitraum';
IF v_warteliste_index_exists = 0 THEN
ALTER TABLE `warteliste`
ADD INDEX `idx_warteliste_zeitraum` (`zeitraum_id`);
END IF;
SELECT COUNT(*)
INTO v_warteliste_zeitraum_exists
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = 'warteliste_zeitraum';
IF v_warteliste_zeitraum_exists = 0 THEN
CREATE TABLE `warteliste_zeitraum` (
`warteid` INT NOT NULL,
`zeitraum_id` INT NOT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`warteid`, `zeitraum_id`),
INDEX `idx_warteliste_zeitraum_zeitraum` (`zeitraum_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SET v_warteliste_zeitraum_exists = 1;
END IF;
SELECT COUNT(*)
INTO v_warteliste_zeitraum_index_exists
FROM information_schema.statistics
WHERE table_schema = DATABASE()
AND table_name = 'warteliste_zeitraum'
AND index_name = 'idx_warteliste_zeitraum_zeitraum';
IF v_warteliste_zeitraum_index_exists = 0 THEN
ALTER TABLE `warteliste_zeitraum`
ADD INDEX `idx_warteliste_zeitraum_zeitraum` (`zeitraum_id`);
END IF;
INSERT IGNORE INTO `warteliste_zeitraum` (`warteid`, `zeitraum_id`)
SELECT `warteid`, `zeitraum_id`
FROM `warteliste`
WHERE `zeitraum_id` IS NOT NULL
AND `zeitraum_id` > 0;
INSERT INTO `impf_workflow_meta` (`meta_key`, `meta_value`)
VALUES ('legacy_warteliste_zeitraeume_migrated', '1') AS `incoming`
ON DUPLICATE KEY UPDATE `meta_value` = `incoming`.`meta_value`;
END $$
DELIMITER ;
CALL `migrate_warteliste_multi_zeitfenster_20260322`();
DROP PROCEDURE IF EXISTS `migrate_warteliste_multi_zeitfenster_20260322`;
@@ -0,0 +1,24 @@
CREATE TABLE IF NOT EXISTS time_error_notification_state (
employee_id INT NOT NULL,
cycle_started_on DATE NOT NULL,
first_error_date DATE NOT NULL,
last_notification_stage VARCHAR(50) DEFAULT NULL,
last_notification_sent_at DATETIME DEFAULT NULL,
employee_day_1_sent_at DATETIME DEFAULT NULL,
employee_day_3_sent_at DATETIME DEFAULT NULL,
admin_day_7_sent_at DATETIME DEFAULT NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (employee_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS time_error_notifications (
id INT NOT NULL AUTO_INCREMENT,
employee_id INT NOT NULL,
cycle_started_on DATE NOT NULL,
notification_stage VARCHAR(50) NOT NULL,
recipient_email VARCHAR(255) NOT NULL,
sent_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uniq_time_error_notification (employee_id, cycle_started_on, notification_stage, recipient_email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+21 -6
View File
@@ -63,7 +63,21 @@
<?php else: ?> <?php else: ?>
<div id="navbar" class="navbar-collapse collapse"> <div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
<li><a href="anfragen.php">Anfragen</a></li> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Anfragen <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="anfragen.php">Übersicht Anfragen</a></li>
<li><a href="anfragen.php?aktion=1">Anfragen bearbeiten</a></li>
<li><a href="anfragen.php?aktion=12">Formular-Auswertung</a></li>
<li><a href="anfragen.php?aktion=18">Mailvorlagen anlegen</a></li>
<li><a href="anfragen.php?aktion=16">Mailvorlagen anpassen</a></li>
<li class="divider"></li>
<li><a href="anfragen.php?aktion=4">Urlaub eintragen</a></li>
<li><a href="anfragen.php?aktion=6">Notfallsprechstunde eintragen</a></li>
<li class="divider"></li>
<li><a href="http://ts03.fritz.box:8080/" target="_blank">Anrufbeantworter</a></li>
</ul>
</li>
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Impfungen <span class="caret"></span></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Impfungen <span class="caret"></span></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
@@ -74,17 +88,18 @@
<li><a href="impfworkflow_stammdaten.php">Stammdaten</a></li> <li><a href="impfworkflow_stammdaten.php">Stammdaten</a></li>
</ul> </ul>
</li> </li>
<!--<li><a href="togoadmin.php">togo-Impfung</a></li>--> <li class="dropdown">
<li><a href="http://ts03.fritz.box:8080/" target="_blank">Anrufbeantworter</a></li> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Verwaltung <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="../zeiterfassung">Zeiterfassung</a></li> <li><a href="../zeiterfassung">Zeiterfassung</a></li>
<li><a href="webseitenadmin.php">Webseiteninhalt ändern</a></li> <li><a href="webseitenadmin.php">Webseiteninhalt ändern</a></li>
<li><a href="settings.php">Einstellungen</a></li> <li><a href="settings.php">Einstellungen</a></li>
</ul>
</li>
<li><a href="zeiterfassung_hilfe.php">Hilfe</a></li>
<li><a href="logout.php">Logout</a></li> <li><a href="logout.php">Logout</a></li>
</ul> </ul>
</div><!--/.navbar-collapse --> </div><!--/.navbar-collapse -->
<?php endif; ?> <?php endif; ?>
</div> </div>
</nav> </nav>
+16 -3
View File
@@ -58,17 +58,23 @@ if (!check_worker()) {
$inhaltid = (int)($_POST["inhaltid"] ?? 0); $inhaltid = (int)($_POST["inhaltid"] ?? 0);
$inhalt = $_POST["inhalt"] ?? ""; $inhalt = $_POST["inhalt"] ?? "";
$webseitentitel = $_POST["webseitentitel"] ?? ""; $webseitentitel = $_POST["webseitentitel"] ?? "";
$beschreibung = $_POST["beschreibung"] ?? "";
$url = $_POST["url"] ?? "";
try { try {
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
UPDATE webseiteninhalt UPDATE webseiteninhalt
SET inhalt = :inhalt, SET inhalt = :inhalt,
webseitentitel = :webseitentitel webseitentitel = :webseitentitel,
beschreibung = :beschreibung,
url = :url
WHERE inhaltid = :inhaltid WHERE inhaltid = :inhaltid
"); ");
$stmt->execute([ $stmt->execute([
':inhalt' => $inhalt, ':inhalt' => $inhalt,
':webseitentitel' => $webseitentitel, ':webseitentitel' => $webseitentitel,
':beschreibung' => $beschreibung,
':url' => $url,
':inhaltid' => $inhaltid, ':inhaltid' => $inhaltid,
]); ]);
@@ -85,7 +91,7 @@ if (!check_worker()) {
try { try {
$stmt = $pdo->prepare(" $stmt = $pdo->prepare("
SELECT webseitentitel, inhalt SELECT webseitentitel, inhalt, beschreibung, url
FROM webseiteninhalt FROM webseiteninhalt
WHERE inhaltid = ? WHERE inhaltid = ?
LIMIT 1 LIMIT 1
@@ -98,13 +104,20 @@ if (!check_worker()) {
} else { } else {
$webseitentitel = $rowconfig["webseitentitel"] ?? ""; $webseitentitel = $rowconfig["webseitentitel"] ?? "";
$inhalt = $rowconfig["inhalt"] ?? ""; $inhalt = $rowconfig["inhalt"] ?? "";
$beschreibung = $rowconfig["beschreibung"] ?? "";
$url = $rowconfig["url"] ?? "";
echo "<h1>Webseiteninhalt bearbeiten</h1><br>"; echo "<h1>Webseiteninhalt bearbeiten</h1><br>";
echo "<h4>Vorlage: " . htmlspecialchars($webseitentitel, ENT_QUOTES, 'UTF-8') . "</h4>"; echo "<h4>Vorlage: " . htmlspecialchars($webseitentitel, ENT_QUOTES, 'UTF-8') . "</h4>";
echo "<br><br>"; echo "<br><br>";
echo "<form action='" . htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8') . "' method='POST'>"; echo "<form action='" . htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8') . "' method='POST'>";
echo "<input name='webseitentitel' type='hidden' value='" . htmlspecialchars($webseitentitel, ENT_QUOTES, 'UTF-8') . "'>"; echo "<label>Titel</label><br>";
echo "<input name='webseitentitel' type='text' class='form-control' value='" . htmlspecialchars($webseitentitel, ENT_QUOTES, 'UTF-8') . "'><br><br>";
echo "<label>Beschreibung</label><br>";
echo "<input name='beschreibung' type='text' class='form-control' value='" . htmlspecialchars($beschreibung, ENT_QUOTES, 'UTF-8') . "'><br><br>";
echo "<label>URL / Hinweis</label><br>";
echo "<input name='url' type='text' class='form-control' value='" . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . "'><br><br>";
echo "<div id='my-editor'></div>"; echo "<div id='my-editor'></div>";
// Inhalt ist HTML -> bewusst NICHT escapen, sonst zerstörst du HTML im Editor // Inhalt ist HTML -> bewusst NICHT escapen, sonst zerstörst du HTML im Editor
echo "<textarea height='200' name='inhalt' id='trumbowyg-demo'>" . $inhalt . "</textarea>"; echo "<textarea height='200' name='inhalt' id='trumbowyg-demo'>" . $inhalt . "</textarea>";
+168
View File
@@ -0,0 +1,168 @@
<?php
session_start();
require_once __DIR__ . "/../inc/config.inc.php";
require_once __DIR__ . "/../inc/functions.inc.php";
$user = check_admin_user();
include __DIR__ . "/templates/header.inc.php";
if (!$user) {
echo "<div class='container main-container'><h3>Erst anmelden: <a href='login.php'>Login</a></h3><br>";
include __DIR__ . "/templates/footer.inc.php";
exit;
}
?>
<div class="container main-container">
<div class="page-header">
<h2>FAQ und Anleitung: Admin + Zeiterfassung</h2>
<p>Diese Seite dient als Nachschlagewerk für die tägliche Arbeit in der Administration und in der Zeiterfassung.</p>
</div>
<div class="alert alert-info">
<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>
<h4>Anfragen</h4>
<p>Im Bereich <strong>Anfragen</strong> werden eingehende Formularanfragen bearbeitet, gefiltert und beantwortet. Dort lassen sich offene, letzte oder alle Anfragen anzeigen und in den Bearbeitungsstatus überführen.</p>
<h4>Mailvorlagen</h4>
<p>Mailvorlagen werden genutzt, um wiederkehrende Antworten und Abläufe einheitlich zu versenden. Änderungen an Vorlagen wirken sich auf spätere Nachrichten aus, daher sollten Texte dort zentral gepflegt werden.</p>
<h4>Formular-Auswertung</h4>
<p>Die Formular-Auswertung dient dazu, Anfragen strukturiert auszuwerten. Das ist vor allem hilfreich, wenn Mengen, Anfragearten oder Bearbeitungsstände geprüft werden sollen.</p>
<h4>Impfverwaltung</h4>
<p>Unter <strong>Impfungen</strong> werden Wartelisten, Teilnehmer, Impfevents und Stammdaten verwaltet. Dieser Bereich ist unabhängig von der Zeiterfassung, gehört aber zur Admin-Oberfläche.</p>
<h4>Webseiteninhalte ändern</h4>
<p>Über <strong>Webseiteninhalt ändern</strong> werden Texte und Inhalte der öffentlichen Webseite gepflegt. Änderungen dort betreffen direkt die Darstellung der Praxis-Webseite.</p>
<h4>Einstellungen</h4>
<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, Abwesenheiten und Mitarbeiterverwaltung.</p>
<hr>
<h3>2. Zeiterfassung: Funktionen für Mitarbeiter</h3>
<h4>Startseite / Stempeln</h4>
<p>Auf der Startseite der Zeiterfassung wird gestempelt. Das System setzt automatisch abwechselnd <strong>KOMMEN</strong> und <strong>GEHEN</strong>. Zusätzlich wird dort angezeigt, ob aktuell ein Buchungsproblem vorhanden ist.</p>
<h4>Zeitübersicht</h4>
<p>In der <strong>Zeitübersicht</strong> können die eigenen Buchungen für einen Monat kontrolliert werden. Dort lassen sich auch Tagesansichten aufrufen und bei Bedarf manuell anpassen.</p>
<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>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 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>
<hr>
<h3>3. Zeiterfassung: Funktionen für Admins</h3>
<h4>Alle Zeitbuchungen</h4>
<p>Unter <strong>Alle Zeitbuchungen</strong> kann für jeden Mitarbeiter ein Monat ausgewählt und angezeigt werden. Zusätzlich lassen sich Einzel-PDFs und eine Sammel-PDF für alle Mitarbeiter eines Monats erzeugen.</p>
<h4>PDF-Ausgaben</h4>
<p>Die Einzel-PDF erstellt die Monatsübersicht eines einzelnen Mitarbeiters. Die Sammel-PDF enthält alle Mitarbeiter mit Buchungen im gewählten Monat. Mitarbeiter ohne Buchung im Monat werden dabei nicht ausgegeben.</p>
<h4>Alle Fehlbuchungen</h4>
<p>Unter <strong>Alle Fehlbuchungen</strong> werden fehlerhafte Tage aller Mitarbeiter angezeigt. Von dort aus können Admins einzelne Tage manuell bearbeiten oder automatische Schließungen für einen Mitarbeiter durchführen.</p>
<h4>Fehlbuchungen automatisch schließen</h4>
<p>Admins können für einen Mitarbeiter alle automatisch schließbaren Fehlbuchungen mit einer Stundenanzahl ergänzen. Beispiel: Fehlt das <strong>GEHEN</strong>, wird es um die angegebene Anzahl Stunden nach dem letzten <strong>KOMMEN</strong> eingetragen.</p>
<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>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>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>
<h4>Benachrichtigungen bei Zeitfehlern</h4>
<p>Für offene Zeitfehler existiert eine gestaffelte Benachrichtigungslogik. Mitarbeiter werden erinnert, und bei länger offenen Fehlern erfolgt später eine Eskalation an Admins. Die E-Mails enthalten einen Direktlink zur Zeiterfassung.</p>
<hr>
<h3>4. Typische Fragen von Mitarbeitern</h3>
<h4>Ich habe vergessen zu stempeln. Was soll ich tun?</h4>
<p>Bitte in der Zeiterfassung den Bereich <strong>Fehlbuchungen</strong> oder die <strong>Zeitübersicht</strong> öffnen und den betroffenen Tag korrigieren. Falls das nicht möglich ist, bitte einen Admin informieren.</p>
<h4>Warum wird mir ein Buchungsfehler angezeigt?</h4>
<p>Ein Fehler entsteht, wenn die Reihenfolge der Buchungen nicht stimmt, zum Beispiel zwei <strong>KOMMEN</strong> hintereinander oder wenn ein <strong>GEHEN</strong> fehlt.</p>
<h4>Bekomme ich Erinnerungen bei offenen Fehlern?</h4>
<p>Ja. Offene Zeitfehler können Erinnerungen auslösen. Bleiben Fehler bestehen, werden sie nach dem vorgesehenen Ablauf weiter eskaliert.</p>
<h4>Kann ich meine Zeiten selbst ändern?</h4>
<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 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 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>
<hr>
<h3>5. Typische Fragen von Admins</h3>
<h4>Wie finde ich schnell offene Zeitprobleme?</h4>
<p>Über <strong>Alle Fehlbuchungen</strong> in der Zeiterfassung. Dort sind alle problematischen Tage je Mitarbeiter sichtbar.</p>
<h4>Wie schließe ich mehrere ähnliche Fehlbuchungen auf einmal?</h4>
<p>In <strong>Alle Fehlbuchungen</strong> kann für einen Mitarbeiter eine Stundenanzahl eingetragen werden, um automatisch fehlende <strong>GEHEN</strong>-Buchungen zu ergänzen, wenn der letzte Eintrag des Tages ein <strong>KOMMEN</strong> ist.</p>
<h4>Wie erstelle ich Monatsnachweise für mehrere Mitarbeiter?</h4>
<p>Über <strong>Alle Zeitbuchungen</strong> und dann die Sammel-PDF für den gewünschten Monat. So werden alle Mitarbeiter mit Buchungen in einem Dokument zusammengefasst.</p>
<h4>Warum erscheint ein Mitarbeiter nicht in der Sammel-PDF?</h4>
<p>Mitarbeiter ohne Buchung im ausgewählten Monat werden in der Gesamt-PDF nicht aufgenommen.</p>
<h4>Was tun, wenn die PDF für einen Mitarbeiter nicht erzeugt wird?</h4>
<p>Dann liegen im gewählten Monat meist noch offene Zeitfehler vor. Diese müssen zuerst bereinigt werden.</p>
<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>
<div class="alert alert-success" style="margin-top:30px;">
<strong>Hinweis:</strong> Diese Seite ist als lebendes Handbuch gedacht. Wenn neue Funktionen in Admin oder Zeiterfassung hinzukommen, sollte diese Hilfeseite mit aktualisiert werden.
</div>
</div>
<?php include __DIR__ . "/templates/footer.inc.php"; ?>
-5
View File
@@ -5,11 +5,6 @@ CALSCALE:GREGORIAN
METHOD:PUBLISH METHOD:PUBLISH
BEGIN:VEVENT BEGIN:VEVENT
SUMMARY:Urlaub SUMMARY:Urlaub
DTSTART:20250912
DTEND:20250921
END:VEVENT
BEGIN:VEVENT
SUMMARY:Urlaub
DTSTART:20251002 DTSTART:20251002
DTEND:20251005 DTEND:20251005
END:VEVENT END:VEVENT
-5
View File
@@ -5,11 +5,6 @@ CALSCALE:GREGORIAN
METHOD:PUBLISH METHOD:PUBLISH
BEGIN:VEVENT BEGIN:VEVENT
SUMMARY:Urlaub SUMMARY:Urlaub
DTSTART:20250912
DTEND:20250921
END:VEVENT
BEGIN:VEVENT
SUMMARY:Urlaub
DTSTART:20251002 DTSTART:20251002
DTEND:20251005 DTEND:20251005
END:VEVENT END:VEVENT
Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg width="720" height="720" viewBox="0 0 720 720" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="720" height="720" rx="36" fill="#F3F4F6"/>
<circle cx="360" cy="235" r="112" fill="#9CA3AF"/>
<path d="M152 608C152 488.706 248.706 392 368 392H352C471.294 392 568 488.706 568 608V624H152V608Z" fill="#9CA3AF"/>
<path d="M228 608C228 530.68 290.68 468 368 468H352C429.32 468 492 530.68 492 608V624H228V608Z" fill="#6B7280"/>
</svg>

After

Width:  |  Height:  |  Size: 452 B

+170 -23
View File
@@ -8,6 +8,32 @@
<head> <head>
<?php <?php
error_reporting(E_ALL);
ini_set('display_errors', '1');
register_shutdown_function(static function (): void {
$error = error_get_last();
if ($error === null) {
return;
}
$fatalTypes = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR];
if (!in_array((int)$error['type'], $fatalTypes, true)) {
return;
}
if (!headers_sent()) {
header('Content-Type: text/html; charset=utf-8');
}
echo '<div style="max-width:900px;margin:20px auto;padding:16px;border:3px solid #b30000;background:#fff7f7;color:#111;font-family:Arial,sans-serif;">'
. '<h3 style="margin-top:0;">Fehler in impfwarteliste.php</h3>'
. '<p>Die Seite ist wegen eines PHP-Fehlers abgebrochen.</p>'
. '<p><strong>Meldung:</strong> ' . htmlspecialchars((string)$error['message'], ENT_QUOTES, 'UTF-8') . '<br>'
. '<strong>Datei:</strong> ' . htmlspecialchars((string)$error['file'], ENT_QUOTES, 'UTF-8') . '<br>'
. '<strong>Zeile:</strong> ' . (int)$error['line'] . '</p>'
. '</div>';
});
include('header.php'); include('header.php');
?> ?>
@@ -26,11 +52,17 @@
include_once("inc/config.inc.php"); include_once("inc/config.inc.php");
include_once("inc/functions.inc.php"); include_once("inc/functions.inc.php");
include_once('inc/functions.impfen.inc.php'); include_once('inc/functions.impfen.inc.php');
include_once('inc/impfworkflow_notifications.inc.php');
$workflowSetupError = '';
if ($con instanceof mysqli) { if ($con instanceof mysqli) {
mysqli_set_charset($con, "utf8mb4"); mysqli_set_charset($con, "utf8mb4");
} }
if (isset($pdo) && $pdo instanceof PDO) { if (isset($pdo) && $pdo instanceof PDO) {
try {
impfWorkflowEnsureTables($pdo); impfWorkflowEnsureTables($pdo);
} catch (Throwable $e) {
$workflowSetupError = $e->getMessage();
}
} }
$zeitOptionenJson = "{}"; $zeitOptionenJson = "{}";
?> ?>
@@ -57,11 +89,20 @@ $mailbetreff = "Ihr Wartelistenplatz für eine Impfung bei Praxis Creutzburg";
<section class="box special"> <section class="box special">
<h2>Impfwarteliste</h2> <h2>Impfwarteliste</h2>
<?php if ($workflowSetupError !== ''): ?>
<div style="border:3px solid red; margin: 10px 0; padding: 10px; text-align:left;">
Die Impfworkflow-Tabellen konnten nicht automatisch geprueft werden: <?php echo htmlspecialchars($workflowSetupError, ENT_QUOTES, 'UTF-8'); ?><br>
Bitte fuehren Sie zuerst das Migrationsskript aus.
</div>
<?php endif; ?>
<?php <?php
if(isset($_POST["id"]) || isset($_GET["id"])){ if($workflowSetupError !== ''){
// Hinweis wurde bereits oberhalb ausgegeben.
}else if(isset($_POST["id"]) || isset($_GET["id"])){
if(isset($_POST["id"])){ if(isset($_POST["id"])){
$id = mysqli_real_escape_string($con, $_POST["id"]); $id = mysqli_real_escape_string($con, $_POST["id"]);
@@ -83,6 +124,17 @@ if(isset($_POST["id"]) || isset($_GET["id"])){
$impfenzeitraum = $zeitraumAktuell['label']; $impfenzeitraum = $zeitraumAktuell['label'];
} }
} }
$ausgewaehlteZeitraeume = [];
if (isset($pdo) && $pdo instanceof PDO) {
$ausgewaehlteZeitraeume = impfGetWartelistenZeitraeumeLabels($pdo, (int)$warteid, false);
}
if (!empty($ausgewaehlteZeitraeume)) {
$impfenzeitraum = implode('<br>', array_map(static function ($label) {
return e((string)$label);
}, $ausgewaehlteZeitraeume));
} else {
$impfenzeitraum = htmlspecialchars((string)$impfenzeitraum, ENT_QUOTES, 'UTF-8');
}
//echo $userid; //echo $userid;
$queryuser = mysqli_query($con, "SELECT * FROM persons WHERE person_id='" . $userid . "'"); $queryuser = mysqli_query($con, "SELECT * FROM persons WHERE person_id='" . $userid . "'");
$rowuser = $queryuser->fetch_assoc(); $rowuser = $queryuser->fetch_assoc();
@@ -133,7 +185,7 @@ if(isset($_POST["id"]) || isset($_GET["id"])){
echo "<h4>Art: $Patientenart</h4>"; echo "<h4>Art: $Patientenart</h4>";
echo "<h4>Telefon: $tel</h4>"; echo "<h4>Telefon: $tel</h4>";
echo "<h4>Impfstoff: $impfstofftext</h4>"; echo "<h4>Impfstoff: $impfstofftext</h4>";
echo "<h4>Zeitraum: $impfenzeitraum </h4><br>"; echo "<h4>Zeitraum:<br>$impfenzeitraum</h4><br>";
echo "<form action='". $_SERVER['PHP_SELF'] . "' method=POST>"; echo "<form action='". $_SERVER['PHP_SELF'] . "' method=POST>";
echo '<input type="hidden" name="warteid" id="warteid" value="'. $warteid .'" />'; echo '<input type="hidden" name="warteid" id="warteid" value="'. $warteid .'" />';
@@ -151,6 +203,13 @@ if(isset($_POST["id"]) || isset($_GET["id"])){
echo "Sie haben die folgenden Angaben:<br><br>"; echo "Sie haben die folgenden Angaben:<br><br>";
echo "<h4>Name: $userausgabe</h4>"; echo "<h4>Name: $userausgabe</h4>";
echo "<h4>Impfstoff: $impfstofftext</h4><br>"; echo "<h4>Impfstoff: $impfstofftext</h4><br>";
if (!empty($ausgewaehlteZeitraeume)) {
echo "<h4>Ausgewählte Zeitfenster:<br>" . implode('<br>', array_map(static function ($label) {
return e((string)$label);
}, $ausgewaehlteZeitraeume)) . "</h4>";
} else {
echo "<h4>Zeitraum: $impfenzeitraum</h4>";
}
echo "<h4>Wir informieren Sie, sobald ein konkreter Impftermin für Ihren Impfstoff festgelegt wurde.</h4>"; echo "<h4>Wir informieren Sie, sobald ein konkreter Impftermin für Ihren Impfstoff festgelegt wurde.</h4>";
echo "Die Terminvergabe erfolgt durch das Praxisteam, sobald eine komplette Impfflasche mit passenden Wartelistenplätzen gefüllt ist.<br><br>"; echo "Die Terminvergabe erfolgt durch das Praxisteam, sobald eine komplette Impfflasche mit passenden Wartelistenplätzen gefüllt ist.<br><br>";
echo "Können Sie Ihren Warteplatz nicht wahrnehmen oder benötigen diesen nicht mehr, dann tragen Sie sich bitte aus:<br>"; echo "Können Sie Ihren Warteplatz nicht wahrnehmen oder benötigen diesen nicht mehr, dann tragen Sie sich bitte aus:<br>";
@@ -198,6 +257,24 @@ if(isset($_POST["id"]) || isset($_GET["id"])){
echo "<input type=hidden name='".$key."' value='".$value. "'>\n"; echo "<input type=hidden name='".$key."' value='".$value. "'>\n";
continue; continue;
} }
if($key === "impfenzeitraeume" && is_array($value)){
$zeitraumIds = impfNormalizeZeitraumIds($value);
$zeitraumLabels = [];
foreach ($zeitraumIds as $zeitraumId) {
$zeitraumRow = null;
if (isset($pdo) && $pdo instanceof PDO) {
$zeitraumRow = impfLoadZeitraumById($pdo, (int)$zeitraumId, true);
}
if ($zeitraumRow) {
$zeitraumLabels[] = (string)$zeitraumRow['label'];
echo '<input type="hidden" name="impfenzeitraeume[]" value="'.(int)$zeitraumId.'">' . "\n";
}
}
echo "<tr><td width=100 valign=top class=fett>$key:</td><td>" . implode("<br>", array_map(static function ($label) {
return e((string)$label);
}, $zeitraumLabels)) . "</td></tr>\n";
continue;
}
if($value !== ""){ if($value !== ""){
if($key == "Impfstoff"){ if($key == "Impfstoff"){
$sqlimpfstoffstring = "SELECT * FROM impfstoff WHERE impfid ='" . $value . "' order by sortierung"; $sqlimpfstoffstring = "SELECT * FROM impfstoff WHERE impfid ='" . $value . "' order by sortierung";
@@ -215,6 +292,7 @@ if(isset($_POST["id"]) || isset($_GET["id"])){
} }
$zeitraumText = $zeitraumRow ? $zeitraumRow['label'] : 'Unbekannter Zeitraum'; $zeitraumText = $zeitraumRow ? $zeitraumRow['label'] : 'Unbekannter Zeitraum';
echo "<tr><td width=100 valign=top class=fett>$key:</td><td>$zeitraumText</td></tr>\n"; echo "<tr><td width=100 valign=top class=fett>$key:</td><td>$zeitraumText</td></tr>\n";
echo"<input type=hidden name='impfenzeitraeume[]' value='".(int)$value."'>\n";
echo"<input type=hidden name='".$key."' value='".$value. "'>\n"; echo"<input type=hidden name='".$key."' value='".$value. "'>\n";
}else{ }else{
echo "<tr><td width=100 valign=top class=fett>$key:</td><td>$value</td></tr>\n"; echo "<tr><td width=100 valign=top class=fett>$key:</td><td>$value</td></tr>\n";
@@ -249,12 +327,12 @@ if(isset($_POST["id"]) || isset($_GET["id"])){
$Impfaufklaerung = mysqli_real_escape_string($con, $_POST["Impfaufklaerung"] ?? "Nein"); $Impfaufklaerung = mysqli_real_escape_string($con, $_POST["Impfaufklaerung"] ?? "Nein");
$WeitereFragen = mysqli_real_escape_string($con, $_POST["WeitereFragen"] ?? "Nein"); $WeitereFragen = mysqli_real_escape_string($con, $_POST["WeitereFragen"] ?? "Nein");
$impfenmit = mysqli_real_escape_string($con, $_POST["zusammenmit"] ?? ""); $impfenmit = mysqli_real_escape_string($con, $_POST["zusammenmit"] ?? "");
$impfenzeitraumId = (int)($_POST["impfenzeitraum"] ?? 0); $impfenzeitraumIds = impfNormalizeZeitraumIds($_POST["impfenzeitraeume"] ?? ($_POST["impfenzeitraum"] ?? []));
$impfart = (int)($_POST["impfart"] ?? 0); $impfart = (int)($_POST["impfart"] ?? 0);
$letzteimpfung = trim($_POST["letzteimpfung"] ?? ""); $letzteimpfung = trim($_POST["letzteimpfung"] ?? "");
if ($impfstoff <= 0 || $impfart <= 0 || $impfenzeitraumId <= 0) { if ($impfstoff <= 0 || $impfart <= 0 || empty($impfenzeitraumIds)) {
echo "<h3>Pflichtfelder fehlen</h3><br>Bitte wählen Sie Impfstoff, Zeitraum und Impfungsart aus.<br><br>"; echo "<h3>Pflichtfelder fehlen</h3><br>Bitte wählen Sie Impfstoff, mindestens einen Zeitraum und die Impfungsart aus.<br><br>";
goto end_aktion_1; goto end_aktion_1;
} }
@@ -267,12 +345,19 @@ if(isset($_POST["id"]) || isset($_GET["id"])){
$letzteimpfung = ""; $letzteimpfung = "";
} }
$zeitraumRow = (isset($pdo) && $pdo instanceof PDO) ? impfLoadZeitraumById($pdo, $impfenzeitraumId, true) : null; $zeitraumLabels = [];
if (isset($pdo) && $pdo instanceof PDO) {
foreach ($impfenzeitraumIds as $impfenzeitraumId) {
$zeitraumRow = impfLoadZeitraumById($pdo, (int)$impfenzeitraumId, true);
if (!$zeitraumRow || !in_array($impfstoff, $zeitraumRow['impfstoff_id_list'] ?? [], true)) { if (!$zeitraumRow || !in_array($impfstoff, $zeitraumRow['impfstoff_id_list'] ?? [], true)) {
echo "<h3>Ungültiger Zeitraum</h3><br>Bitte wählen Sie einen gültigen Zeitraum für den ausgewählten Impfstoff.<br><br>"; echo "<h3>Ungültiger Zeitraum</h3><br>Bitte wählen Sie einen gültigen Zeitraum für den ausgewählten Impfstoff aus.<br><br>";
goto end_aktion_1; goto end_aktion_1;
} }
$impfenzeitraum = mysqli_real_escape_string($con, $zeitraumRow['label']); $zeitraumLabels[] = (string)$zeitraumRow['label'];
}
}
$impfenzeitraum = mysqli_real_escape_string($con, implode(' | ', $zeitraumLabels));
$impfenzeitraumId = (int)($impfenzeitraumIds[0] ?? 0);
//echo $impfenmit; //echo $impfenmit;
@@ -333,12 +418,43 @@ if(isset($_POST["id"]) || isset($_GET["id"])){
$letzteimpfungSql = ($letzteimpfung !== "") ? ("'" . mysqli_real_escape_string($con, $letzteimpfung) . "'") : "NULL"; $letzteimpfungSql = ($letzteimpfung !== "") ? ("'" . mysqli_real_escape_string($con, $letzteimpfung) . "'") : "NULL";
$query = mysqli_query($con, "SELECT * FROM warteliste WHERE userid='" . (int)$userid . "'"); $query = mysqli_query($con, "SELECT * FROM warteliste WHERE userid='" . (int)$userid . "'");
if($query && $query->num_rows == 0){ if($query && $query->num_rows == 0){
$query = mysqli_query($con, "INSERT INTO warteliste (userid, hash, impfenangebot, impfstoff, Patientenart,Impfaufklaerung, WeitereFragen, date_created, impfenmit, impfenzeitraum, zeitraum_id, impfart, letzteimpfung, checked) VALUES ('". (int)$userid ."', '".$hash."', '".$impfenangebot."', '".$impfstoff."', '".$Patientenart."', '".$Impfaufklaerung."', '".$WeitereFragen."', now(), '".$impfenmit."', '".$impfenzeitraum."', '". (int)$impfenzeitraumId ."', '".$impfart."', ".$letzteimpfungSql.", '0')"); $warteid = 0;
if($query){ $saveOk = false;
$warteid = mysqli_insert_id($con); $saveErrorShown = false;
SendMailMessageVorlage($pdo, "2", $warteid , "8" ); try {
if (isset($pdo) && $pdo instanceof PDO) {
[$ok, $msg, $newWarteid] = impfCreateWaitlistEntryForPerson(
$pdo,
(int)$userid,
$impfstoff,
$impfenzeitraumIds,
$impfart,
($letzteimpfung !== "") ? $letzteimpfung : null,
0
);
if (!$ok) {
throw new RuntimeException($msg);
}
$warteid = (int)$newWarteid;
$pdo->prepare("UPDATE warteliste SET impfenangebot = :impfenangebot, impfenmit = :impfenmit, Impfaufklaerung = :aufklaerung, WeitereFragen = :fragen WHERE warteid = :warteid")
->execute([
'impfenangebot' => $impfenangebot,
'impfenmit' => $impfenmit,
'aufklaerung' => $Impfaufklaerung,
'fragen' => $WeitereFragen,
'warteid' => $warteid,
]);
$saveOk = true;
} else {
throw new RuntimeException("PDO Verbindungsobjekt fehlt.");
}
SendMailMessageVorlage($pdo, "2", (int)$warteid, "8" );
echo "<h3>Nachricht abgeschickt!</h3><br>Sie müssen die Eintragung in der Warteliste noch bestätigen!<br>Überprüfen Sie auch Ihren Spam-Filter!<br><br>"; echo "<h3>Nachricht abgeschickt!</h3><br>Sie müssen die Eintragung in der Warteliste noch bestätigen!<br>Überprüfen Sie auch Ihren Spam-Filter!<br><br>";
}else{ } catch (Throwable $e) {
echo "<h3>Speicherung nicht erfolgreich</h3><br>" . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8') . "<br><br>";
$saveErrorShown = true;
}
if(!$saveOk && !$saveErrorShown){
echo "<h3>Speicherung nicht erfolgreich</h3><br>Ihre Anfrage konnte nicht gespeichert werden.<br>Nutzen Sie das Formular erneut<br><br>"; echo "<h3>Speicherung nicht erfolgreich</h3><br>Ihre Anfrage konnte nicht gespeichert werden.<br>Nutzen Sie das Formular erneut<br><br>";
} }
}else{ }else{
@@ -381,6 +497,13 @@ if(isset($_POST["id"]) || isset($_GET["id"])){
$userausgabe = $vorname . " " . $nachname; $userausgabe = $vorname . " " . $nachname;
SendMailMessageVorlage($pdo, "2", $warteid , "9" ); SendMailMessageVorlage($pdo, "2", $warteid , "9" );
if (isset($pdo) && $pdo instanceof PDO) {
try {
impfWorkflowNotificationProcess($pdo);
} catch (Throwable $e) {
error_log('impfWorkflowNotificationProcess failed in impfwarteliste confirm: ' . $e->getMessage());
}
}
@@ -398,11 +521,22 @@ if(isset($_POST["id"]) || isset($_GET["id"])){
$queryimpf = mysqli_query($con, "SELECT * FROM warteliste WHERE warteid='" . $_POST["warteid"] . "'"); $queryimpf = mysqli_query($con, "SELECT * FROM warteliste WHERE warteid='" . $_POST["warteid"] . "'");
$rowimpf = $queryimpf->fetch_assoc() ; $rowimpf = $queryimpf->fetch_assoc() ;
SendMailMessageVorlage($pdo, "2", $_POST["warteid"], "10" ); SendMailMessageVorlage($pdo, "2", $_POST["warteid"], "10" );
if (isset($pdo) && $pdo instanceof PDO) {
$pdo->prepare("DELETE FROM warteliste_zeitraum WHERE warteid = :warteid")->execute([
'warteid' => (int)$_POST["warteid"],
]);
}
$query = mysqli_query($con, "DELETE FROM warteliste WHERE warteid ='".$_POST["warteid"]."'"); $query = mysqli_query($con, "DELETE FROM warteliste WHERE warteid ='".$_POST["warteid"]."'");
if($query){ if($query){
echo "<h4>Ihr Warteplatz wurde erfolgreich gelöscht!<h4><br>"; echo "<h4>Ihr Warteplatz wurde erfolgreich gelöscht!<h4><br>";
echo "Sie erhalten gleiche eine schriftliche Bestätigung per E-Mail<br>"; echo "Sie erhalten gleiche eine schriftliche Bestätigung per E-Mail<br>";
if (isset($pdo) && $pdo instanceof PDO) {
try {
impfWorkflowNotificationProcess($pdo);
} catch (Throwable $e) {
error_log('impfWorkflowNotificationProcess failed in impfwarteliste delete: ' . $e->getMessage());
}
}
} }
} }
@@ -443,8 +577,11 @@ if(isset($_POST["id"]) || isset($_GET["id"])){
echo "<h4>Mail wird gleich versendet!</h4>"; echo "<h4>Mail wird gleich versendet!</h4>";
echo "<br>Überprüfen Sie auch Ihren SPAM Ordner!<br>"; echo "<br>Überprüfen Sie auch Ihren SPAM Ordner!<br>";
SendMailMessageVorlage($pdo, "1", (int)$terminid, "1" ); SendMailMessageVorlage($pdo, "1", (int)$terminid, "1" );
if (isset($pdo) && $pdo instanceof PDO) {
$pdo->prepare("DELETE FROM warteliste_zeitraum WHERE warteid = :warteid")->execute([
'warteid' => (int)$warteid,
]);
}
$query = mysqli_query($con, "DELETE FROM warteliste WHERE warteid ='".$warteid."'"); $query = mysqli_query($con, "DELETE FROM warteliste WHERE warteid ='".$warteid."'");
}else{ }else{
echo "<h4>Fehler bei Speichern der Anfragen!</h4>"; echo "<h4>Fehler bei Speichern der Anfragen!</h4>";
@@ -512,7 +649,7 @@ if(isset($_POST["id"]) || isset($_GET["id"])){
} }
$zeitOptionenByImpfstoff[$iid][] = [ $zeitOptionenByImpfstoff[$iid][] = [
'id' => (int)$zeitraum['zeitraum_id'], 'id' => (int)$zeitraum['zeitraum_id'],
'label' => (string)$zeitraum['label'], 'label' => impfZeitraumLabel($zeitraum, false),
]; ];
} }
} }
@@ -602,9 +739,9 @@ if(isset($_POST["id"]) || isset($_GET["id"])){
</div> </div>
<br> <br>
<div class="12u"> <div class="12u">
<label for="impfenzeitraum">Wählen Sie den möglichen Zeitbereich für den gewählten Impfstoff:</label> <label for="impfenzeitraeume">Wählen Sie die möglichen Zeitbereiche für den gewählten Impfstoff:</label>
<div class="select-wrapper"> <div class="select-wrapper">
<select name="impfenzeitraum" id="impfenzeitraum" required disabled onchange="checkZeitraum()"> <select name="impfenzeitraeume[]" id="impfenzeitraeume" multiple size="6" required disabled onchange="checkZeitraum()">
<option value="">- Bitte zuerst Impfstoff auswählen -</option> <option value="">- Bitte zuerst Impfstoff auswählen -</option>
</select> </select>
<div id="Zeitrauminfo"></div> <div id="Zeitrauminfo"></div>
@@ -680,6 +817,7 @@ if(isset($_POST["id"]) || isset($_GET["id"])){
<script> <script>
const zeitfensterByImpfstoff = <?php echo $zeitOptionenJson ?: '{}'; ?>; const zeitfensterByImpfstoff = <?php echo $zeitOptionenJson ?: '{}'; ?>;
const initialZeitraumIds = <?php echo json_encode(impfNormalizeZeitraumIds($_POST["impfenzeitraeume"] ?? ($_POST["impfenzeitraum"] ?? [])), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
function isIE() { function isIE() {
return /Trident\/|MSIE/.test(window.navigator.userAgent); return /Trident\/|MSIE/.test(window.navigator.userAgent);
@@ -687,7 +825,7 @@ if(isset($_POST["id"]) || isset($_GET["id"])){
function updateZeitfenster() { function updateZeitfenster() {
const impfstoff = document.getElementById('AstraImpfung'); const impfstoff = document.getElementById('AstraImpfung');
const zeitraum = document.getElementById('impfenzeitraum'); const zeitraum = document.getElementById('impfenzeitraeume');
const zeitraumInfo = document.getElementById('Zeitrauminfo'); const zeitraumInfo = document.getElementById('Zeitrauminfo');
const submit = document.getElementById('submitbox'); const submit = document.getElementById('submitbox');
const impfstoffId = impfstoff.value; const impfstoffId = impfstoff.value;
@@ -697,7 +835,7 @@ if(isset($_POST["id"]) || isset($_GET["id"])){
if (!impfstoffId || optionen.length === 0) { if (!impfstoffId || optionen.length === 0) {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = ''; opt.value = '';
opt.textContent = '- Kein Zeitbereich verfügbar -'; opt.textContent = '- Keine Zeitbereiche verfügbar -';
zeitraum.appendChild(opt); zeitraum.appendChild(opt);
zeitraum.disabled = true; zeitraum.disabled = true;
submit.disabled = true; submit.disabled = true;
@@ -713,11 +851,15 @@ if(isset($_POST["id"]) || isset($_GET["id"])){
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = String(eintrag.id || ''); opt.value = String(eintrag.id || '');
opt.textContent = eintrag.label || ''; opt.textContent = eintrag.label || '';
if (initialZeitraumIds.includes(Number(eintrag.id))) {
opt.selected = true;
}
zeitraum.appendChild(opt); zeitraum.appendChild(opt);
}); });
zeitraum.disabled = false; zeitraum.disabled = false;
submit.disabled = false; submit.disabled = false;
zeitraumInfo.innerHTML = ''; zeitraumInfo.innerHTML = '';
checkZeitraum();
} }
function checklastImpf() { function checklastImpf() {
@@ -736,10 +878,14 @@ if(isset($_POST["id"]) || isset($_GET["id"])){
function checkZeitraum() { function checkZeitraum() {
const info = document.getElementById('Zeitrauminfo'); const info = document.getElementById('Zeitrauminfo');
if (document.getElementById('impfenzeitraum').value === "") { const submit = document.getElementById('submitbox');
const selected = Array.from(document.getElementById('impfenzeitraeume').selectedOptions || []).filter((opt) => opt.value !== '');
if (selected.length === 0) {
info.innerHTML = ""; info.innerHTML = "";
submit.disabled = true;
} else { } else {
info.innerHTML = '<div style="border:5px solid red; margin: 5px; padding: 5px;">Bitte halten Sie sich den gewählten Zeitraum frei. Die konkrete Terminanfrage erhalten Sie später per E-Mail.</div>'; info.innerHTML = '<div style="border:5px solid red; margin: 5px; padding: 5px;">Bitte halten Sie sich die gewählten Zeitbereiche frei. Die konkrete Terminanfrage erhalten Sie später per E-Mail.</div>';
submit.disabled = false;
} }
} }
@@ -750,6 +896,7 @@ if(isset($_POST["id"]) || isset($_GET["id"])){
} }
checklastImpf(); checklastImpf();
updateZeitfenster(); updateZeitfenster();
checkZeitraum();
}); });
</script> </script>
+387
View File
@@ -0,0 +1,387 @@
<?php
if (!function_exists('vacationSyncTableExists')) {
function vacationSyncTableExists(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('vacationSyncTableHasColumn')) {
function vacationSyncTableHasColumn(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('vacationSyncEnsureSchema')) {
function vacationSyncEnsureSchema(PDO $pdo): void
{
$urlaubExists = vacationSyncTableExists($pdo, 'urlaub');
$companyHolidaysExists = vacationSyncTableExists($pdo, 'company_holidays');
if (!$urlaubExists && !$companyHolidaysExists) {
return;
}
if ($urlaubExists && !vacationSyncTableHasColumn($pdo, 'urlaub', 'company_holiday_id')) {
$pdo->exec("ALTER TABLE urlaub ADD COLUMN company_holiday_id INT NULL AFTER vertreterurl");
}
if (!$companyHolidaysExists) {
return;
}
if (!vacationSyncTableHasColumn($pdo, 'company_holidays', 'urlaub_id')) {
$pdo->exec("ALTER TABLE company_holidays ADD COLUMN urlaub_id INT NULL AFTER created_by");
}
if (!vacationSyncTableHasColumn($pdo, 'company_holidays', 'vertretung')) {
$pdo->exec("ALTER TABLE company_holidays ADD COLUMN vertretung VARCHAR(255) NOT NULL DEFAULT '' AFTER description");
}
if (!vacationSyncTableHasColumn($pdo, 'company_holidays', 'vertretertelefon')) {
$pdo->exec("ALTER TABLE company_holidays ADD COLUMN vertretertelefon VARCHAR(255) NOT NULL DEFAULT '' AFTER vertretung");
}
if (!vacationSyncTableHasColumn($pdo, 'company_holidays', 'vertreteradresse')) {
$pdo->exec("ALTER TABLE company_holidays ADD COLUMN vertreteradresse VARCHAR(1000) NOT NULL DEFAULT '' AFTER vertretertelefon");
}
if (!vacationSyncTableHasColumn($pdo, 'company_holidays', 'vertreterurl')) {
$pdo->exec("ALTER TABLE company_holidays ADD COLUMN vertreterurl VARCHAR(255) NOT NULL DEFAULT '' AFTER vertreteradresse");
}
}
}
if (!function_exists('vacationSyncFindCompanyHolidayIdForUrlaub')) {
function vacationSyncFindCompanyHolidayIdForUrlaub(PDO $pdo, int $urlaubId): int
{
if ($urlaubId <= 0) {
return 0;
}
if (!vacationSyncTableExists($pdo, 'urlaub') || !vacationSyncTableExists($pdo, 'company_holidays')) {
return 0;
}
vacationSyncEnsureSchema($pdo);
$stmt = $pdo->prepare("SELECT company_holiday_id FROM urlaub WHERE urlaubid = :urlaub_id LIMIT 1");
$stmt->execute(['urlaub_id' => $urlaubId]);
$linkedId = (int)($stmt->fetchColumn() ?: 0);
if ($linkedId > 0) {
return $linkedId;
}
$stmt = $pdo->prepare("SELECT id FROM company_holidays WHERE urlaub_id = :urlaub_id LIMIT 1");
$stmt->execute(['urlaub_id' => $urlaubId]);
$linkedId = (int)($stmt->fetchColumn() ?: 0);
if ($linkedId > 0) {
return $linkedId;
}
$stmt = $pdo->prepare("SELECT start, ende FROM urlaub WHERE urlaubid = :urlaub_id LIMIT 1");
$stmt->execute(['urlaub_id' => $urlaubId]);
$urlaub = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$urlaub) {
return 0;
}
$stmt = $pdo->prepare("
SELECT id
FROM company_holidays
WHERE start_date = :start_date
AND end_date = :end_date
ORDER BY id ASC
LIMIT 1
");
$stmt->execute([
'start_date' => $urlaub['start'],
'end_date' => $urlaub['ende'],
]);
return (int)($stmt->fetchColumn() ?: 0);
}
}
if (!function_exists('vacationSyncFindUrlaubIdForCompanyHoliday')) {
function vacationSyncFindUrlaubIdForCompanyHoliday(PDO $pdo, int $companyHolidayId): int
{
if ($companyHolidayId <= 0) {
return 0;
}
if (!vacationSyncTableExists($pdo, 'urlaub') || !vacationSyncTableExists($pdo, 'company_holidays')) {
return 0;
}
vacationSyncEnsureSchema($pdo);
$stmt = $pdo->prepare("SELECT urlaub_id FROM company_holidays WHERE id = :company_holiday_id LIMIT 1");
$stmt->execute(['company_holiday_id' => $companyHolidayId]);
$linkedId = (int)($stmt->fetchColumn() ?: 0);
if ($linkedId > 0) {
return $linkedId;
}
$stmt = $pdo->prepare("SELECT urlaubid FROM urlaub WHERE company_holiday_id = :company_holiday_id LIMIT 1");
$stmt->execute(['company_holiday_id' => $companyHolidayId]);
$linkedId = (int)($stmt->fetchColumn() ?: 0);
if ($linkedId > 0) {
return $linkedId;
}
$stmt = $pdo->prepare("SELECT start_date, end_date FROM company_holidays WHERE id = :company_holiday_id LIMIT 1");
$stmt->execute(['company_holiday_id' => $companyHolidayId]);
$holiday = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$holiday) {
return 0;
}
$stmt = $pdo->prepare("
SELECT urlaubid
FROM urlaub
WHERE start = :start_date
AND ende = :end_date
ORDER BY urlaubid ASC
LIMIT 1
");
$stmt->execute([
'start_date' => $holiday['start_date'],
'end_date' => $holiday['end_date'],
]);
return (int)($stmt->fetchColumn() ?: 0);
}
}
if (!function_exists('vacationSyncCompanyHolidayFromUrlaub')) {
function vacationSyncCompanyHolidayFromUrlaub(PDO $pdo, int $urlaubId, ?int $createdBy = null): int
{
if ($urlaubId <= 0) {
return 0;
}
if (!vacationSyncTableExists($pdo, 'urlaub') || !vacationSyncTableExists($pdo, 'company_holidays')) {
return 0;
}
vacationSyncEnsureSchema($pdo);
$stmtUrlaub = $pdo->prepare("
SELECT urlaubid, start, ende, vertretung, vertretertelefon, vertreteradresse, vertreterurl, company_holiday_id
FROM urlaub
WHERE urlaubid = :urlaub_id
LIMIT 1
");
$stmtUrlaub->execute(['urlaub_id' => $urlaubId]);
$urlaub = $stmtUrlaub->fetch(PDO::FETCH_ASSOC);
if (!$urlaub) {
return 0;
}
$companyHolidayId = (int)($urlaub['company_holiday_id'] ?? 0);
if ($companyHolidayId <= 0) {
$companyHolidayId = vacationSyncFindCompanyHolidayIdForUrlaub($pdo, $urlaubId);
}
$description = 'Betriebsurlaub';
if ($companyHolidayId > 0) {
$stmtExisting = $pdo->prepare("SELECT description FROM company_holidays WHERE id = :company_holiday_id LIMIT 1");
$stmtExisting->execute(['company_holiday_id' => $companyHolidayId]);
$existingDescription = $stmtExisting->fetchColumn();
if ($existingDescription !== false && trim((string)$existingDescription) !== '') {
$description = (string)$existingDescription;
}
}
if ($companyHolidayId > 0) {
$stmtUpdate = $pdo->prepare("
UPDATE company_holidays
SET start_date = :start_date,
end_date = :end_date,
vertretung = :vertretung,
vertretertelefon = :vertretertelefon,
vertreteradresse = :vertreteradresse,
vertreterurl = :vertreterurl,
urlaub_id = :urlaub_id
WHERE id = :company_holiday_id
");
$stmtUpdate->execute([
'start_date' => $urlaub['start'],
'end_date' => $urlaub['ende'],
'vertretung' => (string)$urlaub['vertretung'],
'vertretertelefon' => (string)$urlaub['vertretertelefon'],
'vertreteradresse' => (string)$urlaub['vertreteradresse'],
'vertreterurl' => (string)$urlaub['vertreterurl'],
'urlaub_id' => $urlaubId,
'company_holiday_id' => $companyHolidayId,
]);
} else {
$stmtInsert = $pdo->prepare("
INSERT INTO company_holidays (
start_date, end_date, description, vertretung, vertretertelefon, vertreteradresse, vertreterurl, created_by, urlaub_id
)
VALUES (
:start_date, :end_date, :description, :vertretung, :vertretertelefon, :vertreteradresse, :vertreterurl, :created_by, :urlaub_id
)
");
$stmtInsert->execute([
'start_date' => $urlaub['start'],
'end_date' => $urlaub['ende'],
'description' => $description,
'vertretung' => (string)$urlaub['vertretung'],
'vertretertelefon' => (string)$urlaub['vertretertelefon'],
'vertreteradresse' => (string)$urlaub['vertreteradresse'],
'vertreterurl' => (string)$urlaub['vertreterurl'],
'created_by' => $createdBy,
'urlaub_id' => $urlaubId,
]);
$companyHolidayId = (int)$pdo->lastInsertId();
}
if ($companyHolidayId > 0) {
$stmtLink = $pdo->prepare("UPDATE urlaub SET company_holiday_id = :company_holiday_id WHERE urlaubid = :urlaub_id");
$stmtLink->execute([
'company_holiday_id' => $companyHolidayId,
'urlaub_id' => $urlaubId,
]);
}
return $companyHolidayId;
}
}
if (!function_exists('vacationSyncUrlaubFromCompanyHoliday')) {
function vacationSyncUrlaubFromCompanyHoliday(PDO $pdo, int $companyHolidayId): int
{
if ($companyHolidayId <= 0) {
return 0;
}
if (!vacationSyncTableExists($pdo, 'company_holidays') || !vacationSyncTableExists($pdo, 'urlaub')) {
return 0;
}
vacationSyncEnsureSchema($pdo);
$stmtHoliday = $pdo->prepare("
SELECT id, start_date, end_date, vertretung, vertretertelefon, vertreteradresse, vertreterurl, urlaub_id
FROM company_holidays
WHERE id = :company_holiday_id
LIMIT 1
");
$stmtHoliday->execute(['company_holiday_id' => $companyHolidayId]);
$holiday = $stmtHoliday->fetch(PDO::FETCH_ASSOC);
if (!$holiday) {
return 0;
}
$urlaubId = (int)($holiday['urlaub_id'] ?? 0);
if ($urlaubId <= 0) {
$urlaubId = vacationSyncFindUrlaubIdForCompanyHoliday($pdo, $companyHolidayId);
}
if ($urlaubId > 0) {
$stmtUpdate = $pdo->prepare("
UPDATE urlaub
SET start = :start_date,
ende = :end_date,
vertretung = :vertretung,
vertretertelefon = :vertretertelefon,
vertreteradresse = :vertreteradresse,
vertreterurl = :vertreterurl,
company_holiday_id = :company_holiday_id
WHERE urlaubid = :urlaub_id
");
$stmtUpdate->execute([
'start_date' => $holiday['start_date'],
'end_date' => $holiday['end_date'],
'vertretung' => (string)$holiday['vertretung'],
'vertretertelefon' => (string)$holiday['vertretertelefon'],
'vertreteradresse' => (string)$holiday['vertreteradresse'],
'vertreterurl' => (string)$holiday['vertreterurl'],
'company_holiday_id' => $companyHolidayId,
'urlaub_id' => $urlaubId,
]);
} else {
$stmtInsert = $pdo->prepare("
INSERT INTO urlaub
(vertretung, start, ende, vertretertelefon, vertreteradresse, vertreterurl, company_holiday_id)
VALUES
(:vertretung, :start_date, :end_date, :vertretertelefon, :vertreteradresse, :vertreterurl, :company_holiday_id)
");
$stmtInsert->execute([
'vertretung' => (string)$holiday['vertretung'],
'start_date' => $holiday['start_date'],
'end_date' => $holiday['end_date'],
'vertretertelefon' => (string)$holiday['vertretertelefon'],
'vertreteradresse' => (string)$holiday['vertreteradresse'],
'vertreterurl' => (string)$holiday['vertreterurl'],
'company_holiday_id' => $companyHolidayId,
]);
$urlaubId = (int)$pdo->lastInsertId();
}
if ($urlaubId > 0) {
$stmtLink = $pdo->prepare("UPDATE company_holidays SET urlaub_id = :urlaub_id WHERE id = :company_holiday_id");
$stmtLink->execute([
'urlaub_id' => $urlaubId,
'company_holiday_id' => $companyHolidayId,
]);
}
return $urlaubId;
}
}
if (!function_exists('vacationSyncDeleteCompanyHolidayByUrlaub')) {
function vacationSyncDeleteCompanyHolidayByUrlaub(PDO $pdo, int $urlaubId): void
{
if (!vacationSyncTableExists($pdo, 'urlaub') || !vacationSyncTableExists($pdo, 'company_holidays')) {
return;
}
$companyHolidayId = vacationSyncFindCompanyHolidayIdForUrlaub($pdo, $urlaubId);
if ($companyHolidayId <= 0) {
return;
}
$stmt = $pdo->prepare("DELETE FROM company_holidays WHERE id = :company_holiday_id");
$stmt->execute(['company_holiday_id' => $companyHolidayId]);
}
}
if (!function_exists('vacationSyncDeleteUrlaubByCompanyHoliday')) {
function vacationSyncDeleteUrlaubByCompanyHoliday(PDO $pdo, int $companyHolidayId): void
{
if (!vacationSyncTableExists($pdo, 'urlaub') || !vacationSyncTableExists($pdo, 'company_holidays')) {
return;
}
$urlaubId = vacationSyncFindUrlaubIdForCompanyHoliday($pdo, $companyHolidayId);
if ($urlaubId <= 0) {
return;
}
$stmt = $pdo->prepare("DELETE FROM urlaub WHERE urlaubid = :urlaub_id");
$stmt->execute(['urlaub_id' => $urlaubId]);
}
}
+352 -32
View File
@@ -35,6 +35,41 @@ if (!function_exists('impfTableHasIndex')) {
} }
} }
if (!function_exists('impfEnsureTable')) {
function impfEnsureTable(PDO $pdo, string $table, string $createSql): void
{
if (impfTableExists($pdo, $table)) {
return;
}
$pdo->exec($createSql);
}
}
if (!function_exists('impfNormalizeZeitraumIds')) {
function impfNormalizeZeitraumIds($zeitraumIds): array
{
if ($zeitraumIds === null) {
return [];
}
if (!is_array($zeitraumIds)) {
$zeitraumIds = [$zeitraumIds];
}
$result = [];
foreach ($zeitraumIds as $zeitraumId) {
$zeitraumId = (int)$zeitraumId;
if ($zeitraumId <= 0 || isset($result[$zeitraumId])) {
continue;
}
$result[$zeitraumId] = $zeitraumId;
}
return array_values($result);
}
}
if (!function_exists('impfWeekdayName')) { if (!function_exists('impfWeekdayName')) {
function impfWeekdayName(int $day): string function impfWeekdayName(int $day): string
{ {
@@ -52,24 +87,44 @@ if (!function_exists('impfWeekdayName')) {
} }
} }
if (!function_exists('impfLimitLabelLength')) {
function impfLimitLabelLength(string $text, int $maxLength = 50): string
{
$text = trim($text);
if ($text === '') {
return $text;
}
if (function_exists('mb_strimwidth')) {
return rtrim(mb_strimwidth($text, 0, $maxLength, '...', 'UTF-8'));
}
if (strlen($text) <= $maxLength) {
return $text;
}
return rtrim(substr($text, 0, max(0, $maxLength - 3))) . '...';
}
}
if (!function_exists('impfWorkflowEnsureTables')) { if (!function_exists('impfWorkflowEnsureTables')) {
function impfWorkflowEnsureTables(PDO $pdo): void function impfWorkflowEnsureTables(PDO $pdo): void
{ {
$pdo->exec("CREATE TABLE IF NOT EXISTS impf_workflow_meta ( impfEnsureTable($pdo, 'impf_workflow_meta', "CREATE TABLE impf_workflow_meta (
meta_key VARCHAR(100) NOT NULL, meta_key VARCHAR(100) NOT NULL,
meta_value VARCHAR(255) NOT NULL DEFAULT '', meta_value VARCHAR(255) NOT NULL DEFAULT '',
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (meta_key) PRIMARY KEY (meta_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3"); ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3");
$pdo->exec("CREATE TABLE IF NOT EXISTS impfstoff_workflow ( impfEnsureTable($pdo, 'impfstoff_workflow', "CREATE TABLE impfstoff_workflow (
impfstoff_id INT NOT NULL, impfstoff_id INT NOT NULL,
dosen_pro_flasche INT NOT NULL, dosen_pro_flasche INT NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (impfstoff_id) PRIMARY KEY (impfstoff_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3"); ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3");
$pdo->exec("CREATE TABLE IF NOT EXISTS impfstoff_wochenplan ( impfEnsureTable($pdo, 'impfstoff_wochenplan', "CREATE TABLE impfstoff_wochenplan (
plan_id INT NOT NULL AUTO_INCREMENT, plan_id INT NOT NULL AUTO_INCREMENT,
impfstoff_id INT NOT NULL, impfstoff_id INT NOT NULL,
wochentag TINYINT NOT NULL, wochentag TINYINT NOT NULL,
@@ -83,7 +138,7 @@ if (!function_exists('impfWorkflowEnsureTables')) {
INDEX idx_impfstoff_wochenplan_wochentag (wochentag) INDEX idx_impfstoff_wochenplan_wochentag (wochentag)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3"); ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3");
$pdo->exec("CREATE TABLE IF NOT EXISTS impf_zeitraum ( impfEnsureTable($pdo, 'impf_zeitraum', "CREATE TABLE impf_zeitraum (
zeitraum_id INT NOT NULL AUTO_INCREMENT, zeitraum_id INT NOT NULL AUTO_INCREMENT,
bezeichnung VARCHAR(120) NOT NULL DEFAULT '', bezeichnung VARCHAR(120) NOT NULL DEFAULT '',
wochentag TINYINT NOT NULL, wochentag TINYINT NOT NULL,
@@ -101,7 +156,7 @@ if (!function_exists('impfWorkflowEnsureTables')) {
$pdo->exec("ALTER TABLE impf_zeitraum ADD COLUMN bezeichnung VARCHAR(120) NOT NULL DEFAULT '' AFTER zeitraum_id"); $pdo->exec("ALTER TABLE impf_zeitraum ADD COLUMN bezeichnung VARCHAR(120) NOT NULL DEFAULT '' AFTER zeitraum_id");
} }
$pdo->exec("CREATE TABLE IF NOT EXISTS impf_zeitraum_impfstoff ( impfEnsureTable($pdo, 'impf_zeitraum_impfstoff', "CREATE TABLE impf_zeitraum_impfstoff (
zeitraum_id INT NOT NULL, zeitraum_id INT NOT NULL,
impfstoff_id INT NOT NULL, impfstoff_id INT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -109,6 +164,18 @@ if (!function_exists('impfWorkflowEnsureTables')) {
INDEX idx_impf_zeitraum_impfstoff_impfstoff (impfstoff_id) INDEX idx_impf_zeitraum_impfstoff_impfstoff (impfstoff_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3"); ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3");
impfEnsureTable($pdo, 'warteliste_zeitraum', "CREATE TABLE warteliste_zeitraum (
warteid INT NOT NULL,
zeitraum_id INT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (warteid, zeitraum_id),
INDEX idx_warteliste_zeitraum_zeitraum (zeitraum_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3");
if (impfTableExists($pdo, 'warteliste_zeitraum') && !impfTableHasIndex($pdo, 'warteliste_zeitraum', 'idx_warteliste_zeitraum_zeitraum')) {
$pdo->exec("ALTER TABLE warteliste_zeitraum ADD INDEX idx_warteliste_zeitraum_zeitraum (zeitraum_id)");
}
if (impfTableExists($pdo, 'warteliste') && !impfTableHasColumn($pdo, 'warteliste', 'zeitraum_id')) { if (impfTableExists($pdo, 'warteliste') && !impfTableHasColumn($pdo, 'warteliste', 'zeitraum_id')) {
$pdo->exec("ALTER TABLE warteliste ADD COLUMN zeitraum_id INT NULL AFTER impfenzeitraum"); $pdo->exec("ALTER TABLE warteliste ADD COLUMN zeitraum_id INT NULL AFTER impfenzeitraum");
} }
@@ -117,6 +184,7 @@ if (!function_exists('impfWorkflowEnsureTables')) {
} }
impfWorkflowMigrateLegacyPlans($pdo); impfWorkflowMigrateLegacyPlans($pdo);
impfWorkflowMigrateLegacyWartelisteZeitraeume($pdo);
} }
} }
@@ -227,6 +295,42 @@ if (!function_exists('impfWorkflowMigrateLegacyPlans')) {
} }
} }
if (!function_exists('impfWorkflowMigrateLegacyWartelisteZeitraeume')) {
function impfWorkflowMigrateLegacyWartelisteZeitraeume(PDO $pdo): void
{
if (!impfTableExists($pdo, 'warteliste') || !impfTableExists($pdo, 'warteliste_zeitraum')) {
return;
}
if (impfWorkflowGetMeta($pdo, 'legacy_warteliste_zeitraeume_migrated') === '1') {
return;
}
$manageTransaction = !$pdo->inTransaction();
if ($manageTransaction) {
$pdo->beginTransaction();
}
try {
$pdo->exec("INSERT IGNORE INTO warteliste_zeitraum (warteid, zeitraum_id)
SELECT warteid, zeitraum_id
FROM warteliste
WHERE zeitraum_id IS NOT NULL
AND zeitraum_id > 0");
impfWorkflowSetMeta($pdo, 'legacy_warteliste_zeitraeume_migrated', '1');
if ($manageTransaction) {
$pdo->commit();
}
} catch (Throwable $e) {
if ($manageTransaction && $pdo->inTransaction()) {
$pdo->rollBack();
}
throw $e;
}
}
}
if (!function_exists('impfCsvToIntList')) { if (!function_exists('impfCsvToIntList')) {
function impfCsvToIntList(?string $csv): array function impfCsvToIntList(?string $csv): array
{ {
@@ -243,7 +347,7 @@ if (!function_exists('impfCsvToIntList')) {
} }
if (!function_exists('impfZeitraumLabel')) { if (!function_exists('impfZeitraumLabel')) {
function impfZeitraumLabel(array $zeitraum): string function impfZeitraumLabel(array $zeitraum, bool $includeName = true): string
{ {
$zeitText = impfWeekdayName((int)$zeitraum['wochentag']) . ' ' . substr((string)$zeitraum['start'], 0, 5) . '-' . substr((string)$zeitraum['ende'], 0, 5); $zeitText = impfWeekdayName((int)$zeitraum['wochentag']) . ' ' . substr((string)$zeitraum['start'], 0, 5) . '-' . substr((string)$zeitraum['ende'], 0, 5);
$ort = trim((string)($zeitraum['anzeigename'] ?? '') . ' - ' . (string)($zeitraum['adresse'] ?? '')); $ort = trim((string)($zeitraum['anzeigename'] ?? '') . ' - ' . (string)($zeitraum['adresse'] ?? ''));
@@ -252,12 +356,7 @@ if (!function_exists('impfZeitraumLabel')) {
$zeitText .= ' (' . $ortText . ')'; $zeitText .= ' (' . $ortText . ')';
} }
$bezeichnung = trim((string)($zeitraum['bezeichnung'] ?? '')); return impfLimitLabelLength($zeitText, 50);
if ($bezeichnung !== '') {
return $bezeichnung . ': ' . $zeitText;
}
return $zeitText;
} }
} }
@@ -388,7 +487,7 @@ if (!function_exists('impfGetWartelistenFormOptions')) {
} }
$zeitfenster[$impfstoffId][] = [ $zeitfenster[$impfstoffId][] = [
'id' => (int)$zeitraum['zeitraum_id'], 'id' => (int)$zeitraum['zeitraum_id'],
'label' => (string)$zeitraum['label'], 'label' => impfZeitraumLabel($zeitraum, false),
]; ];
} }
} }
@@ -406,12 +505,206 @@ if (!function_exists('impfGetWartelistenFormOptions')) {
} }
} }
if (!function_exists('impfGetWartelistenZeitraeume')) {
function impfGetWartelistenZeitraeume(PDO $pdo, int $warteid, bool $onlyActive = false): array
{
if ($warteid <= 0 || !impfTableExists($pdo, 'warteliste_zeitraum')) {
return [];
}
$sql = "SELECT z.zeitraum_id, z.bezeichnung, z.wochentag, z.start, z.ende, z.impfortid, z.aktiv, z.created_at,
o.anzeigename, o.adresse
FROM warteliste_zeitraum wz
INNER JOIN impf_zeitraum z ON z.zeitraum_id = wz.zeitraum_id
LEFT JOIN impfort o ON o.ortid = z.impfortid
WHERE wz.warteid = :warteid";
if ($onlyActive) {
$sql .= " AND z.aktiv = 1";
}
$sql .= " ORDER BY z.wochentag, z.start, z.ende, z.bezeichnung, z.zeitraum_id";
$st = $pdo->prepare($sql);
$st->execute(['warteid' => $warteid]);
$rows = $st->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as &$row) {
$row['label'] = impfZeitraumLabel($row);
}
unset($row);
if (!empty($rows)) {
return $rows;
}
$stFallback = $pdo->prepare("SELECT w.zeitraum_id, z.bezeichnung, z.wochentag, z.start, z.ende, z.impfortid, z.aktiv, z.created_at,
o.anzeigename, o.adresse
FROM warteliste w
LEFT JOIN impf_zeitraum z ON z.zeitraum_id = w.zeitraum_id
LEFT JOIN impfort o ON o.ortid = z.impfortid
WHERE w.warteid = :warteid
AND w.zeitraum_id IS NOT NULL
LIMIT 1");
$stFallback->execute(['warteid' => $warteid]);
$row = $stFallback->fetch(PDO::FETCH_ASSOC);
if (!$row) {
return [];
}
$row['label'] = impfZeitraumLabel($row);
return [$row];
}
}
if (!function_exists('impfGetWartelistenZeitraeumeLabels')) {
function impfGetWartelistenZeitraeumeLabels(PDO $pdo, int $warteid, bool $onlyActive = false): array
{
$rows = impfGetWartelistenZeitraeume($pdo, $warteid, $onlyActive);
return array_values(array_map(static function (array $row): string {
return (string)($row['label'] ?? '');
}, $rows));
}
}
if (!function_exists('impfGetWartelistenZeitraeumeLabelsMap')) {
function impfGetWartelistenZeitraeumeLabelsMap(PDO $pdo, array $warteids, bool $onlyActive = false): array
{
$warteids = array_values(array_unique(array_filter(array_map('intval', $warteids), static function (int $warteid): bool {
return $warteid > 0;
})));
if (empty($warteids) || !impfTableExists($pdo, 'warteliste_zeitraum')) {
return [];
}
$result = [];
foreach ($warteids as $warteid) {
$result[$warteid] = [];
}
$placeholders = [];
$params = [];
foreach ($warteids as $index => $warteid) {
$key = 'wid' . $index;
$placeholders[] = ':' . $key;
$params[$key] = $warteid;
}
$inList = implode(', ', $placeholders);
$sql = "SELECT wz.warteid, z.zeitraum_id, z.bezeichnung, z.wochentag, z.start, z.ende, z.impfortid, z.aktiv, z.created_at,
o.anzeigename, o.adresse
FROM warteliste_zeitraum wz
INNER JOIN impf_zeitraum z ON z.zeitraum_id = wz.zeitraum_id
LEFT JOIN impfort o ON o.ortid = z.impfortid
WHERE wz.warteid IN (" . $inList . ")";
if ($onlyActive) {
$sql .= " AND z.aktiv = 1";
}
$sql .= " ORDER BY wz.warteid, z.wochentag, z.start, z.ende, z.bezeichnung, z.zeitraum_id";
$st = $pdo->prepare($sql);
$st->execute($params);
$rows = $st->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
$warteid = (int)($row['warteid'] ?? 0);
if ($warteid <= 0) {
continue;
}
$row['label'] = impfZeitraumLabel($row);
$result[$warteid][] = (string)$row['label'];
}
$missing = array_values(array_filter($warteids, static function (int $warteid) use ($result): bool {
return empty($result[$warteid]);
}));
if (empty($missing)) {
return $result;
}
$fallbackPlaceholders = [];
$fallbackParams = [];
foreach ($missing as $index => $warteid) {
$key = 'f_wid' . $index;
$fallbackPlaceholders[] = ':' . $key;
$fallbackParams[$key] = $warteid;
}
$fallbackInList = implode(', ', $fallbackPlaceholders);
$fallbackSql = "SELECT w.warteid, w.zeitraum_id, z.bezeichnung, z.wochentag, z.start, z.ende, z.impfortid, z.aktiv, z.created_at,
o.anzeigename, o.adresse
FROM warteliste w
LEFT JOIN impf_zeitraum z ON z.zeitraum_id = w.zeitraum_id
LEFT JOIN impfort o ON o.ortid = z.impfortid
WHERE w.warteid IN (" . $fallbackInList . ")
AND w.zeitraum_id IS NOT NULL";
if ($onlyActive) {
$fallbackSql .= " AND z.aktiv = 1";
}
$stFallback = $pdo->prepare($fallbackSql);
$stFallback->execute($fallbackParams);
$fallbackRows = $stFallback->fetchAll(PDO::FETCH_ASSOC);
foreach ($fallbackRows as $row) {
$warteid = (int)($row['warteid'] ?? 0);
if ($warteid <= 0) {
continue;
}
$row['label'] = impfZeitraumLabel($row);
$result[$warteid] = [(string)$row['label']];
}
return $result;
}
}
if (!function_exists('impfSetWartelistenZeitraeume')) {
function impfSetWartelistenZeitraeume(PDO $pdo, int $warteid, $zeitraumIds): void
{
$zeitraumIds = impfNormalizeZeitraumIds($zeitraumIds);
if ($warteid <= 0) {
throw new InvalidArgumentException('Unguelige Wartelisten-ID.');
}
$manageTransaction = !$pdo->inTransaction();
if ($manageTransaction) {
$pdo->beginTransaction();
}
try {
$stDelete = $pdo->prepare("DELETE FROM warteliste_zeitraum WHERE warteid = :warteid");
$stDelete->execute(['warteid' => $warteid]);
if (!empty($zeitraumIds)) {
$stInsert = $pdo->prepare("INSERT INTO warteliste_zeitraum (warteid, zeitraum_id)
VALUES (:warteid, :zeitraum_id)");
foreach ($zeitraumIds as $zeitraumId) {
$stInsert->execute([
'warteid' => $warteid,
'zeitraum_id' => $zeitraumId,
]);
}
}
if ($manageTransaction) {
$pdo->commit();
}
} catch (Throwable $e) {
if ($manageTransaction && $pdo->inTransaction()) {
$pdo->rollBack();
}
throw $e;
}
}
}
if (!function_exists('impfCreateWaitlistEntryForPerson')) { if (!function_exists('impfCreateWaitlistEntryForPerson')) {
function impfCreateWaitlistEntryForPerson( function impfCreateWaitlistEntryForPerson(
PDO $pdo, PDO $pdo,
int $personId, int $personId,
int $impfstoffId, int $impfstoffId,
int $zeitraumId, $zeitraumIds,
int $impfart, int $impfart,
?string $letzteImpfung = null, ?string $letzteImpfung = null,
int $checked = 1 int $checked = 1
@@ -422,13 +715,15 @@ if (!function_exists('impfCreateWaitlistEntryForPerson')) {
if ($impfstoffId <= 0) { if ($impfstoffId <= 0) {
return [false, 'Bitte einen Impfstoff auswaehlen.', null]; return [false, 'Bitte einen Impfstoff auswaehlen.', null];
} }
if ($zeitraumId <= 0) {
return [false, 'Bitte ein Zeitfenster auswaehlen.', null];
}
if ($impfart < 1 || $impfart > 4) { if ($impfart < 1 || $impfart > 4) {
return [false, 'Bitte eine gueltige Impfungsart auswaehlen.', null]; return [false, 'Bitte eine gueltige Impfungsart auswaehlen.', null];
} }
$zeitraumIds = impfNormalizeZeitraumIds($zeitraumIds);
if (empty($zeitraumIds)) {
return [false, 'Bitte mindestens ein Zeitfenster auswaehlen.', null];
}
$letzteImpfung = $letzteImpfung !== null ? trim($letzteImpfung) : null; $letzteImpfung = $letzteImpfung !== null ? trim($letzteImpfung) : null;
if ($impfart === 1) { if ($impfart === 1) {
$letzteImpfung = null; $letzteImpfung = null;
@@ -448,26 +743,32 @@ if (!function_exists('impfCreateWaitlistEntryForPerson')) {
return [false, 'Die Person wurde nicht gefunden.', null]; return [false, 'Die Person wurde nicht gefunden.', null];
} }
$zeitraum = impfLoadZeitraumById($pdo, $zeitraumId, true); $zeitraumRows = [];
if (!$zeitraum) { $zeitraumLabels = [];
return [false, 'Das ausgewaehlte Zeitfenster ist nicht mehr verfuegbar.', null]; foreach ($zeitraumIds as $zeitraumId) {
$row = impfLoadZeitraumById($pdo, $zeitraumId, true);
if (!$row) {
return [false, 'Mindestens ein ausgewaehltes Zeitfenster ist nicht mehr verfuegbar.', null];
} }
if (!in_array($impfstoffId, $zeitraum['impfstoff_id_list'] ?? [], true)) { if (!in_array($impfstoffId, $row['impfstoff_id_list'] ?? [], true)) {
return [false, 'Impfstoff und Zeitfenster passen nicht zusammen.', null]; return [false, 'Impfstoff und Zeitfenster passen nicht zusammen.', null];
} }
$zeitraumRows[$zeitraumId] = $row;
$zeitraumLabels[] = (string)$row['label'];
}
$stDup = $pdo->prepare("SELECT warteid $stDup = $pdo->prepare("SELECT w.warteid
FROM warteliste FROM warteliste w
WHERE userid = :uid LEFT JOIN warteliste_zeitraum wz ON wz.warteid = w.warteid
AND checked IN (0, 1) WHERE w.userid = :uid
AND impfstoff = :impfstoff AND w.checked IN (0, 1)
AND COALESCE(zeitraum_id, 0) = :zeitraum_id AND w.impfstoff = :impfstoff
AND impfart = :impfart AND w.impfart = :impfart
GROUP BY w.warteid
LIMIT 1"); LIMIT 1");
$stDup->execute([ $stDup->execute([
'uid' => $personId, 'uid' => $personId,
'impfstoff' => $impfstoffId, 'impfstoff' => $impfstoffId,
'zeitraum_id' => $zeitraumId,
'impfart' => $impfart, 'impfart' => $impfart,
]); ]);
if ($stDup->fetchColumn()) { if ($stDup->fetchColumn()) {
@@ -477,7 +778,15 @@ if (!function_exists('impfCreateWaitlistEntryForPerson')) {
$patientenart = ((int)($person['patientenart'] ?? 0) === 1) ? 1 : 0; $patientenart = ((int)($person['patientenart'] ?? 0) === 1) ? 1 : 0;
$hash = bin2hex(random_bytes(16)); $hash = bin2hex(random_bytes(16));
$checkedValue = ($checked === 0) ? 0 : 1; $checkedValue = ($checked === 0) ? 0 : 1;
$primaerZeitraumId = (int)$zeitraumIds[0];
$impfenzeitraum = implode(' | ', $zeitraumLabels);
$manageTransaction = !$pdo->inTransaction();
if ($manageTransaction) {
$pdo->beginTransaction();
}
try {
$stInsert = $pdo->prepare("INSERT INTO warteliste $stInsert = $pdo->prepare("INSERT INTO warteliste
(userid, checked, hash, impfenangebot, impfstoff, Patientenart, Impfaufklaerung, WeitereFragen, impfart, impfenmit, letzteimpfung, impfenzeitraum, zeitraum_id, date_created) (userid, checked, hash, impfenangebot, impfstoff, Patientenart, Impfaufklaerung, WeitereFragen, impfart, impfenmit, letzteimpfung, impfenzeitraum, zeitraum_id, date_created)
VALUES VALUES
@@ -490,13 +799,24 @@ if (!function_exists('impfCreateWaitlistEntryForPerson')) {
'patientenart' => $patientenart, 'patientenart' => $patientenart,
'impfart' => $impfart, 'impfart' => $impfart,
'letzteimpfung' => $letzteImpfung, 'letzteimpfung' => $letzteImpfung,
'impfenzeitraum' => (string)$zeitraum['label'], 'impfenzeitraum' => $impfenzeitraum,
'zeitraum_id' => $zeitraumId, 'zeitraum_id' => $primaerZeitraumId,
]); ]);
$warteid = (int)$pdo->lastInsertId(); $warteid = (int)$pdo->lastInsertId();
$personName = trim((string)$person['vorname'] . ' ' . (string)$person['nachname']); impfSetWartelistenZeitraeume($pdo, $warteid, $zeitraumIds);
if ($manageTransaction) {
$pdo->commit();
}
$personName = trim((string)$person['vorname'] . ' ' . (string)$person['nachname']);
return [true, 'Wartelistenplatz fuer ' . $personName . ' wurde gespeichert.', $warteid]; return [true, 'Wartelistenplatz fuer ' . $personName . ' wurde gespeichert.', $warteid];
} catch (Throwable $e) {
if ($manageTransaction && $pdo->inTransaction()) {
$pdo->rollBack();
}
throw $e;
}
} }
} }
+2
View File
@@ -1262,6 +1262,8 @@ function Userspeichern($vorname, $nachname, $geburtstag, $mail, $tele, $ort, $pl
UPDATE persons UPDATE persons
SET vorname=:vorname, SET vorname=:vorname,
nachname=:nachname, nachname=:nachname,
geburtstag=:geburtstag,
email=:email,
tele=:tele, tele=:tele,
ort=:ort, ort=:ort,
plz=:plz, plz=:plz,
+256
View File
@@ -0,0 +1,256 @@
<?php
if (!function_exists('impfWorkflowNotificationEnsureMetaTable')) {
function impfWorkflowNotificationEnsureMetaTable(PDO $pdo): void
{
$pdo->exec("CREATE TABLE IF NOT EXISTS impf_workflow_meta (
meta_key VARCHAR(100) NOT NULL,
meta_value VARCHAR(255) NOT NULL DEFAULT '',
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (meta_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
}
}
if (!function_exists('impfWorkflowNotificationGetMeta')) {
function impfWorkflowNotificationGetMeta(PDO $pdo, string $key): ?string
{
impfWorkflowNotificationEnsureMetaTable($pdo);
$st = $pdo->prepare("SELECT meta_value
FROM impf_workflow_meta
WHERE meta_key = :meta_key
LIMIT 1");
$st->execute(['meta_key' => $key]);
$value = $st->fetchColumn();
return ($value === false) ? null : (string)$value;
}
}
if (!function_exists('impfWorkflowNotificationSetMeta')) {
function impfWorkflowNotificationSetMeta(PDO $pdo, string $key, string $value): void
{
impfWorkflowNotificationEnsureMetaTable($pdo);
$st = $pdo->prepare("INSERT INTO impf_workflow_meta (meta_key, meta_value)
VALUES (:meta_key, :meta_value)
ON DUPLICATE KEY UPDATE meta_value = VALUES(meta_value)");
$st->execute([
'meta_key' => $key,
'meta_value' => $value,
]);
}
}
if (!function_exists('impfWorkflowNotificationGetEmail')) {
function impfWorkflowNotificationGetEmail(PDO $pdo): string
{
return trim((string)(impfWorkflowNotificationGetMeta($pdo, 'benachrichtigung_email') ?? ''));
}
}
if (!function_exists('impfWorkflowNotificationSetEmail')) {
function impfWorkflowNotificationSetEmail(PDO $pdo, string $email): void
{
$email = trim($email);
impfWorkflowNotificationSetMeta($pdo, 'benachrichtigung_email', $email);
}
}
if (!function_exists('impfWorkflowNotificationIsReady')) {
function impfWorkflowNotificationIsReady(PDO $pdo): bool
{
return impfWorkflowNotificationGetEmail($pdo) !== '';
}
}
if (!function_exists('impfWorkflowNotificationShouldTrigger')) {
function impfWorkflowNotificationShouldTrigger(int $wartende, int $dosen): bool
{
if ($wartende < 5) {
return false;
}
if ($dosen > 5) {
return $wartende >= $dosen;
}
return true;
}
}
if (!function_exists('impfWorkflowNotificationStateKey')) {
function impfWorkflowNotificationStateKey(int $impfstoffId, int $zeitraumId): string
{
return 'notification_sent_' . $impfstoffId . '_' . $zeitraumId;
}
}
if (!function_exists('impfWorkflowNotificationCountWaitersForPlan')) {
function impfWorkflowNotificationCountWaitersForPlan(PDO $pdo, int $impfstoffId, int $zeitraumId): int
{
if ($impfstoffId <= 0 || $zeitraumId <= 0) {
return 0;
}
$st = $pdo->prepare("SELECT COUNT(DISTINCT w.userid)
FROM warteliste w
WHERE w.checked = 1
AND (w.impfstoff = :iid OR w.impfstoff = 0)
AND (
EXISTS (
SELECT 1
FROM warteliste_zeitraum wz
WHERE wz.warteid = w.warteid
AND wz.zeitraum_id = :zid
)
OR (
NOT EXISTS (
SELECT 1
FROM warteliste_zeitraum wz_none
WHERE wz_none.warteid = w.warteid
)
AND (w.zeitraum_id = :zid OR w.zeitraum_id IS NULL)
)
)");
$st->execute([
'iid' => $impfstoffId,
'zid' => $zeitraumId,
]);
return (int)$st->fetchColumn();
}
}
if (!function_exists('impfWorkflowNotificationSendForPlan')) {
function impfWorkflowNotificationSendForPlan(
PDO $pdo,
int $impfstoffId,
string $impfstoffName,
int $zeitraumId,
string $zeitraumLabel,
int $wartende,
int $dosen
): array {
if ($impfstoffId <= 0 || $zeitraumId <= 0) {
return [];
}
$stateKey = impfWorkflowNotificationStateKey($impfstoffId, $zeitraumId);
$alreadySent = impfWorkflowNotificationGetMeta($pdo, $stateKey) === '1';
$shouldTrigger = impfWorkflowNotificationShouldTrigger($wartende, $dosen);
if (!$shouldTrigger) {
if ($alreadySent) {
impfWorkflowNotificationSetMeta($pdo, $stateKey, '0');
}
return [];
}
if ($alreadySent) {
return [];
}
$email = impfWorkflowNotificationGetEmail($pdo);
if ($email === '') {
return [];
}
$thresholdText = ($dosen > 5)
? 'Die Flasche hat mehr als 5 Dosen, daher wird erst bei einer vollen Flasche benachrichtigt.'
: 'Es sind mindestens 5 Interessenten fuer dieses Zeitfenster vorhanden.';
$subject = 'Impfworkflow: Warteliste ist bereit fuer ' . $impfstoffName;
$body = '<p>Fuer den Impfworkflow ist ein Zeitfenster benachrichtigungsreif.</p>'
. '<p><strong>Impfstoff:</strong> ' . htmlspecialchars($impfstoffName, ENT_QUOTES, 'UTF-8') . '<br>'
. '<strong>Zeitfenster:</strong> ' . htmlspecialchars($zeitraumLabel, ENT_QUOTES, 'UTF-8') . '<br>'
. '<strong>Interessenten:</strong> ' . $wartende . '<br>'
. '<strong>Dosen pro Flasche:</strong> ' . $dosen . '</p>'
. '<p>' . htmlspecialchars($thresholdText, ENT_QUOTES, 'UTF-8') . '</p>';
if (!SendMailMessage($pdo, $email, $subject, $body)) {
throw new RuntimeException('Benachrichtigungs-E-Mail konnte nicht versendet werden.');
}
impfWorkflowNotificationSetMeta($pdo, $stateKey, '1');
return [[
'impfstoff_id' => $impfstoffId,
'zeitraum_id' => $zeitraumId,
'email' => $email,
'impfstoff' => $impfstoffName,
'zeitraum' => $zeitraumLabel,
'wartende' => $wartende,
'dosen' => $dosen,
]];
}
}
if (!function_exists('impfWorkflowNotificationProcess')) {
function impfWorkflowNotificationProcess(PDO $pdo, int $impfstoffId = 0, array $zeitraumIds = []): array
{
if (!function_exists('impfGetZeitraeumeByImpfstoff') || !function_exists('impfLoadZeitraumById')) {
return [];
}
$zeitraumIds = array_values(array_unique(array_filter(array_map('intval', $zeitraumIds), static function (int $zeitraumId): bool {
return $zeitraumId > 0;
})));
$sql = "SELECT r.impfstoff_id, r.dosen_pro_flasche, i.impfname
FROM impfstoff_workflow r
INNER JOIN impfstoff i ON i.impfid = r.impfstoff_id
WHERE (i.aktiv = 1 OR i.aktivwarteliste = 1 OR i.aktivtermin = 1 OR i.aktivgrippe = 1)";
$params = [];
if ($impfstoffId > 0) {
$sql .= " AND r.impfstoff_id = :iid";
$params['iid'] = $impfstoffId;
}
$sql .= " ORDER BY i.impfname";
$stRules = $pdo->prepare($sql);
$stRules->execute($params);
$rules = $stRules->fetchAll(PDO::FETCH_ASSOC);
if (empty($rules)) {
return [];
}
$zeitraeumeByImpfstoff = impfGetZeitraeumeByImpfstoff($pdo, true);
$sent = [];
foreach ($rules as $rule) {
$currentImpfstoffId = (int)$rule['impfstoff_id'];
$dosen = (int)$rule['dosen_pro_flasche'];
if ($currentImpfstoffId <= 0 || $dosen <= 0 || empty($zeitraeumeByImpfstoff[$currentImpfstoffId])) {
continue;
}
foreach ($zeitraeumeByImpfstoff[$currentImpfstoffId] as $zeitraum) {
$currentZeitraumId = (int)($zeitraum['zeitraum_id'] ?? 0);
if ($currentZeitraumId <= 0) {
continue;
}
if (!empty($zeitraumIds) && !in_array($currentZeitraumId, $zeitraumIds, true)) {
continue;
}
$wartende = impfWorkflowNotificationCountWaitersForPlan($pdo, $currentImpfstoffId, $currentZeitraumId);
$sent = array_merge(
$sent,
impfWorkflowNotificationSendForPlan(
$pdo,
$currentImpfstoffId,
(string)$rule['impfname'],
$currentZeitraumId,
(string)($zeitraum['label'] ?? ''),
$wartende,
$dosen
)
);
}
}
return $sent;
}
}
+74
View File
@@ -0,0 +1,74 @@
<?php
if (!function_exists('websiteContentEnsureEntry')) {
function websiteContentEnsureEntry(PDO $pdo, array $entry): int
{
$title = trim((string)($entry['webseitentitel'] ?? ''));
if ($title === '') {
throw new InvalidArgumentException('webseitentitel is required');
}
$stmt = $pdo->prepare("
SELECT inhaltid
FROM webseiteninhalt
WHERE webseitentitel = :webseitentitel
ORDER BY inhaltid DESC
LIMIT 1
");
$stmt->execute(['webseitentitel' => $title]);
$existingId = (int)($stmt->fetchColumn() ?: 0);
if ($existingId > 0) {
return $existingId;
}
$insert = $pdo->prepare("
INSERT INTO webseiteninhalt (webseitentitel, inhalt, beschreibung, url)
VALUES (:webseitentitel, :inhalt, :beschreibung, :url)
");
$insert->execute([
'webseitentitel' => $title,
'inhalt' => (string)($entry['inhalt'] ?? ''),
'beschreibung' => (string)($entry['beschreibung'] ?? ''),
'url' => (string)($entry['url'] ?? ''),
]);
return (int)$pdo->lastInsertId();
}
}
if (!function_exists('websiteContentEnsureEntries')) {
function websiteContentEnsureEntries(PDO $pdo, array $entries): void
{
foreach ($entries as $entry) {
websiteContentEnsureEntry($pdo, $entry);
}
}
}
if (!function_exists('websiteContentGetByTitle')) {
function websiteContentGetByTitle(PDO $pdo, string $title, string $fallback = ''): array
{
$stmt = $pdo->prepare("
SELECT inhaltid, webseitentitel, inhalt, beschreibung, url
FROM webseiteninhalt
WHERE webseitentitel = :webseitentitel
ORDER BY inhaltid DESC
LIMIT 1
");
$stmt->execute(['webseitentitel' => $title]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
return $row;
}
return [
'inhaltid' => 0,
'webseitentitel' => $title,
'inhalt' => $fallback,
'beschreibung' => '',
'url' => '',
];
}
}
+39 -10
View File
@@ -3,6 +3,7 @@
require_once(__DIR__ . "/../inc/config.inc.php"); require_once(__DIR__ . "/../inc/config.inc.php");
require_once(__DIR__ . "/../inc/functions.inc.php"); require_once(__DIR__ . "/../inc/functions.inc.php");
require_once(__DIR__ . "/../inc/functions.impfen.inc.php"); require_once(__DIR__ . "/../inc/functions.impfen.inc.php");
require_once(__DIR__ . "/../inc/impfworkflow_notifications.inc.php");
ini_set('display_errors', '1'); ini_set('display_errors', '1');
error_reporting(E_ALL); error_reporting(E_ALL);
@@ -66,7 +67,7 @@ $zeitOptionenJson = json_encode($zeitOptionenByImpfstoff, JSON_UNESCAPED_UNICODE
$form = [ $form = [
'impfstoff_id' => (int)($_POST['impfstoff_id'] ?? 0), 'impfstoff_id' => (int)($_POST['impfstoff_id'] ?? 0),
'zeitraum_id' => (int)($_POST['zeitraum_id'] ?? 0), 'zeitraum_ids' => impfNormalizeZeitraumIds($_POST['zeitraum_ids'] ?? ($_POST['zeitraum_id'] ?? [])),
'impfart' => (int)($_POST['impfart'] ?? 0), 'impfart' => (int)($_POST['impfart'] ?? 0),
'letzteimpfung' => trim((string)($_POST['letzteimpfung'] ?? '')), 'letzteimpfung' => trim((string)($_POST['letzteimpfung'] ?? '')),
]; ];
@@ -82,6 +83,10 @@ $stActive = $pdo->prepare("SELECT w.warteid, w.checked, w.impfart, w.letzteimpfu
ORDER BY w.date_created DESC, w.warteid DESC"); ORDER BY w.date_created DESC, w.warteid DESC");
$stActive->execute(['pid' => $personId]); $stActive->execute(['pid' => $personId]);
$activeWaitRows = $stActive->fetchAll(PDO::FETCH_ASSOC); $activeWaitRows = $stActive->fetchAll(PDO::FETCH_ASSOC);
foreach ($activeWaitRows as &$activeWaitRow) {
$activeWaitRow['zeitfenster_labels'] = impfGetWartelistenZeitraeumeLabels($pdo, (int)$activeWaitRow['warteid'], false);
}
unset($activeWaitRow);
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST' && (string)($_POST['aktion'] ?? '') === 'create_waitlist') { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST' && (string)($_POST['aktion'] ?? '') === 'create_waitlist') {
try { try {
@@ -89,7 +94,7 @@ if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST' && (string)($_POST['aktion'
$pdo, $pdo,
$personId, $personId,
(int)$form['impfstoff_id'], (int)$form['impfstoff_id'],
(int)$form['zeitraum_id'], $form['zeitraum_ids'],
(int)$form['impfart'], (int)$form['impfart'],
$form['letzteimpfung'] !== '' ? $form['letzteimpfung'] : null, $form['letzteimpfung'] !== '' ? $form['letzteimpfung'] : null,
1 1
@@ -106,6 +111,13 @@ if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST' && (string)($_POST['aktion'
$stActive->execute(['pid' => $personId]); $stActive->execute(['pid' => $personId]);
$activeWaitRows = $stActive->fetchAll(PDO::FETCH_ASSOC); $activeWaitRows = $stActive->fetchAll(PDO::FETCH_ASSOC);
foreach ($activeWaitRows as &$activeWaitRow) {
$activeWaitRow['zeitfenster_labels'] = impfGetWartelistenZeitraeumeLabels($pdo, (int)$activeWaitRow['warteid'], false);
}
unset($activeWaitRow);
if (isset($pdo) && $pdo instanceof PDO) {
impfWorkflowNotificationProcess($pdo);
}
} else { } else {
$errorMessage = (string)$message; $errorMessage = (string)$message;
} }
@@ -141,12 +153,17 @@ if (!empty($activeWaitRows)) {
echo "Sie koennen im internen Bereich mehrere verschiedene Wartelistenanfragen anlegen. Exakte Duplikate werden weiterhin geblockt."; echo "Sie koennen im internen Bereich mehrere verschiedene Wartelistenanfragen anlegen. Exakte Duplikate werden weiterhin geblockt.";
echo "</div>"; echo "</div>";
echo "<table class='table table-bordered table-striped'>"; echo "<table class='table table-bordered table-striped'>";
echo "<thead><tr><th>Impfstoff</th><th>Zeitraum</th><th>Impfungsart</th><th>Status</th><th>Letzte Impfung</th></tr></thead><tbody>"; echo "<thead><tr><th>Impfstoff</th><th>Zeitraeume</th><th>Impfungsart</th><th>Status</th><th>Letzte Impfung</th></tr></thead><tbody>";
foreach ($activeWaitRows as $activeWaitRow) { foreach ($activeWaitRows as $activeWaitRow) {
$statusText = ((int)$activeWaitRow['checked'] === 1) ? 'Bestaetigt' : 'Unbestaetigt'; $statusText = ((int)$activeWaitRow['checked'] === 1) ? 'Bestaetigt' : 'Unbestaetigt';
$zeitfensterText = !empty($activeWaitRow['zeitfenster_labels'])
? implode('<br>', array_map(static function (string $label): string {
return e($label);
}, $activeWaitRow['zeitfenster_labels']))
: e((string)($activeWaitRow['impfenzeitraum'] ?? ''));
echo "<tr>"; echo "<tr>";
echo "<td>" . e((string)($activeWaitRow['impfname'] ?? 'Unbekannt')) . "</td>"; echo "<td>" . e((string)($activeWaitRow['impfname'] ?? 'Unbekannt')) . "</td>";
echo "<td>" . e((string)($activeWaitRow['impfenzeitraum'] ?? '')) . "</td>"; echo "<td>" . $zeitfensterText . "</td>";
echo "<td>" . e((string)($impfartLabels[(int)$activeWaitRow['impfart']] ?? ('Status ' . (int)$activeWaitRow['impfart']))) . "</td>"; echo "<td>" . e((string)($impfartLabels[(int)$activeWaitRow['impfart']] ?? ('Status ' . (int)$activeWaitRow['impfart']))) . "</td>";
echo "<td>" . e($statusText) . "</td>"; echo "<td>" . e($statusText) . "</td>";
echo "<td>" . e((string)($activeWaitRow['letzteimpfung'] ?? '')) . "</td>"; echo "<td>" . e((string)($activeWaitRow['letzteimpfung'] ?? '')) . "</td>";
@@ -178,8 +195,8 @@ if (empty($verfuegbareImpfstoffe)) {
<div class="row" style="margin-top:12px;"> <div class="row" style="margin-top:12px;">
<div class="col-sm-10"> <div class="col-sm-10">
<label for="zeitraum_id">Welcher Zeitbereich passt fuer Sie?</label> <label for="zeitraum_ids">Welche Zeitbereiche passen fuer Sie?</label>
<select class="form-control" name="zeitraum_id" id="zeitraum_id" required disabled> <select class="form-control" name="zeitraum_ids[]" id="zeitraum_ids" required multiple size="6" disabled onchange="checkLetzteImpfung()">
<option value="">- Bitte zuerst Impfstoff auswaehlen -</option> <option value="">- Bitte zuerst Impfstoff auswaehlen -</option>
</select> </select>
</div> </div>
@@ -215,11 +232,11 @@ if (empty($verfuegbareImpfstoffe)) {
<script> <script>
const zeitfensterByImpfstoff = <?php echo $zeitOptionenJson ?: '{}'; ?>; const zeitfensterByImpfstoff = <?php echo $zeitOptionenJson ?: '{}'; ?>;
const initialZeitraumId = <?php echo (int)$form['zeitraum_id']; ?>; const initialZeitraumIds = <?php echo json_encode($form['zeitraum_ids'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?>;
function updateZeitfenster() { function updateZeitfenster() {
const impfstoff = document.getElementById('impfstoff_id'); const impfstoff = document.getElementById('impfstoff_id');
const zeitraum = document.getElementById('zeitraum_id'); const zeitraum = document.getElementById('zeitraum_ids');
const submit = document.getElementById('submit_waitlist'); const submit = document.getElementById('submit_waitlist');
const impfstoffId = impfstoff.value; const impfstoffId = impfstoff.value;
const optionen = zeitfensterByImpfstoff[impfstoffId] || []; const optionen = zeitfensterByImpfstoff[impfstoffId] || [];
@@ -228,7 +245,7 @@ if (empty($verfuegbareImpfstoffe)) {
if (!impfstoffId || optionen.length === 0) { if (!impfstoffId || optionen.length === 0) {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = ''; opt.value = '';
opt.textContent = '- Kein Zeitfenster verfuegbar -'; opt.textContent = '- Keine Zeitfenster verfuegbar -';
zeitraum.appendChild(opt); zeitraum.appendChild(opt);
zeitraum.disabled = true; zeitraum.disabled = true;
submit.disabled = true; submit.disabled = true;
@@ -244,7 +261,7 @@ if (empty($verfuegbareImpfstoffe)) {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = String(eintrag.id || ''); opt.value = String(eintrag.id || '');
opt.textContent = eintrag.label || ''; opt.textContent = eintrag.label || '';
if (initialZeitraumId > 0 && Number(eintrag.id) === initialZeitraumId) { if (initialZeitraumIds.includes(Number(eintrag.id))) {
opt.selected = true; opt.selected = true;
} }
zeitraum.appendChild(opt); zeitraum.appendChild(opt);
@@ -252,12 +269,24 @@ if (empty($verfuegbareImpfstoffe)) {
zeitraum.disabled = false; zeitraum.disabled = false;
submit.disabled = false; submit.disabled = false;
checkLetzteImpfung();
} }
function checkLetzteImpfung() { function checkLetzteImpfung() {
const impfart = document.getElementById('impfart'); const impfart = document.getElementById('impfart');
const box = document.getElementById('letzteimpfung_box'); const box = document.getElementById('letzteimpfung_box');
const input = document.getElementById('letzteimpfung'); const input = document.getElementById('letzteimpfung');
const zeitraum = document.getElementById('zeitraum_ids');
const submit = document.getElementById('submit_waitlist');
const selectedCount = zeitraum && zeitraum.selectedOptions
? Array.from(zeitraum.selectedOptions).filter((opt) => opt.value !== '').length
: 0;
if (selectedCount === 0) {
submit.disabled = true;
} else if (zeitraum && !zeitraum.disabled) {
submit.disabled = false;
}
if (impfart.value === '' || impfart.value === '1') { if (impfart.value === '' || impfart.value === '1') {
box.style.display = 'none'; box.style.display = 'none';
+1 -1
View File
@@ -1,7 +1,7 @@
$(function() { $(function() {
$("#user_input").autocomplete({ $("#user_input").autocomplete({
source: "inc/suchepatient.php", source: "../inc/suchepatient.php",
minLength: 3, minLength: 3,
select: function( event, ui ) { select: function( event, ui ) {
event.preventDefault(); event.preventDefault();
+4 -4
View File
@@ -29,7 +29,7 @@ fbq('track', 'PageView');
src="https://www.facebook.com/tr?id=1304867248096206&ev=PageView&noscript=1" src="https://www.facebook.com/tr?id=1304867248096206&ev=PageView&noscript=1"
/></noscript> /></noscript>
<!-- End Meta Pixel Code --> <!-- End Meta Pixel Code -->
<!--
<script type="application/ld+json"> <script type="application/ld+json">
{ {
"@context": "https://schema.org/", "@context": "https://schema.org/",
@@ -87,7 +87,7 @@ src="https://www.facebook.com/tr?id=1304867248096206&ev=PageView&noscript=1"
</script> </script>
-->
</head> </head>
<body > <body >
@@ -106,7 +106,7 @@ src="https://www.facebook.com/tr?id=1304867248096206&ev=PageView&noscript=1"
</header> </header>
<!-- Main --> <!-- Main -->
<!--
<section id="main" class="container"> <section id="main" class="container">
<?php <?php
echo showHeaderpraxis(); echo showHeaderpraxis();
@@ -200,7 +200,7 @@ src="https://www.facebook.com/tr?id=1304867248096206&ev=PageView&noscript=1"
<input type=submit class=button value="Zu den Stellenangeboten"> <input type=submit class=button value="Zu den Stellenangeboten">
</form> </form>
</section> </section>
-->
<?php <?php
include_once('footer.php'); include_once('footer.php');
+4 -3
View File
@@ -29,7 +29,7 @@ fbq('track', 'PageView');
src="https://www.facebook.com/tr?id=1304867248096206&ev=PageView&noscript=1" src="https://www.facebook.com/tr?id=1304867248096206&ev=PageView&noscript=1"
/></noscript> /></noscript>
<!-- End Meta Pixel Code --> <!-- End Meta Pixel Code -->
<!--
<script type="application/ld+json"> <script type="application/ld+json">
{ {
"@context": "https://schema.org/", "@context": "https://schema.org/",
@@ -90,7 +90,7 @@ src="https://www.facebook.com/tr?id=1304867248096206&ev=PageView&noscript=1"
} }
</script> </script>
-->
</head> </head>
@@ -120,6 +120,7 @@ src="https://www.facebook.com/tr?id=1304867248096206&ev=PageView&noscript=1"
<div class="12u"> <div class="12u">
<!-- Form --> <!-- Form -->
<!--
<section class="box"> <section class="box">
<h3>Ihre Hausarztpraxis in Coppenbrügge und Bisperode</h3> <h3>Ihre Hausarztpraxis in Coppenbrügge und Bisperode</h3>
@@ -207,7 +208,7 @@ src="https://www.facebook.com/tr?id=1304867248096206&ev=PageView&noscript=1"
<input type=submit class=button value="Zu den Stellenangeboten"> <input type=submit class=button value="Zu den Stellenangeboten">
</form> </form>
</section> </section>
-->
<?php <?php
include_once('footer.php'); include_once('footer.php');
+1
View File
@@ -9,6 +9,7 @@
<li><a href="praxis.php">Die Praxis</a></li> <li><a href="praxis.php">Die Praxis</a></li>
<li><a href="praxis.php#philosophie">Philosophie</a></li> <li><a href="praxis.php#philosophie">Philosophie</a></li>
<li><a href="praxis.php#team">Das Team</a></li> <li><a href="praxis.php#team">Das Team</a></li>
<li><a href="pcm.php">PCM</a></li>
<li><a href="sprechzeiten.php#sprechzeiten">Sprechzeiten</a></li> <li><a href="sprechzeiten.php#sprechzeiten">Sprechzeiten</a></li>
<li><a href="sprechzeiten.php#anfahrt">Anfahrt</a></li> <li><a href="sprechzeiten.php#anfahrt">Anfahrt</a></li>
+96
View File
@@ -0,0 +1,96 @@
<!DOCTYPE HTML>
<html>
<head>
<?php include('header.php'); ?>
<title>Praxis Creutzburg - PCM</title>
</head>
<body>
<header id="header" class="../skel-layers-fixed">
<?php
include('menu.php');
include_once("inc/config.inc.php");
include_once("inc/functions.inc.php");
include_once('inc/functions.impfen.inc.php');
include_once('inc/website_content.inc.php');
$pcmContent = websiteContentGetByTitle($pdo, 'PCM - Seiteninhalt');
?>
</header>
<section id="main" class="container">
<?php echo showHeaderpraxis(); ?>
<section class="box special features">
<span class="image featured"><img src="images/praxis1.jpg" alt="Praxis Creutzburg"></span>
<style>
.pcm-intro {
max-width: 860px;
margin: 0 auto 1.8rem auto;
text-align: center;
}
.pcm-intro .pcm-kicker {
display: inline-flex;
padding: 0.4rem 0.9rem;
margin-bottom: 0.9rem;
border-radius: 999px;
background: linear-gradient(135deg, rgba(233, 241, 255, 0.95), rgba(245, 248, 252, 0.95));
color: #29415f;
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.pcm-intro h2 {
margin-bottom: 0.6rem;
}
.pcm-intro p {
font-size: 1.08rem;
line-height: 1.8;
color: #54626f;
}
.pcm-content {
max-width: 880px;
margin: 0 auto;
text-align: left;
}
.pcm-content h3 {
margin-top: 1.8rem;
color: #30404f;
}
.pcm-content ul {
margin: 0.8rem 0 0;
padding-left: 1.15rem;
line-height: 1.8;
color: #4e5860;
}
.pcm-callout {
margin: 1.3rem 0;
padding: 1rem 1.15rem;
border-left: 4px solid #6b8bb4;
background: #f7f9fc;
border-radius: 16px;
color: #44505d;
}
.pcm-content p {
line-height: 1.8;
color: #4e5860;
}
</style>
<div class="pcm-intro">
<span class="pcm-kicker">Primary Care Management</span>
<h2>PCM - die Qualifikation im Überblick</h2>
<p>PCM stärkt Medizinische Fachangestellte in medizinischen, organisatorischen und kommunikativen Aufgaben rund um die hausärztliche Versorgung. Die ausführliche Einordnung dazu steht im folgenden Inhalt.</p>
</div>
<div class="pcm-content">
<?php if (trim((string)($pcmContent['inhalt'] ?? '')) !== ''): ?>
<?php echo $pcmContent['inhalt']; ?>
<?php else: ?>
<p>Die Inhalte zur Qualifikation PCM werden derzeit vorbereitet.</p>
<?php endif; ?>
</div>
<p><a href="praxis.php#team" class="button alt">Zurück zum Team</a></p>
</section>
</section>
<?php include_once('footer.php'); ?>
</body>
</html>
+220 -8
View File
@@ -79,15 +79,227 @@
</section> </section>
<section class="box special features" id="team"> <section class="box special features" id="team">
<style>
.team-intro {
max-width: 920px;
margin: 0 auto 2.2rem auto;
padding: 0;
text-align: center;
font-size: 1.05rem;
}
.team-list {
max-width: 1080px;
margin: 0 auto;
}
.team-profile {
padding: 2rem 0;
border-top: 1px solid rgba(0, 0, 0, 0.12);
}
.team-profile:first-child {
border-top: 0;
padding-top: 0;
}
.team-profile__role {
display: block;
margin-bottom: 0.55rem;
color: #6c5320;
font-size: 0.92rem;
font-weight: 800;
letter-spacing: 0.18em;
text-transform: uppercase;
}
.team-profile__name {
margin: 0 0 0.2rem 0;
font-size: 2.35rem;
line-height: 1.06;
color: #263545;
}
.team-profile__subtitle {
margin: 0 0 1rem 0;
font-size: 1.18rem;
font-weight: 700;
color: #495666;
}
.team-profile__body {
display: grid;
grid-template-columns: 240px 1fr;
gap: 1.6rem;
align-items: center;
}
.team-profile__media img {
display: block;
width: 100%;
height: auto;
aspect-ratio: 1 / 1;
object-fit: cover;
border-radius: 20px;
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08);
}
.team-profile__text p:first-child {
margin-top: 0;
}
.team-profile__text p:last-child {
margin-bottom: 0;
}
.team-profile__points {
margin: 1rem 0 0;
padding-left: 1.1rem;
}
.team-profile__points li {
margin-bottom: 0.35rem;
}
@media (max-width: 736px) {
.team-profile {
padding: 1.5rem 0;
}
.team-profile__body {
grid-template-columns: 1fr;
}
.team-profile__name {
font-size: 1.9rem;
}
}
</style>
<h2>Praxis-Team</h2> <h2>Praxis-Team</h2>
<h3>Arzt</h3> <div class="team-intro">
Heiner Creutzburg <p>Unser Team arbeitet eng zusammen und begleitet Sie vom Empfang bis zur Behandlung mit Ruhe, Struktur und einem freundlichen Miteinander. Neben der ärztlichen Versorgung ist gerade die Organisation im Hintergrund ein wichtiger Teil guter Praxisarbeit.</p>
<span class="image featured"><img src="images/arztbuero.jpg" alt="" title=""> <p>Auf dieser Seite stellen wir Ihnen die Menschen vor, die den Praxisalltag prägen und für einen verlässlichen Ablauf sorgen.</p>
</div>
<div class="team-list">
<section class="team-profile">
<div class="team-profile__body">
<div class="team-profile__media">
<img src="images/heiner-creutzburg.jpg" alt="Heiner Creutzburg">
</div>
<div class="team-profile__text">
<span class="team-profile__role">Arzt</span>
<h3 class="team-profile__name">Heiner Creutzburg</h3>
<p class="team-profile__subtitle">Facharzt für Innere Medizin</p>
<p>Heiner Creutzburg steht für die hausärztliche und internistische Versorgung in unserer Praxis. Im Mittelpunkt stehen eine sorgfältige medizinische Einschätzung, eine klare Kommunikation und ein ganzheitlicher Blick auf Beschwerden, Verlauf und Therapie.</p>
<ul class="team-profile__points">
<li>Langjährige Erfahrung in der hausärztlichen Versorgung</li>
<li>Interne Medizin und umfassende Diagnostik</li>
<li>Verlässliche Begleitung in akuten und chronischen Fragen</li>
</ul>
</div>
</div>
</section>
<section class="team-profile">
<div class="team-profile__body">
<div class="team-profile__media">
<img src="images/person-silhouette.svg" alt="Dalia Alayan-Ibrahim">
</div>
<div class="team-profile__text">
<span class="team-profile__role">Medizinische Fachangestellte, Primary Care Management B.Sc.</span>
<h3 class="team-profile__name">Dalia Alayan-Ibrahim</h3>
<p class="team-profile__subtitle">Praxismanagerin, PCM B.Sc.</p>
<p>Dalia Alayan-Ibrahim übernimmt in unserer Praxis die Organisation der Abläufe und die Verantwortung für den Personalbereich. Sie sorgt mit dafür, dass Termine, Kommunikation und interne Abläufe gut ineinandergreifen und Patientinnen und Patienten sich jederzeit gut begleitet fühlen.</p>
<p>Mit ihrem abgeschlossenen Studium im Bereich Primary Care Management (Bachelor of Science) bringt sie zusätzlich vertiefte Kompetenzen in Praxisorganisation, koordinierter Versorgung und teamorientiertem Arbeiten mit. Darüber hinaus übernimmt sie alle delegierten Tätigkeiten, führt Patientengespräche und betreut eine eigene Sprechstunde im dafür vorgesehenen Rahmen. Das stärkt die Struktur im Alltag und schafft mehr Raum für eine verlässliche Betreuung.</p>
<ul class="team-profile__points">
<li>Praxisorganisation und Koordination der Abläufe</li>
<li>Personalverantwortung und strukturierte Teamarbeit</li>
<li>Delegierte Tätigkeiten, Patientengespräche und eigene Sprechstunde</li>
<li>Zusätzliche Stärke durch das PCM Studium</li>
</ul>
<p><a href="pcm.php" class="button alt small">Mehr über PCM</a></p>
</div>
</div>
</section>
<section class="team-profile">
<div class="team-profile__body">
<div class="team-profile__media">
<img src="images/person-silhouette.svg" alt="Medizinische Fachangestellte">
</div>
<div class="team-profile__text">
<span class="team-profile__role">Medizinische Fachangestellte</span>
<h3 class="team-profile__name">Finja Baruth</h3>
<p class="team-profile__subtitle">Anmeldung, Assistenz, Labortätigkeiten und Praxisorganisation</p>
<p>Finja Baruth unterstützt unser Team in der Patientenaufnahme, in der täglichen Assistenz sowie bei vielen organisatorischen Abläufen im Praxisalltag. So bleibt der Ablauf auch an lebhaften Tagen verlässlich, freundlich und gut strukturiert.</p>
<ul class="team-profile__points">
<li>Assistenz und Unterstützung im Behandlungsalltag</li>
<li>Organisation und Koordination an der Anmeldung</li>
<li>Labortätigkeiten und Vorbereitung diagnostischer Abläufe</li>
<li>Typische Standardaufgaben in einer modernen Arztpraxis</li>
<li>Verlässliche Begleitung für Patienten und Team</li>
</ul>
</div>
</div>
</section>
<section class="team-profile">
<div class="team-profile__body">
<div class="team-profile__media">
<img src="images/person-silhouette.svg" alt="Medizinische Fachangestellte">
</div>
<div class="team-profile__text">
<span class="team-profile__role">Medizinische Fachangestellte</span>
<h3 class="team-profile__name">Svenja Vespermann</h3>
<p class="team-profile__subtitle">Assistenz, Labortätigkeiten und verlässliche Praxisabläufe</p>
<p>Svenja Vespermann sorgt mit dafür, dass Vorbereitungen, organisatorische Aufgaben und die praktische Unterstützung im Alltag sauber ineinandergreifen. Das schafft Ruhe im Tagesgeschäft und hilft, Patientinnen und Patienten gut durch die Praxis zu begleiten.</p>
<ul class="team-profile__points">
<li>Unterstützung bei Praxisorganisation und Assistenz</li>
<li>Labortätigkeiten und Mitwirkung bei diagnostischen Standardabläufen</li>
<li>Sorgfältige Vor- und Nachbereitung im Alltag</li>
<li>Typische Aufgaben in Anmeldung, Organisation und Behandlungsablauf</li>
<li>Teamarbeit mit Blick auf einen freundlichen Ablauf</li>
</ul>
</div>
</div>
</section>
<section class="team-profile">
<div class="team-profile__body">
<div class="team-profile__media">
<img src="images/person-silhouette.svg" alt="Auszubildende zur MFA">
</div>
<div class="team-profile__text">
<span class="team-profile__role">Auszubildende zur Medizinische Fachangestellte</span>
<h3 class="team-profile__name">Promise Nnodim</h3>
<p class="team-profile__subtitle">Lernen, Mitwachsen und Praxisnähe</p>
<p>Unsere Auszubildende zur Medizinischen Fachangestellten lernt die Abläufe in der Praxis Schritt für Schritt kennen und wächst dabei in die vielfältigen Aufgaben einer modernen Hausarztpraxis hinein. Begleitung, Lernen und Praxisnähe stehen dabei im Vordergrund.</p>
<ul class="team-profile__points">
<li>Einblick in Anmeldung, Organisation und Assistenz</li>
<li>Begleitung durch erfahrene Kolleginnen und Kollegen</li>
<li>Frühe Verankerung im Team und in den Praxisabläufen</li>
</ul>
</div>
</div>
</section>
<section class="team-profile">
<div class="team-profile__body">
<div class="team-profile__media">
<img src="images/person-silhouette.svg" alt="Barbara Creutzburg">
</div>
<div class="team-profile__text">
<span class="team-profile__role">Praxisassistentin</span>
<h3 class="team-profile__name">Barbara Creutzburg</h3>
<p class="team-profile__subtitle">Unterstützung im Praxisalltag und organisatorische Begleitung</p>
<p>Barbara Creutzburg unterstützt die Praxis in organisatorischen und assistierenden Aufgaben und trägt dazu bei, dass Abläufe im Hintergrund zuverlässig vorbereitet und begleitet werden.</p>
<ul class="team-profile__points">
<li>Unterstützung bei organisatorischen Praxisaufgaben</li>
<li>Assistierende Tätigkeiten im täglichen Ablauf</li>
<li>Verlässliche Begleitung im Hintergrund</li>
</ul>
</div>
</div>
</section>
<section class="team-profile">
<div class="team-profile__body">
<div class="team-profile__media">
<img src="images/clemenscreutzburg.jpg" alt="Clemens Creutzburg">
</div>
<div class="team-profile__text">
<span class="team-profile__role">Webmaster und EDV-Verantwortlicher</span>
<h3 class="team-profile__name">Clemens Creutzburg</h3>
<p class="team-profile__subtitle">Technik, digitale Prozesse und Systembetreuung</p>
<p>Clemens Creutzburg verantwortet die technische Betreuung der digitalen Systeme und unterstützt die Praxis bei Webseite, EDV-Struktur und technischen Abläufen.</p>
<ul class="team-profile__points">
<li>Betreuung der Webseite und digitaler Inhalte</li>
<li>Verantwortung für EDV, Technik und Systemstruktur</li>
<li>Unterstützung bei stabilen digitalen Praxisprozessen</li>
</ul>
</div>
</div>
</section>
</div>
</section> </section>
<?php <?php
View File
+2 -1
View File
@@ -63,6 +63,7 @@ src="https://www.facebook.com/tr?id=1304867248096206&ev=PageView&noscript=1"
Die Praxis Creutzburg hat aktuell die folgende Stellausschreibungen.<br> Die Praxis Creutzburg hat aktuell die folgende Stellausschreibungen.<br>
</section> </section>
<!--
<section class="box"> <section class="box">
<h2>Medizinische Fachangestellte (MFA) in Teilzeit (m/w/d)</h2> <h2>Medizinische Fachangestellte (MFA) in Teilzeit (m/w/d)</h2>
@@ -118,7 +119,7 @@ src="https://www.facebook.com/tr?id=1304867248096206&ev=PageView&noscript=1"
Schlossstraße 18<br> Schlossstraße 18<br>
31863 Coppenbrügge<br> 31863 Coppenbrügge<br>
E-Mail: <a href="mailto:alayan@praxis-creutzburg.de">alayan@praxis-creutzburg.de</a></p> E-Mail: <a href="mailto:alayan@praxis-creutzburg.de">alayan@praxis-creutzburg.de</a></p>
-->
</div> </div>
</div> </div>
+233
View File
@@ -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'; ?>
+10
View File
@@ -17,6 +17,13 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
# echo "Location: createPDF.php?id=$userId&month=$month&year=$year"; # echo "Location: createPDF.php?id=$userId&month=$month&year=$year";
header("Location: createPDF.php?id=$userId&month=$month&year=$year"); header("Location: createPDF.php?id=$userId&month=$month&year=$year");
exit(); exit();
} elseif ($_POST["action"] == "Alle PDFs anzeigen") {
$selectedMonth = $_POST["month"];
$monthYear = explode("/", $selectedMonth);
$month = $monthYear[0];
$year = $monthYear[1];
header("Location: createAllPDF.php?month=$month&year=$year");
exit();
} }
} }
@@ -120,6 +127,9 @@ if ($selectedMonth && $selectedUser) {
<input type="submit" value="Zeiten anzeigen" class="btn btn-primary btn-lg"> <input type="submit" value="Zeiten anzeigen" class="btn btn-primary btn-lg">
<!-- Button zum Anzeigen der PDF --> <!-- Button zum Anzeigen der PDF -->
<input type="submit" name="action" value="PDF anzeigen" class="btn btn-primary btn-lg" formtarget="_blank"> <input type="submit" name="action" value="PDF anzeigen" class="btn btn-primary btn-lg" formtarget="_blank">
<?php if (is_admin_user()): ?>
<input type="submit" name="action" value="Alle PDFs anzeigen" class="btn btn-secondary btn-lg" formtarget="_blank">
<?php endif; ?>
</form> </form>
+28
View File
@@ -12,6 +12,10 @@ if (!isset($_SESSION['userid'])) {
$user_id = $_SESSION['userid']; $user_id = $_SESSION['userid'];
$user = check_user(); $user = check_user();
if (!is_admin_user()) {
die("Keine Rechte für diese Ansicht.");
}
?> ?>
<?php include 'header.php'; ?> <?php include 'header.php'; ?>
@@ -25,6 +29,13 @@ $user = check_user();
<div class="container"> <div class="container">
<h2 class="mb-4">Zeitbuchungsfehler Auswertung</h2> <h2 class="mb-4">Zeitbuchungsfehler Auswertung</h2>
<?php if (!empty($_SESSION['time_error_close_result'])): ?>
<?php $closeResult = $_SESSION['time_error_close_result']; unset($_SESSION['time_error_close_result']); ?>
<div class="alert alert-<?php echo htmlspecialchars($closeResult['type'], ENT_QUOTES, 'UTF-8'); ?>" role="alert">
<?php echo htmlspecialchars($closeResult['message'], ENT_QUOTES, 'UTF-8'); ?>
</div>
<?php endif; ?>
<?php <?php
// Benutzer aus der Datenbank erhalten // Benutzer aus der Datenbank erhalten
@@ -132,6 +143,23 @@ foreach($users AS $user){
<?php if (!empty($invalidDates)): ?> <?php if (!empty($invalidDates)): ?>
<form action="closeEmployeeTimeErrors.php" method="post" class="form-inline mb-3">
<input type="hidden" name="employee_id" value="<?php echo (int)$user['id']; ?>">
<label class="mr-2" for="hours_to_close_<?php echo (int)$user['id']; ?>">Fehlbuchungen automatisch schließen mit</label>
<input
type="number"
step="0.25"
min="0.25"
class="form-control mr-2"
id="hours_to_close_<?php echo (int)$user['id']; ?>"
name="hours_to_close"
value="8"
required
>
<span class="mr-2">Stunden</span>
<button type="submit" class="btn btn-primary">Alle schließen</button>
</form>
<table class="table table-striped"> <table class="table table-striped">
<thead class="thead-dark"> <thead class="thead-dark">
<tr> <tr>
+351 -133
View File
@@ -1,197 +1,415 @@
<?php <?php
// API: returns JSON events for FullCalendar
session_start(); session_start();
require_once(__DIR__ . '/../inc/config.inc.php'); require_once(__DIR__ . '/../inc/config.inc.php');
require_once(__DIR__ . '/../inc/functions.inc.php'); require_once(__DIR__ . '/../inc/functions.inc.php');
require_once(__DIR__ . '/../inc/vacation_absence.inc.php');
// Enable full error reporting for API debugging
ini_set('display_errors', '1'); ini_set('display_errors', '1');
ini_set('display_startup_errors', '1'); ini_set('display_startup_errors', '1');
error_reporting(E_ALL); 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(); $user = check_user();
$isAdmin = is_admin_user(); $isAdmin = is_admin_user();
$start = $_GET['start'] ?? null; // expected ISO date $start = trim((string)($_GET['start'] ?? ''));
$end = $_GET['end'] ?? null; $end = trim((string)($_GET['end'] ?? ''));
$onlyApproved = isset($_GET['only_approved']) && ($_GET['only_approved'] == '1' || $_GET['only_approved'] === 'true');
// public allows non-admin users to request all *approved* vacations (team view)
$public = isset($_GET['public']) && ($_GET['public'] == '1' || $_GET['public'] === 'true');
// include_rejected if set to 1 will return rejected entries; default behavior: do not return rejected for regular users
$includeRejected = isset($_GET['include_rejected']) && ($_GET['include_rejected'] == '1' || $_GET['include_rejected'] === 'true');
// only_personal forces the API to return only the current user's vacations (useful even if the user is admin)
$onlyPersonal = isset($_GET['only_personal']) && ($_GET['only_personal'] == '1' || $_GET['only_personal'] === 'true');
// public_all when used with public=1 returns all non-rejected team vacations (approved + beantragt)
$publicAll = isset($_GET['public_all']) && ($_GET['public_all'] == '1' || $_GET['public_all'] === 'true');
if (!$start || !$end) { if ($start === '' || $end === '') {
http_response_code(400); http_response_code(400);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['error' => 'start and end required']); echo json_encode(['error' => 'start and end required']);
exit; exit;
} }
$events = []; $onlyApproved = isset($_GET['only_approved']) && ($_GET['only_approved'] == '1' || $_GET['only_approved'] === 'true');
try { $public = isset($_GET['public']) && ($_GET['public'] == '1' || $_GET['public'] === 'true');
$branch = 'unknown'; $includeRejected = isset($_GET['include_rejected']) && ($_GET['include_rejected'] == '1' || $_GET['include_rejected'] === 'true');
$debugMode = isset($_GET['debug']) && ($_GET['debug'] == '1' || $_GET['debug'] === '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');
// Vacations: support a personal-only mode, admin mode, and public/team mode $scope = strtolower(trim((string)($_GET['scope'] ?? '')));
if ($onlyPersonal) { if ($scope === '') {
$branch = 'onlyPersonal'; if ($onlyPersonal) {
if ($onlyApproved) { $scope = 'personal';
$branch = 'onlyPersonal_onlyApproved'; } elseif ($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.user_id = ? AND v.start_date <= ? AND v.end_date >= ? AND LOWER(TRIM(v.status)) = 'genehmigt' ORDER BY v.start_date"); $scope = 'team';
$stmt->execute([$_SESSION['userid'], $end, $start]); } elseif ($isAdmin) {
$scope = 'team';
} else { } else {
if ($includeRejected) { $scope = 'personal';
$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 {
// By default admins see genehmigt + beantragt; include_rejected=1 can override
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]);
}
}
} else {
$branch = 'public_or_regular';
if ($public && $onlyApproved) {
$branch = 'public_onlyApproved';
// public team view: show all approved vacations (read-only)
$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';
// public team view: explicitly show only approved (genehmigt) and pending (beantragt) vacations
$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 {
// By default exclude rejected ('abgelehnt') for regular users; include if include_rejected=1
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]);
}
}
} }
} }
try { if (!in_array($scope, ['personal', 'team', 'admin_all'], true)) {
$vacations = $stmt->fetchAll(); $scope = 'personal';
}
// If debug mode is enabled, prepare meta information if ($scope === 'admin_all' && !$isAdmin) {
if ($debugMode) { $scope = 'personal';
$rawStatuses = array_map(function($r){ return $r['status'] ?? null; }, $vacations); }
$meta = [
'branch' => $branch,
'count' => count($vacations),
'raw_statuses' => $rawStatuses
];
}
foreach ($vacations as $v) { $statusMode = strtolower(trim((string)($_GET['status_filter'] ?? '')));
// Normalize status: collapse whitespace (including NBSP), trim, lowercase if ($statusMode === '') {
if (isset($v['status'])) { if ($onlyApproved) {
$normalized = preg_replace('/\s+/u', ' ', $v['status']); $statusMode = 'approved';
$status = mb_strtolower(trim($normalized)); } elseif ($includeRejected) {
$statusMode = 'all';
} elseif ($scope === 'personal' || $scope === 'admin_all') {
$statusMode = 'all';
} else { } else {
$status = ''; $statusMode = 'active';
} }
// Defensive filter: do not expose rejected ('abgelehnt') entries to non-admins }
if (!$isAdmin && !$includeRejected && mb_stripos($status, 'abgelehnt') !== false) {
$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; continue;
} }
$isApproved = (mb_stripos($status, 'genehmigt') !== false);
if ($isAdmin) { if ($requestedTypes !== null && !in_array($absenceType, $requestedTypes, true)) {
$title = $v['vorname'] . ' ' . $v['nachname'] . ' (' . ($v['status'] ?? 'beantragt') . ')'; continue;
} else {
$title = $isApproved ? 'Urlaub' : 'Urlaubsantrag';
} }
// Safely compute end date; fallback to start_date if invalid
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 { try {
$endInclusive = (new DateTime($v['end_date']))->modify('+1 day')->format('Y-m-d'); $endInclusive = (new DateTime($v['end_date']))->modify('+1 day')->format('Y-m-d');
} catch (Exception $e) { } catch (Exception $e) {
$endInclusive = $v['start_date']; $endInclusive = $v['start_date'];
} }
$backgroundColor = vacationApiTypeColor($absenceType);
if ($status === 'abgelehnt') {
$backgroundColor = vacationApiStatusColor($status);
}
$events[] = [ $events[] = [
'id' => 'vac_' . $v['id'], 'id' => 'vac_' . $v['id'],
'title' => $title, 'title' => $title,
'start' => $v['start_date'], 'start' => $v['start_date'],
'end' => $endInclusive, 'end' => $endInclusive,
'allDay' => true, 'allDay' => true,
'color' => ($isApproved) ? '#28a745' : '#ffc107', 'backgroundColor' => $backgroundColor,
'borderColor' => $backgroundColor,
'textColor' => '#ffffff',
'extendedProps' => [ 'extendedProps' => [
'type' => 'user', 'type' => 'user',
'user_id' => $v['user_id'], 'user_id' => $v['user_id'],
'employee_name' => $employeeName,
'absence_type' => $absenceType,
'absence_label' => $typeLabel,
'status' => $v['status'], 'status' => $v['status'],
'comment' => $v['comment_user'] ?? '' 'status_label' => $statusLabel,
] 'comment' => $v['comment_user'] ?? '',
'scope' => $scope,
],
]; ];
}
} 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;
} }
} catch (Exception $ex) { if ($includeCompany) {
header('Content-Type: application/json; charset=utf-8'); $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");
$payload = ['error' => $ex->getMessage(), 'branch' => $branch, 'trace' => $ex->getTraceAsString()]; $stmt->execute([$end, $start]);
echo json_encode($payload); $holidays = $stmt->fetchAll(PDO::FETCH_ASSOC);
exit;
}
// Company holidays (visible to all) foreach ($holidays as $h) {
$stmt = $pdo->prepare("SELECT * FROM company_holidays WHERE start_date <= ? AND end_date >= ? ORDER BY start_date"); try {
$stmt->execute([$end, $start]);
$holidays = $stmt->fetchAll();
foreach ($holidays as $h) {
$endInclusive = (new DateTime($h['end_date']))->modify('+1 day')->format('Y-m-d'); $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[] = [ $events[] = [
'id' => 'com_' . $h['id'], 'id' => 'com_' . $h['id'],
'title' => $h['description'] ?: 'Betriebsurlaub', 'title' => $title,
'start' => $h['start_date'], 'start' => $h['start_date'],
'end' => $endInclusive, 'end' => $endInclusive,
'allDay' => true, 'allDay' => true,
'color' => '#007bff', 'backgroundColor' => '#0b7285',
'borderColor' => '#0b7285',
'textColor' => '#ffffff',
'extendedProps' => [ 'extendedProps' => [
'type' => 'company', 'type' => 'company',
'description' => $h['description'] 'description' => $description,
] 'vertretung' => $h['vertretung'] ?? '',
'vertretertelefon' => $h['vertretertelefon'] ?? '',
'vertreteradresse' => $h['vertreteradresse'] ?? '',
'vertreterurl' => $h['vertreterurl'] ?? '',
],
]; ];
}
} }
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');
if ($debugMode) {
echo json_encode(['events' => $events, 'meta' => $meta]); $json = json_encode(
} else { $events,
echo 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;
?> ?>
+14 -11
View File
@@ -2,12 +2,15 @@
session_start(); session_start();
require_once("inc/config.inc.php"); require_once("inc/config.inc.php");
require_once("inc/functions.inc.php"); require_once("inc/functions.inc.php");
require_once("inc/vacation_absence.inc.php");
$user = check_user(); $user = check_user();
if (!is_admin_user()) { if (!is_admin_user()) {
die('Zugriff verweigert. Nur Chefs dürfen Anträge genehmigen.'); die('Zugriff verweigert. Nur Chefs dürfen Anträge genehmigen.');
} }
vacationAbsenceEnsureSchema($pdo);
// Handle approve/reject actions // Handle approve/reject actions
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['id']) && isset($_POST['action'])) { if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['id']) && isset($_POST['action'])) {
$id = (int)$_POST['id']; $id = (int)$_POST['id'];
@@ -38,12 +41,13 @@ $requests = $stmt->fetchAll();
?> ?>
<div class="container"> <div class="container">
<h2>Urlaubsanträge - Genehmigung</h2> <h2>Abwesenheitsanträge - Genehmigung</h2>
<table class="table table-bordered"> <table class="table table-bordered">
<thead> <thead>
<tr> <tr>
<th>Mitarbeiter</th> <th>Mitarbeiter</th>
<th>Grund</th>
<th>Von</th> <th>Von</th>
<th>Bis</th> <th>Bis</th>
<th>Tage</th> <th>Tage</th>
@@ -56,10 +60,11 @@ $requests = $stmt->fetchAll();
<?php foreach ($requests as $r): ?> <?php foreach ($requests as $r): ?>
<tr> <tr>
<td><?php echo htmlspecialchars($r['vorname'] . ' ' . $r['nachname']); ?></td> <td><?php echo htmlspecialchars($r['vorname'] . ' ' . $r['nachname']); ?></td>
<td><?php echo $r['start_date']; ?></td> <td><?php echo htmlspecialchars(vacationAbsenceReasonLabel($r['absence_reason'] ?? 'urlaub')); ?></td>
<td><?php echo $r['end_date']; ?></td> <td><?php echo htmlspecialchars((string)$r['start_date']); ?></td>
<td><?php echo $r['days']; ?></td> <td><?php echo htmlspecialchars((string)$r['end_date']); ?></td>
<td><?php echo htmlspecialchars($r['comment_user']); ?></td> <td><?php echo (int)$r['days']; ?></td>
<td><?php echo htmlspecialchars((string)($r['comment_user'] ?? '')); ?></td>
<td> <td>
<?php <?php
if ($r['status'] === 'beantragt' || $r['status'] === null) { if ($r['status'] === 'beantragt' || $r['status'] === null) {
@@ -74,7 +79,7 @@ $requests = $stmt->fetchAll();
<td> <td>
<?php if ($r['status'] !== 'genehmigt'): ?> <?php if ($r['status'] !== 'genehmigt'): ?>
<form method="post" style="display:inline-block; margin-right:6px;"> <form method="post" style="display:inline-block; margin-right:6px;">
<input type="hidden" name="id" value="<?php echo $r['id']; ?>"> <input type="hidden" name="id" value="<?php echo (int)$r['id']; ?>">
<input type="hidden" name="action" value="approve"> <input type="hidden" name="action" value="approve">
<button class="btn btn-sm btn-success" type="submit">Genehmigen</button> <button class="btn btn-sm btn-success" type="submit">Genehmigen</button>
</form> </form>
@@ -82,13 +87,13 @@ $requests = $stmt->fetchAll();
<?php if ($r['status'] !== 'abgelehnt'): ?> <?php if ($r['status'] !== 'abgelehnt'): ?>
<form method="post" style="display:inline-block;"> <form method="post" style="display:inline-block;">
<input type="hidden" name="id" value="<?php echo $r['id']; ?>"> <input type="hidden" name="id" value="<?php echo (int)$r['id']; ?>">
<input type="hidden" name="action" value="reject"> <input type="hidden" name="action" value="reject">
<button class="btn btn-sm btn-danger" type="submit">Ablehnen</button> <button class="btn btn-sm btn-danger" type="submit">Ablehnen</button>
</form> </form>
<?php endif; ?> <?php endif; ?>
<form method="post" style="display:inline-block; margin-left:6px;" onsubmit="return confirm('Wirklich löschen?');"> <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="id" value="<?php echo (int)$r['id']; ?>">
<input type="hidden" name="action" value="delete"> <input type="hidden" name="action" value="delete">
<button class="btn btn-sm btn-outline-danger" type="submit">Löschen</button> <button class="btn btn-sm btn-outline-danger" type="submit">Löschen</button>
</form> </form>
@@ -100,6 +105,4 @@ $requests = $stmt->fetchAll();
</div> </div>
<?php include 'footer.php'; <?php include 'footer.php'; ?>
?>
+147
View File
@@ -0,0 +1,147 @@
<?php
session_start();
require_once("inc/config.inc.php");
require_once("inc/functions.inc.php");
$user = check_user();
if (!is_admin_user()) {
http_response_code(403);
die("Keine Berechtigung.");
}
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
die("Ungültige Anfrage.");
}
$employeeId = isset($_POST['employee_id']) ? (int)$_POST['employee_id'] : 0;
$hoursRaw = isset($_POST['hours_to_close']) ? str_replace(',', '.', trim((string)$_POST['hours_to_close'])) : '';
$hoursToClose = is_numeric($hoursRaw) ? (float)$hoursRaw : 0.0;
if ($employeeId <= 0) {
$_SESSION['time_error_close_result'] = [
'type' => 'danger',
'message' => 'Es wurde kein gültiger Mitarbeiter übergeben.',
];
header("Location: allefehlbuchungen.php");
exit;
}
if ($hoursToClose <= 0) {
$_SESSION['time_error_close_result'] = [
'type' => 'danger',
'message' => 'Bitte eine gültige Stundenanzahl größer als 0 angeben.',
];
header("Location: allefehlbuchungen.php");
exit;
}
try {
$stmt = $pdo->prepare("SELECT id, vorname, nachname FROM users WHERE id = :employee_id LIMIT 1");
$stmt->bindValue(':employee_id', $employeeId, PDO::PARAM_INT);
$stmt->execute();
$employee = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$employee) {
throw new RuntimeException('Mitarbeiter wurde nicht gefunden.');
}
$stmt = $pdo->prepare(
"SELECT
DATE(timestamp_datetime) AS datum,
GROUP_CONCAT(timestamp_type ORDER BY timestamp_datetime) AS day_sequence
FROM timestamps
WHERE employee_id = :employee_id
GROUP BY DATE(timestamp_datetime)
ORDER BY datum ASC"
);
$stmt->bindValue(':employee_id', $employeeId, PDO::PARAM_INT);
$stmt->execute();
$days = $stmt->fetchAll(PDO::FETCH_ASSOC);
$invalidDates = [];
foreach ($days as $day) {
if (!isValidSequence((string)$day['day_sequence'])) {
$invalidDates[] = (string)$day['datum'];
}
}
if (empty($invalidDates)) {
$_SESSION['time_error_close_result'] = [
'type' => 'info',
'message' => 'Es wurden keine offenen Fehlbuchungen für den Mitarbeiter gefunden.',
];
header("Location: allefehlbuchungen.php");
exit;
}
$pdo->beginTransaction();
$insertedDates = [];
$skippedDates = [];
$secondsToClose = (int)round($hoursToClose * 3600);
$lastEntryStmt = $pdo->prepare(
"SELECT timestamp_type, timestamp_datetime
FROM timestamps
WHERE employee_id = :employee_id
AND DATE(timestamp_datetime) = :datum
ORDER BY timestamp_datetime DESC, timestamp_id DESC
LIMIT 1"
);
$insertStmt = $pdo->prepare(
"INSERT INTO timestamps (employee_id, timestamp_type, timestamp_datetime, timestamp_endpoint)
VALUES (:employee_id, 'GEHEN', :timestamp_datetime, 0)"
);
foreach ($invalidDates as $invalidDate) {
$lastEntryStmt->bindValue(':employee_id', $employeeId, PDO::PARAM_INT);
$lastEntryStmt->bindValue(':datum', $invalidDate);
$lastEntryStmt->execute();
$lastEntry = $lastEntryStmt->fetch(PDO::FETCH_ASSOC);
if (!$lastEntry || (string)$lastEntry['timestamp_type'] !== 'KOMMEN') {
$skippedDates[] = $invalidDate;
continue;
}
$closeTimestamp = (new DateTimeImmutable((string)$lastEntry['timestamp_datetime']))
->modify('+' . $secondsToClose . ' seconds')
->format('Y-m-d H:i:s');
$insertStmt->bindValue(':employee_id', $employeeId, PDO::PARAM_INT);
$insertStmt->bindValue(':timestamp_datetime', $closeTimestamp);
$insertStmt->execute();
$insertedDates[] = $invalidDate;
}
$pdo->commit();
$message = count($insertedDates) . ' Fehlbuchungstage für '
. $employee['vorname'] . ' ' . $employee['nachname']
. ' wurden mit ' . rtrim(rtrim(number_format($hoursToClose, 2, '.', ''), '0'), '.')
. ' Stunden geschlossen.';
if (!empty($skippedDates)) {
$message .= ' ' . count($skippedDates) . ' Tage konnten nicht automatisch geschlossen werden.';
}
$_SESSION['time_error_close_result'] = [
'type' => !empty($insertedDates) ? 'success' : 'warning',
'message' => $message,
];
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
$_SESSION['time_error_close_result'] = [
'type' => 'danger',
'message' => 'Automatisches Schliessen fehlgeschlagen: ' . $e->getMessage(),
];
}
header("Location: allefehlbuchungen.php");
exit;
+95 -26
View File
@@ -2,52 +2,115 @@
session_start(); session_start();
require_once('inc/config.inc.php'); require_once('inc/config.inc.php');
require_once('inc/functions.inc.php'); require_once('inc/functions.inc.php');
require_once(__DIR__ . '/../inc/company_holiday_sync.inc.php');
$user = check_user(); $user = check_user();
if (!is_admin_user()) { if (!is_admin_user()) {
die('Zugriff verweigert. Nur Chefs dürfen Betriebsurlaub verwalten.'); die('Zugriff verweigert. Nur Chefs duerfen Betriebsurlaub verwalten.');
} }
// Create table if not exists (optional helper) $error = '';
// Administrators can also run the SQL directly in DB. This is just a convenience. $schemaError = '';
try {
vacationSyncEnsureSchema($pdo);
} catch (Throwable $e) {
$schemaError = 'Die Seite konnte das Urlaubsschema nicht automatisch aktualisieren: ' . $e->getMessage();
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['start_date']) && isset($_POST['end_date'])) { if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['start_date']) && isset($_POST['end_date'])) {
$start = $_POST['start_date']; $start = trim((string)($_POST['start_date'] ?? ''));
$end = $_POST['end_date']; $end = trim((string)($_POST['end_date'] ?? ''));
$desc = trim($_POST['description'] ?? 'Betriebsurlaub'); $desc = trim((string)($_POST['description'] ?? 'Betriebsurlaub'));
$vertretung = trim((string)($_POST['vertretung'] ?? ''));
$vertretertelefon = trim((string)($_POST['vertretertelefon'] ?? ''));
$vertreteradresse = trim((string)($_POST['vertreteradresse'] ?? ''));
$vertreterurl = trim((string)($_POST['vertreterurl'] ?? ''));
$stmt = $pdo->prepare("INSERT INTO company_holidays (start_date, end_date, description, created_by) VALUES (?, ?, ?, ?)"); if ($start === '' || $end === '') {
$stmt->execute([$start, $end, $desc, $_SESSION['userid']]); $error = 'Bitte Start- und Enddatum angeben.';
} elseif ($start > $end) {
$error = 'Das Enddatum darf nicht vor dem Startdatum liegen.';
} elseif ($vertretung === '' || $vertretertelefon === '' || $vertreteradresse === '' || $vertreterurl === '') {
$error = 'Bitte alle Vertreterinformationen vollstaendig ausfuellen.';
} elseif ($schemaError !== '') {
$error = $schemaError;
} else {
$stmt = $pdo->prepare("
INSERT INTO company_holidays (
start_date, end_date, description, vertretung, vertretertelefon, vertreteradresse, vertreterurl, created_by
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$start,
$end,
$desc,
$vertretung,
$vertretertelefon,
$vertreteradresse,
$vertreterurl,
$_SESSION['userid']
]);
vacationSyncUrlaubFromCompanyHoliday($pdo, (int)$pdo->lastInsertId());
header('Location: company_holidays.php'); header('Location: company_holidays.php');
exit(); exit();
}
} }
include 'header.php'; include 'header.php';
$stmt = $pdo->prepare("SELECT * FROM company_holidays ORDER BY start_date DESC"); $stmt = $pdo->prepare("SELECT * FROM company_holidays ORDER BY start_date DESC");
$stmt->execute(); $stmt->execute();
$holidays = $stmt->fetchAll(); $holidays = $stmt->fetchAll(PDO::FETCH_ASSOC);
?> ?>
<div class="container"> <div class="container">
<h2>Betriebsurlaub verwalten</h2> <h2>Betriebsurlaub verwalten</h2>
<form method="post" class="form-inline mb-3"> <?php if ($error !== ''): ?>
<div class="form-group mr-2"> <div class="alert alert-danger"><?php echo htmlspecialchars($error); ?></div>
<?php endif; ?>
<?php if ($schemaError !== ''): ?>
<div class="alert alert-warning"><?php echo htmlspecialchars($schemaError); ?></div>
<?php endif; ?>
<form method="post" class="mb-3">
<div class="form-row">
<div class="form-group col-md-3">
<label>Von:</label> <label>Von:</label>
<input type="date" name="start_date" class="form-control" required> <input type="date" name="start_date" class="form-control" required>
</div> </div>
<div class="form-group mr-2"> <div class="form-group col-md-3">
<label>Bis:</label> <label>Bis:</label>
<input type="date" name="end_date" class="form-control" required> <input type="date" name="end_date" class="form-control" required>
</div> </div>
<div class="form-group mr-2"> <div class="form-group col-md-6">
<label>Beschreibung:</label> <label>Beschreibung:</label>
<input type="text" name="description" class="form-control" placeholder="z. B. Betriebsurlaub Weihnachten"> <input type="text" name="description" class="form-control" placeholder="z. B. Betriebsurlaub Weihnachten">
</div> </div>
<button class="btn btn-primary">Hinzufügen</button> </div>
<div class="form-row">
<div class="form-group col-md-6">
<label>Vertretung:</label>
<input type="text" name="vertretung" class="form-control" required>
</div>
<div class="form-group col-md-6">
<label>Vertretung Telefon:</label>
<input type="text" name="vertretertelefon" class="form-control" required>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-8">
<label>Vertretung Adresse:</label>
<input type="text" name="vertreteradresse" class="form-control" required>
</div>
<div class="form-group col-md-4">
<label>Vertretung Webseite:</label>
<input type="text" name="vertreterurl" class="form-control" required>
</div>
</div>
<button class="btn btn-primary">Hinzufuegen</button>
</form> </form>
<table class="table table-bordered"> <table class="table table-bordered">
@@ -56,6 +119,8 @@ $holidays = $stmt->fetchAll();
<th>Von</th> <th>Von</th>
<th>Bis</th> <th>Bis</th>
<th>Beschreibung</th> <th>Beschreibung</th>
<th>Vertretung</th>
<th>Kontakt</th>
<th>Erstellt von</th> <th>Erstellt von</th>
<th>Aktion</th> <th>Aktion</th>
</tr> </tr>
@@ -63,19 +128,25 @@ $holidays = $stmt->fetchAll();
<tbody> <tbody>
<?php foreach ($holidays as $h): ?> <?php foreach ($holidays as $h): ?>
<tr> <tr>
<td><?php echo $h['start_date']; ?></td> <td><?php echo htmlspecialchars((string)$h['start_date']); ?></td>
<td><?php echo $h['end_date']; ?></td> <td><?php echo htmlspecialchars((string)$h['end_date']); ?></td>
<td><?php echo htmlspecialchars($h['description']); ?></td> <td><?php echo htmlspecialchars((string)$h['description']); ?></td>
<td><?php echo htmlspecialchars((string)$h['vertretung']); ?></td>
<td>
<?php echo htmlspecialchars((string)$h['vertretertelefon']); ?><br>
<?php echo htmlspecialchars((string)$h['vertreteradresse']); ?><br>
<?php echo htmlspecialchars((string)$h['vertreterurl']); ?>
</td>
<td><?php <td><?php
$s = $pdo->prepare("SELECT vorname, nachname FROM users WHERE id = ?"); $s = $pdo->prepare("SELECT vorname, nachname FROM users WHERE id = ?");
$s->execute([$h['created_by']]); $s->execute([$h['created_by']]);
$u = $s->fetch(); $u = $s->fetch(PDO::FETCH_ASSOC);
echo htmlspecialchars($u['vorname'] . ' ' . $u['nachname']); echo htmlspecialchars(trim(($u['vorname'] ?? '') . ' ' . ($u['nachname'] ?? '')));
?></td> ?></td>
<td> <td>
<form method="post" action="deleteCompanyHoliday.php" onsubmit="return confirm('Betriebsurlaub wirklich löschen?');"> <form method="post" action="deleteCompanyHoliday.php" onsubmit="return confirm('Betriebsurlaub wirklich loeschen?');">
<input type="hidden" name="id" value="<?php echo intval($h['id']); ?>"> <input type="hidden" name="id" value="<?php echo (int)$h['id']; ?>">
<button class="btn btn-sm btn-danger">Löschen</button> <button class="btn btn-sm btn-danger">Loeschen</button>
</form> </form>
</td> </td>
</tr> </tr>
@@ -85,6 +156,4 @@ $holidays = $stmt->fetchAll();
</div> </div>
<?php include 'footer.php'; <?php include 'footer.php'; ?>
?>
+173
View File
@@ -0,0 +1,173 @@
<?php
session_start();
require_once('./../admin/tcpdf/tcpdf.php');
require_once("inc/config.inc.php");
require_once("inc/functions.inc.php");
if (!isset($_SESSION['userid'])) {
die("Kein Benutzer angemeldet.");
}
$currentUser = check_user();
if (!is_admin_user()) {
die("Keine Berechtigung.");
}
$selectedMonth = $_GET['month'] ?? date('m');
$selectedYear = $_GET['year'] ?? date('Y');
$firmaName = "Praxis Creutzburg";
try {
$usersStmt = $pdo->prepare("
SELECT DISTINCT u.id, u.vorname, u.nachname
FROM users u
INNER JOIN timestamps t ON t.employee_id = u.id
WHERE u.zeiterfassung = '1'
AND MONTH(t.timestamp_datetime) = :month
AND YEAR(t.timestamp_datetime) = :year
ORDER BY u.nachname ASC, u.vorname ASC
");
$usersStmt->bindValue(':month', (int)$selectedMonth, PDO::PARAM_INT);
$usersStmt->bindValue(':year', (int)$selectedYear, PDO::PARAM_INT);
$usersStmt->execute();
$employees = $usersStmt->fetchAll(PDO::FETCH_ASSOC);
$pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
$pdf->SetCreator(PDF_CREATOR);
$pdf->SetAuthor($currentUser['vorname'] . ' ' . $currentUser['nachname']);
$pdf->SetTitle('Arbeitszeiten aller Mitarbeiter');
$pdf->SetSubject('Arbeitszeiten aller Mitarbeiter für ' . $selectedMonth . '/' . $selectedYear);
$stempdate = $selectedYear . "-" . $selectedMonth . "-1";
$date = new DateTime($stempdate);
$formatter = new IntlDateFormatter(
"de-DE",
IntlDateFormatter::LONG,
IntlDateFormatter::NONE,
"Europe/Berlin",
IntlDateFormatter::GREGORIAN,
"MMMM"
);
$monthName = $formatter->format($date);
$sequenceStmt = $pdo->prepare("
SELECT
DATE(timestamp_datetime) AS datum,
GROUP_CONCAT(timestamp_type ORDER BY timestamp_datetime) AS day_sequence
FROM timestamps
WHERE employee_id = :employee_id
AND MONTH(timestamp_datetime) = :selectedMonth
AND YEAR(timestamp_datetime) = :selectedYear
GROUP BY DATE(timestamp_datetime)
");
$timesStmt = $pdo->prepare("
SELECT
DATE(timestamp_datetime) AS day,
MIN(CASE WHEN timestamp_type = 'KOMMEN' THEN timestamp_datetime END) AS first_come,
MAX(CASE WHEN timestamp_type = 'GEHEN' THEN timestamp_datetime END) AS last_go,
SEC_TO_TIME(SUM(
CASE
WHEN timestamp_type = 'GEHEN' THEN UNIX_TIMESTAMP(timestamp_datetime)
WHEN timestamp_type = 'KOMMEN' THEN -UNIX_TIMESTAMP(timestamp_datetime)
ELSE 0
END
)) AS total_time,
SEC_TO_TIME(
TIME_TO_SEC(
SEC_TO_TIME(
UNIX_TIMESTAMP(MAX(CASE WHEN timestamp_type = 'GEHEN' THEN timestamp_datetime END)) -
UNIX_TIMESTAMP(MIN(CASE WHEN timestamp_type = 'KOMMEN' THEN timestamp_datetime END))
)
) - TIME_TO_SEC(SEC_TO_TIME(SUM(
CASE
WHEN timestamp_type = 'GEHEN' THEN UNIX_TIMESTAMP(timestamp_datetime)
WHEN timestamp_type = 'KOMMEN' THEN -UNIX_TIMESTAMP(timestamp_datetime)
ELSE 0
END
)))
) AS difference_between_total_time_and_pause_time
FROM timestamps
WHERE employee_id = :employee_id
AND MONTH(timestamp_datetime) = :month
AND YEAR(timestamp_datetime) = :year
GROUP BY DATE(timestamp_datetime)
");
foreach ($employees as $employee) {
$employeeId = (int)$employee['id'];
$employeeName = trim($employee['vorname'] . ' ' . $employee['nachname']);
$sequenceStmt->bindValue(':employee_id', $employeeId, PDO::PARAM_INT);
$sequenceStmt->bindValue(':selectedMonth', (int)$selectedMonth, PDO::PARAM_INT);
$sequenceStmt->bindValue(':selectedYear', (int)$selectedYear, PDO::PARAM_INT);
$sequenceStmt->execute();
$sequenceRows = $sequenceStmt->fetchAll(PDO::FETCH_ASSOC);
$fehlerhafteTage = [];
foreach ($sequenceRows as $row) {
if (!isValidSequence((string)$row['day_sequence'])) {
$fehlerhafteTage[] = (string)$row['datum'];
}
}
$pdf->AddPage();
$html = '<h1>Arbeitszeiten - ' . $firmaName . '</h1>';
$html .= '<h2>Mitarbeiter: ' . htmlspecialchars($employeeName, ENT_QUOTES, 'UTF-8') . '</h2>';
$html .= '<h3>Monat: ' . htmlspecialchars((string)$monthName, ENT_QUOTES, 'UTF-8') . ' ' . htmlspecialchars((string)$selectedYear, ENT_QUOTES, 'UTF-8') . '</h3>';
if (!empty($fehlerhafteTage)) {
$html .= '<p><strong>Fehlzeiten erkannt.</strong> Bitte erst beheben.</p>';
$html .= '<p>Betroffene Tage: ' . htmlspecialchars(implode(', ', array_map(static function (string $day): string {
return date('d.m.Y', strtotime($day));
}, $fehlerhafteTage)), ENT_QUOTES, 'UTF-8') . '</p>';
$pdf->writeHTML($html, true, false, true, false, '');
continue;
}
$timesStmt->bindValue(':employee_id', $employeeId, PDO::PARAM_INT);
$timesStmt->bindValue(':month', (int)$selectedMonth, PDO::PARAM_INT);
$timesStmt->bindValue(':year', (int)$selectedYear, PDO::PARAM_INT);
$timesStmt->execute();
$timesData = [];
while ($row = $timesStmt->fetch(PDO::FETCH_ASSOC)) {
$day = date('d', strtotime($row['day']));
$timesData[$day] = $row;
}
$html .= '<table border="1" style="font-size:14px;" width="100%">';
$html .= '<tr><th style="width: 7%;">Tag</th><th style="width: 12%;">Start</th><th style="width: 12%;">Pause</th><th style="width: 12%;">Ende</th><th>Gesamtzeit</th><th style="width: 20%;font-size:12px;">aufgezeichnet am:</th><th style="width: 23%;">Bemerkung</th></tr>';
$totalSeconds = 0;
for ($day = 1; $day <= 31; $day++) {
$daytwo = str_pad((string)$day, 2, '0', STR_PAD_LEFT);
if (isset($timesData[$daytwo])) {
$row = $timesData[$daytwo];
$html .= '<tr><td>' . $day . '</td><td>' . date('H:i:s', strtotime($row['first_come'])) . '</td><td>' . $row['difference_between_total_time_and_pause_time'] . '</td><td>' . date('H:i:s', strtotime($row['last_go'])) . '</td><td>' . $row['total_time'] . '</td><td>' . date('d.m.Y', strtotime($row['day'])) . '</td><td></td></tr>';
if (!empty($row['total_time'])) {
[$hours, $minutes, $seconds] = explode(':', $row['total_time']);
$totalSeconds += ((int)$hours * 3600) + ((int)$minutes * 60) + (int)$seconds;
}
} else {
$html .= '<tr><td>' . $day . '</td><td></td><td></td><td></td><td></td><td></td><td></td></tr>';
}
}
$hours = floor($totalSeconds / 3600);
$mins = floor(($totalSeconds / 60) % 60);
$secs = floor($totalSeconds % 60);
$totalTime = sprintf('%02d:%02d:%02d', $hours, $mins, $secs);
$html .= '<tr><td></td><td></td><td></td><td><b>Gesamt</b></td><td><b>' . $totalTime . '</b></td><td></td><td></td></tr>';
$html .= '</table>';
$pdf->writeHTML($html, true, false, true, false, '');
}
$pdf->Output('Arbeitszeiten_alle_' . $selectedYear . '_' . str_pad((string)$selectedMonth, 2, '0', STR_PAD_LEFT) . '.pdf', 'I');
} catch (PDOException $e) {
echo "Datenbankfehler: " . $e->getMessage();
}
?>
+2
View File
@@ -2,6 +2,7 @@
session_start(); session_start();
require_once('inc/config.inc.php'); require_once('inc/config.inc.php');
require_once('inc/functions.inc.php'); require_once('inc/functions.inc.php');
require_once(__DIR__ . '/../inc/company_holiday_sync.inc.php');
$user = check_user(); $user = check_user();
if (!is_admin_user()) { if (!is_admin_user()) {
@@ -18,6 +19,7 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['id'])) {
$id = intval($_POST['id']); $id = intval($_POST['id']);
vacationSyncDeleteUrlaubByCompanyHoliday($pdo, $id);
$stmt = $pdo->prepare("DELETE FROM company_holidays WHERE id = ?"); $stmt = $pdo->prepare("DELETE FROM company_holidays WHERE id = ?");
$stmt->execute([$id]); $stmt->execute([$id]);
+3 -7
View File
@@ -13,26 +13,22 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['id'])) {
$id = (int)$_POST['id']; $id = (int)$_POST['id'];
$referer = $_POST['referer'] ?? 'urlaubsantrag.php'; $referer = $_POST['referer'] ?? 'urlaubsantrag.php';
// Fetch vacation to verify ownership
$stmt = $pdo->prepare("SELECT user_id, status FROM vacations WHERE id = ?"); $stmt = $pdo->prepare("SELECT user_id, status FROM vacations WHERE id = ?");
$stmt->execute([$id]); $stmt->execute([$id]);
$vac = $stmt->fetch(); $vac = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$vac) { if (!$vac) {
die('Urlaubseintrag nicht gefunden.'); die('Urlaubseintrag nicht gefunden.');
} }
$isAdmin = is_admin_user(); $canManageTeamVacations = can_manage_team_vacations();
if (!$canManageTeamVacations && (int)$vac['user_id'] !== (int)$_SESSION['userid']) {
if (!$isAdmin && $vac['user_id'] != $_SESSION['userid']) {
die('Zugriff verweigert.'); die('Zugriff verweigert.');
} }
// Allow deletion for admins or owner
$del = $pdo->prepare("DELETE FROM vacations WHERE id = ?"); $del = $pdo->prepare("DELETE FROM vacations WHERE id = ?");
$del->execute([$id]); $del->execute([$id]);
header('Location: ' . $referer); header('Location: ' . $referer);
exit(); exit();
?> ?>
+15 -6
View File
@@ -41,19 +41,28 @@ if (!isset($user)) {
<a class="nav-link" href="fehlbuchungen.php">Fehlbuchungen</a> <a class="nav-link" href="fehlbuchungen.php">Fehlbuchungen</a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="../admin/index.php">Zur Admin-Oberfläche</a>
</li>
<li class="nav-item">
<a class="nav-link" href="../admin/zeiterfassung_hilfe.php">Hilfe</a>
</li>
<!-- Urlaub Dropdown --> <!-- Urlaub Dropdown -->
<li class="nav-item 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"> <a class="nav-link dropdown-toggle" href="#" id="urlaubDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Urlaub Abwesenheit
</a> </a>
<div class="dropdown-menu" aria-labelledby="urlaubDropdown"> <div class="dropdown-menu" aria-labelledby="urlaubDropdown">
<a class="dropdown-item" href="urlaubsantrag.php">Urlaubsantrag</a> <a class="dropdown-item" href="urlaubsantrag.php">Abwesenheitsantrag</a>
<a class="dropdown-item" href="my_vacations_calendar.php">Mein Urlaubskalender</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> <a class="dropdown-item" href="vacations_calendar_all.php">Team-Urlaubskalender</a>
<?php if (is_admin_user()) : ?> <?php if (is_admin_user()) : ?>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item" href="vacations_overview.php">Urlaubsübersicht</a> <a class="dropdown-item" href="admin_absence_calendar.php">Leitungskalender</a>
<a class="dropdown-item" href="approveVacation.php">Urlaubsanträge genehmigen</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> <a class="dropdown-item" href="company_holidays.php">Betriebsurlaub</a>
<?php endif; ?> <?php endif; ?>
</div> </div>
+9
View File
@@ -145,6 +145,15 @@ function is_admin_user() {
$statement->execute(array('id' => $_SESSION['userid'])); $statement->execute(array('id' => $_SESSION['userid']));
return ($statement->rowCount() == 1); return ($statement->rowCount() == 1);
} }
/**
* Returns true when the user may manage vacations for the team.
* In the current Zeiterfassung, team leads are represented by the
* existing admin role.
*/
function can_manage_team_vacations() {
return is_admin_user();
}
/** /**
* Prüft, ob der Benutzer Bearbeiter ist * Prüft, ob der Benutzer Bearbeiter ist
*/ */
@@ -0,0 +1,517 @@
<?php
use PHPMailer\PHPMailer\Exception;
use PHPMailer\PHPMailer\PHPMailer;
require_once __DIR__ . '/../../inc/PHPMailer/src/Exception.php';
require_once __DIR__ . '/../../inc/PHPMailer/src/PHPMailer.php';
require_once __DIR__ . '/../../inc/PHPMailer/src/SMTP.php';
if (!function_exists('timeErrorNotificationsEnsureTables')) {
function timeErrorNotificationsEnsureTables(PDO $pdo): void
{
$legacyColumnStmt = $pdo->prepare(
"SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'time_error_notifications'
AND COLUMN_NAME = 'error_date'"
);
$legacyColumnStmt->execute();
if ((int)$legacyColumnStmt->fetchColumn() > 0) {
$pdo->exec("DROP TABLE IF EXISTS time_error_notifications");
}
$pdo->exec(
"CREATE TABLE IF NOT EXISTS time_error_notification_state (
employee_id INT NOT NULL,
cycle_started_on DATE NOT NULL,
first_error_date DATE NOT NULL,
last_notification_stage VARCHAR(50) DEFAULT NULL,
last_notification_sent_at DATETIME DEFAULT NULL,
employee_day_1_sent_at DATETIME DEFAULT NULL,
employee_day_3_sent_at DATETIME DEFAULT NULL,
admin_day_7_sent_at DATETIME DEFAULT NULL,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (employee_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
);
$pdo->exec(
"CREATE TABLE IF NOT EXISTS time_error_notifications (
id INT NOT NULL AUTO_INCREMENT,
employee_id INT NOT NULL,
cycle_started_on DATE NOT NULL,
notification_stage VARCHAR(50) NOT NULL,
recipient_email VARCHAR(255) NOT NULL,
sent_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uniq_time_error_notification (employee_id, cycle_started_on, notification_stage, recipient_email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
);
}
}
if (!function_exists('timeErrorNotificationsSendMail')) {
function timeErrorNotificationsSendMail(PDO $pdo, string $recipient, string $subject, string $body): bool
{
$stmt = $pdo->prepare("SELECT * FROM config LIMIT 1");
$stmt->execute();
$config = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$config) {
return false;
}
$mail = new PHPMailer(true);
try {
$mail->SMTPDebug = 0;
$mail->isSMTP();
$mail->Host = (string)$config['mailserver'];
$mail->SMTPAuth = true;
$mail->Username = (string)$config['mailUsername'];
$mail->Password = (string)$config['mailPassword'];
if (!empty($config['mailSMTPSecure'])) {
$mail->SMTPSecure = (string)$config['mailSMTPSecure'];
}
$mail->Port = (int)$config['mailPort'];
$mail->CharSet = 'UTF-8';
$mail->setFrom((string)$config['mailFrom'], (string)$config['mailFromName']);
$mail->addAddress($recipient);
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $body;
$mail->AltBody = trim(html_entity_decode(strip_tags(str_replace(["<br>", "<br/>", "<br />"], "\n", $body)), ENT_QUOTES, 'UTF-8'));
$mail->send();
return true;
} catch (Exception $e) {
return false;
}
}
}
if (!function_exists('timeErrorNotificationsFetchInvalidEntries')) {
function timeErrorNotificationsFetchInvalidEntries(PDO $pdo): array
{
$stmt = $pdo->prepare(
"SELECT
u.id AS employee_id,
u.email,
u.vorname,
u.nachname,
DATE(t.timestamp_datetime) AS error_date,
GROUP_CONCAT(t.timestamp_type ORDER BY t.timestamp_datetime) AS day_sequence
FROM users u
INNER JOIN timestamps t ON t.employee_id = u.id
WHERE u.zeiterfassung = '1'
AND DATE(t.timestamp_datetime) < CURDATE()
GROUP BY u.id, u.email, u.vorname, u.nachname, DATE(t.timestamp_datetime)
ORDER BY error_date ASC, u.nachname ASC, u.vorname ASC"
);
$stmt->execute();
$entries = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
if (!isValidSequence((string)$row['day_sequence'])) {
$entries[] = $row;
}
}
return $entries;
}
}
if (!function_exists('timeErrorNotificationsGroupEntriesByEmployee')) {
function timeErrorNotificationsGroupEntriesByEmployee(array $entries): array
{
$grouped = [];
foreach ($entries as $entry) {
$employeeId = (int)$entry['employee_id'];
if (!isset($grouped[$employeeId])) {
$grouped[$employeeId] = [
'employee_id' => $employeeId,
'email' => (string)$entry['email'],
'vorname' => (string)$entry['vorname'],
'nachname' => (string)$entry['nachname'],
'first_error_date' => (string)$entry['error_date'],
'error_dates' => [],
];
}
$grouped[$employeeId]['error_dates'][] = (string)$entry['error_date'];
if ((string)$entry['error_date'] < $grouped[$employeeId]['first_error_date']) {
$grouped[$employeeId]['first_error_date'] = (string)$entry['error_date'];
}
}
foreach ($grouped as &$employee) {
$employee['error_dates'] = array_values(array_unique($employee['error_dates']));
sort($employee['error_dates']);
}
unset($employee);
return $grouped;
}
}
if (!function_exists('timeErrorNotificationsFetchAdminRecipients')) {
function timeErrorNotificationsFetchAdminRecipients(PDO $pdo): array
{
$stmt = $pdo->prepare(
"SELECT DISTINCT
u.id,
u.email,
u.vorname,
u.nachname
FROM users u
LEFT JOIN users_admin ua ON ua.userid = u.id
WHERE (u.admin = 1 OR ua.userid IS NOT NULL)
AND u.email IS NOT NULL
AND u.email <> ''
ORDER BY u.nachname ASC, u.vorname ASC"
);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
if (!function_exists('timeErrorNotificationsFetchStateByEmployee')) {
function timeErrorNotificationsFetchStateByEmployee(PDO $pdo): array
{
timeErrorNotificationsEnsureTables($pdo);
$stmt = $pdo->prepare("SELECT * FROM time_error_notification_state");
$stmt->execute();
$states = [];
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
$states[(int)$row['employee_id']] = $row;
}
return $states;
}
}
if (!function_exists('timeErrorNotificationsUpsertState')) {
function timeErrorNotificationsUpsertState(PDO $pdo, array $employee, ?array $existingState): array
{
timeErrorNotificationsEnsureTables($pdo);
$cycleStartedOn = $existingState['cycle_started_on'] ?? $employee['first_error_date'];
$stmt = $pdo->prepare(
"INSERT INTO time_error_notification_state (
employee_id,
cycle_started_on,
first_error_date,
last_notification_stage,
last_notification_sent_at,
employee_day_1_sent_at,
employee_day_3_sent_at,
admin_day_7_sent_at
) VALUES (
:employee_id,
:cycle_started_on,
:first_error_date,
:last_notification_stage,
:last_notification_sent_at,
:employee_day_1_sent_at,
:employee_day_3_sent_at,
:admin_day_7_sent_at
)
ON DUPLICATE KEY UPDATE
cycle_started_on = VALUES(cycle_started_on),
first_error_date = VALUES(first_error_date),
last_notification_stage = VALUES(last_notification_stage),
last_notification_sent_at = VALUES(last_notification_sent_at),
employee_day_1_sent_at = VALUES(employee_day_1_sent_at),
employee_day_3_sent_at = VALUES(employee_day_3_sent_at),
admin_day_7_sent_at = VALUES(admin_day_7_sent_at)"
);
$stmt->execute([
'employee_id' => $employee['employee_id'],
'cycle_started_on' => $cycleStartedOn,
'first_error_date' => $employee['first_error_date'],
'last_notification_stage' => $existingState['last_notification_stage'] ?? null,
'last_notification_sent_at' => $existingState['last_notification_sent_at'] ?? null,
'employee_day_1_sent_at' => $existingState['employee_day_1_sent_at'] ?? null,
'employee_day_3_sent_at' => $existingState['employee_day_3_sent_at'] ?? null,
'admin_day_7_sent_at' => $existingState['admin_day_7_sent_at'] ?? null,
]);
return [
'employee_id' => $employee['employee_id'],
'cycle_started_on' => $cycleStartedOn,
'first_error_date' => $employee['first_error_date'],
'last_notification_stage' => $existingState['last_notification_stage'] ?? null,
'last_notification_sent_at' => $existingState['last_notification_sent_at'] ?? null,
'employee_day_1_sent_at' => $existingState['employee_day_1_sent_at'] ?? null,
'employee_day_3_sent_at' => $existingState['employee_day_3_sent_at'] ?? null,
'admin_day_7_sent_at' => $existingState['admin_day_7_sent_at'] ?? null,
];
}
}
if (!function_exists('timeErrorNotificationsClearResolvedStates')) {
function timeErrorNotificationsClearResolvedStates(PDO $pdo, array $openEmployeeIds): int
{
timeErrorNotificationsEnsureTables($pdo);
if (empty($openEmployeeIds)) {
$stmt = $pdo->prepare("DELETE FROM time_error_notification_state");
$stmt->execute();
return $stmt->rowCount();
}
$placeholders = implode(',', array_fill(0, count($openEmployeeIds), '?'));
$stmt = $pdo->prepare("DELETE FROM time_error_notification_state WHERE employee_id NOT IN ($placeholders)");
$stmt->execute(array_values($openEmployeeIds));
return $stmt->rowCount();
}
}
if (!function_exists('timeErrorNotificationsMarkStageSent')) {
function timeErrorNotificationsMarkStageSent(PDO $pdo, array $state, string $stage, string $recipientEmail): array
{
timeErrorNotificationsEnsureTables($pdo);
$stmt = $pdo->prepare(
"INSERT INTO time_error_notifications (employee_id, cycle_started_on, notification_stage, recipient_email)
VALUES (:employee_id, :cycle_started_on, :notification_stage, :recipient_email)
ON DUPLICATE KEY UPDATE sent_at = sent_at"
);
$stmt->execute([
'employee_id' => $state['employee_id'],
'cycle_started_on' => $state['cycle_started_on'],
'notification_stage' => $stage,
'recipient_email' => $recipientEmail,
]);
$now = (new DateTimeImmutable('now'))->format('Y-m-d H:i:s');
$state['last_notification_stage'] = $stage;
$state['last_notification_sent_at'] = $now;
if ($stage === 'employee_day_1') {
$state['employee_day_1_sent_at'] = $now;
} elseif ($stage === 'employee_day_3') {
$state['employee_day_3_sent_at'] = $now;
} elseif ($stage === 'admin_day_7') {
$state['admin_day_7_sent_at'] = $now;
}
$stmt = $pdo->prepare(
"UPDATE time_error_notification_state
SET last_notification_stage = :last_notification_stage,
last_notification_sent_at = :last_notification_sent_at,
employee_day_1_sent_at = :employee_day_1_sent_at,
employee_day_3_sent_at = :employee_day_3_sent_at,
admin_day_7_sent_at = :admin_day_7_sent_at
WHERE employee_id = :employee_id"
);
$stmt->execute([
'last_notification_stage' => $state['last_notification_stage'],
'last_notification_sent_at' => $state['last_notification_sent_at'],
'employee_day_1_sent_at' => $state['employee_day_1_sent_at'],
'employee_day_3_sent_at' => $state['employee_day_3_sent_at'],
'admin_day_7_sent_at' => $state['admin_day_7_sent_at'],
'employee_id' => $state['employee_id'],
]);
return $state;
}
}
if (!function_exists('timeErrorNotificationsDaysSinceDate')) {
function timeErrorNotificationsDaysSinceDate(string $date, ?DateTimeImmutable $today = null): int
{
$today = $today ?: new DateTimeImmutable('today');
$reference = new DateTimeImmutable(substr($date, 0, 10));
return (int)$reference->diff($today)->days;
}
}
if (!function_exists('timeErrorNotificationsFormatDateList')) {
function timeErrorNotificationsFormatDateList(array $errorDates): string
{
$labels = array_map(static function (string $date): string {
return date('d.m.Y', strtotime($date));
}, $errorDates);
return implode(', ', $labels);
}
}
if (!function_exists('timeErrorNotificationsBuildEmployeeMail')) {
function timeErrorNotificationsGetTrackingUrl(): string
{
if (!empty($_SERVER['HTTP_HOST'])) {
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';
return $protocol . $_SERVER['HTTP_HOST'] . '/zeiterfassung/';
}
return 'https://www.praxis-creutzburg.de/zeiterfassung/';
}
}
if (!function_exists('timeErrorNotificationsBuildEmployeeMail')) {
function timeErrorNotificationsBuildEmployeeMail(array $employee, string $stage): array
{
$name = trim($employee['vorname'] . ' ' . $employee['nachname']);
$dateList = timeErrorNotificationsFormatDateList($employee['error_dates']);
$trackingUrl = timeErrorNotificationsGetTrackingUrl();
$subject = ($stage === 'employee_day_3')
? 'Erneute Erinnerung zu offenen Zeitfehlern'
: 'Erinnerung zu offenen Zeitfehlern';
$body = '<p>Hallo ' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . ',</p>'
. '<p>in deiner Zeiterfassung gibt es weiterhin offene Buchungsfehler.</p>'
. '<p><strong>Betroffene Tage:</strong> ' . htmlspecialchars($dateList, ENT_QUOTES, 'UTF-8') . '</p>'
. '<p>Bitte korrigiere die Einträge in der Zeiterfassung. Solange die Fehler offen bleiben, wird der Vorgang weiter verfolgt.</p>'
. '<p><a href="' . htmlspecialchars($trackingUrl, ENT_QUOTES, 'UTF-8') . '">Zur Zeiterfassung</a></p>';
return [
'subject' => $subject,
'body' => $body,
];
}
}
if (!function_exists('timeErrorNotificationsBuildAdminMail')) {
function timeErrorNotificationsBuildAdminMail(array $employee): array
{
$employeeName = trim($employee['vorname'] . ' ' . $employee['nachname']);
$dateList = timeErrorNotificationsFormatDateList($employee['error_dates']);
$trackingUrl = timeErrorNotificationsGetTrackingUrl();
$subject = 'Eskalation: Offene Zeitfehler von ' . $employeeName;
$body = '<p>Bei einem Mitarbeiter bestehen weiterhin offene Zeitfehler.</p>'
. '<p><strong>Mitarbeiter:</strong> ' . htmlspecialchars($employeeName, ENT_QUOTES, 'UTF-8') . '<br>'
. '<strong>E-Mail:</strong> ' . htmlspecialchars($employee['email'], ENT_QUOTES, 'UTF-8') . '<br>'
. '<strong>Betroffene Tage:</strong> ' . htmlspecialchars($dateList, ENT_QUOTES, 'UTF-8') . '</p>'
. '<p>Bitte prüfen Sie die Zeiterfassung und stimmen Sie die Korrektur mit dem Mitarbeiter ab.</p>'
. '<p><a href="' . htmlspecialchars($trackingUrl, ENT_QUOTES, 'UTF-8') . '">Zur Zeiterfassung</a></p>';
return [
'subject' => $subject,
'body' => $body,
];
}
}
if (!function_exists('timeErrorNotificationsProcess')) {
function timeErrorNotificationsProcess(PDO $pdo, ?DateTimeImmutable $today = null): array
{
$today = $today ?: new DateTimeImmutable('today');
timeErrorNotificationsEnsureTables($pdo);
$results = [
'processed_errors' => 0,
'affected_employees' => 0,
'employee_day_1_sent' => 0,
'employee_day_3_sent' => 0,
'admin_day_7_sent' => 0,
'resolved_states_cleared' => 0,
'skipped_missing_employee_email' => 0,
'skipped_missing_admin_email' => 0,
'failed' => [],
];
$entries = timeErrorNotificationsFetchInvalidEntries($pdo);
$employees = timeErrorNotificationsGroupEntriesByEmployee($entries);
$adminRecipients = timeErrorNotificationsFetchAdminRecipients($pdo);
$states = timeErrorNotificationsFetchStateByEmployee($pdo);
$results['processed_errors'] = count($entries);
$results['affected_employees'] = count($employees);
$results['resolved_states_cleared'] = timeErrorNotificationsClearResolvedStates($pdo, array_keys($employees));
foreach ($employees as $employeeId => $employee) {
$state = timeErrorNotificationsUpsertState($pdo, $employee, $states[$employeeId] ?? null);
$daysSinceCycleStart = timeErrorNotificationsDaysSinceDate($state['cycle_started_on'], $today);
$employeeEmail = trim($employee['email']);
if ($state['employee_day_1_sent_at'] === null && $daysSinceCycleStart >= 1) {
if ($employeeEmail === '') {
$results['skipped_missing_employee_email']++;
continue;
}
$mail = timeErrorNotificationsBuildEmployeeMail($employee, 'employee_day_1');
if (timeErrorNotificationsSendMail($pdo, $employeeEmail, $mail['subject'], $mail['body'])) {
$state = timeErrorNotificationsMarkStageSent($pdo, $state, 'employee_day_1', $employeeEmail);
$results['employee_day_1_sent']++;
} else {
$results['failed'][] = [
'stage' => 'employee_day_1',
'employee_id' => $employeeId,
'recipient_email' => $employeeEmail,
];
}
continue;
}
if ($state['employee_day_3_sent_at'] === null && $daysSinceCycleStart >= 3) {
if ($employeeEmail === '') {
$results['skipped_missing_employee_email']++;
continue;
}
$mail = timeErrorNotificationsBuildEmployeeMail($employee, 'employee_day_3');
if (timeErrorNotificationsSendMail($pdo, $employeeEmail, $mail['subject'], $mail['body'])) {
$state = timeErrorNotificationsMarkStageSent($pdo, $state, 'employee_day_3', $employeeEmail);
$results['employee_day_3_sent']++;
} else {
$results['failed'][] = [
'stage' => 'employee_day_3',
'employee_id' => $employeeId,
'recipient_email' => $employeeEmail,
];
}
continue;
}
if ($state['admin_day_7_sent_at'] !== null || empty($state['last_notification_sent_at'])) {
continue;
}
$daysSinceLastNotification = timeErrorNotificationsDaysSinceDate($state['last_notification_sent_at'], $today);
if ($daysSinceLastNotification < 7) {
continue;
}
if (empty($adminRecipients)) {
$results['skipped_missing_admin_email']++;
continue;
}
foreach ($adminRecipients as $adminRecipient) {
$adminEmail = trim((string)$adminRecipient['email']);
if ($adminEmail === '') {
$results['skipped_missing_admin_email']++;
continue;
}
$mail = timeErrorNotificationsBuildAdminMail($employee);
if (timeErrorNotificationsSendMail($pdo, $adminEmail, $mail['subject'], $mail['body'])) {
$state = timeErrorNotificationsMarkStageSent($pdo, $state, 'admin_day_7', $adminEmail);
$results['admin_day_7_sent']++;
} else {
$results['failed'][] = [
'stage' => 'admin_day_7',
'employee_id' => $employeeId,
'recipient_email' => $adminEmail,
];
}
}
}
return $results;
}
}
+102
View File
@@ -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) = ''
");
}
}
+43 -12
View File
@@ -4,21 +4,33 @@ require_once('inc/config.inc.php');
require_once('inc/functions.inc.php'); require_once('inc/functions.inc.php');
$user = check_user(); $user = check_user();
// personal calendar available to any logged-in user
include 'header.php'; include 'header.php';
?> ?>
<div class="container"> <div class="container">
<h2>Mein Urlaubskalender</h2> <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> <div id="calendar"></div>
<br> <br>
<div> <div class="mb-3">
<span class="badge badge-success">genehmigt</span> <strong>Abwesenheitsarten:</strong><br>
<span class="badge badge-warning">beantragt</span> <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> <span class="badge badge-primary">Betriebsurlaub</span>
</div> </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;"> <div id="eventDetails" style="display:none;">
<h4>Details</h4> <h4>Details</h4>
<div id="detailsContent"></div> <div id="detailsContent"></div>
@@ -31,24 +43,43 @@ include 'header.php';
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar'); 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, { var calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth', initialView: 'dayGridMonth',
firstDay: 1, firstDay: 1,
height: 650, height: 650,
events: function(info, successCallback, failureCallback) { events: function(info, successCallback, failureCallback) {
var url = 'api/vacations.php?start=' + info.startStr + '&end=' + info.endStr + '&only_personal=1'; 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); }); fetch(url)
.then(function(res){ return res.json(); })
.then(function(data){ successCallback(data); })
.catch(function(err){ failureCallback(err); });
}, },
eventClick: function(info) { eventClick: function(info) {
var ev = info.event; var ev = info.event;
var props = ev.extendedProps; var props = ev.extendedProps || {};
var html = '<strong>' + ev.title + '</strong><br>' + ev.start.toLocaleDateString() + ' - ' + (new Date(ev.end).toLocaleDateString()) + '<br>'; var html = '<strong>' + ev.title + '</strong><br>' +
formatAllDayRange(ev.start, ev.end) + '<br>';
if (props.type === 'user') { if (props.type === 'user') {
html += 'Status: ' + (props.status || '') + '<br>'; html += 'Abwesenheitsgrund: ' + (props.absence_label || props.absence_type || '') + '<br>';
html += 'Kommentar: ' + (props.comment || '') + '<br>'; html += 'Status: ' + (props.status_label || props.status || '') + '<br>';
if (props.comment) {
html += 'Kommentar: ' + props.comment + '<br>';
}
} else if (props.type === 'company') { } else if (props.type === 'company') {
html += 'Beschreibung: ' + (props.description || '') + '<br>'; html += 'Beschreibung: ' + (props.description || '') + '<br>';
} }
document.getElementById('detailsContent').innerHTML = html; document.getElementById('detailsContent').innerHTML = html;
document.getElementById('eventDetails').style.display = 'block'; document.getElementById('eventDetails').style.display = 'block';
} }
@@ -0,0 +1,54 @@
<?php
session_start();
require_once __DIR__ . '/inc/config.inc.php';
require_once __DIR__ . '/inc/functions.inc.php';
require_once __DIR__ . '/inc/time_error_notifications.inc.php';
$isCli = PHP_SAPI === 'cli';
if (!$isCli) {
$user = check_user();
if (!$user || !is_admin_user()) {
http_response_code(403);
exit('Keine Berechtigung.');
}
}
$result = timeErrorNotificationsProcess($pdo);
if ($isCli) {
echo "Zeiterfassungs-Benachrichtigungen ausgefuehrt\n";
echo "Fehlerhafte Tage geprueft: " . $result['processed_errors'] . "\n";
echo "Betroffene Mitarbeiter: " . $result['affected_employees'] . "\n";
echo "Mitarbeiter-Erinnerungen Tag 1: " . $result['employee_day_1_sent'] . "\n";
echo "Mitarbeiter-Erinnerungen Tag 3: " . $result['employee_day_3_sent'] . "\n";
echo "Admin-Benachrichtigungen Tag 7: " . $result['admin_day_7_sent'] . "\n";
echo "Bereinigte geloeste Status: " . $result['resolved_states_cleared'] . "\n";
echo "Ohne Mitarbeiter-E-Mail uebersprungen: " . $result['skipped_missing_employee_email'] . "\n";
echo "Ohne Admin-E-Mail uebersprungen: " . $result['skipped_missing_admin_email'] . "\n";
echo "Fehlgeschlagene Sendungen: " . count($result['failed']) . "\n";
exit(0);
}
include __DIR__ . '/header.php';
?>
<div class="container">
<div class="row">
<div class="col-md-12">
<h2>Zeiterfassungs-Benachrichtigungen</h2>
<div class="alert alert-info" role="alert">
<strong>Gepruefte fehlerhafte Tage:</strong> <?php echo (int)$result['processed_errors']; ?><br>
<strong>Betroffene Mitarbeiter:</strong> <?php echo (int)$result['affected_employees']; ?><br>
<strong>Mitarbeiter-Erinnerungen Tag 1:</strong> <?php echo (int)$result['employee_day_1_sent']; ?><br>
<strong>Mitarbeiter-Erinnerungen Tag 3:</strong> <?php echo (int)$result['employee_day_3_sent']; ?><br>
<strong>Admin-Benachrichtigungen Tag 7:</strong> <?php echo (int)$result['admin_day_7_sent']; ?><br>
<strong>Bereinigte geloeste Status:</strong> <?php echo (int)$result['resolved_states_cleared']; ?><br>
<strong>Ohne Mitarbeiter-E-Mail uebersprungen:</strong> <?php echo (int)$result['skipped_missing_employee_email']; ?><br>
<strong>Ohne Admin-E-Mail uebersprungen:</strong> <?php echo (int)$result['skipped_missing_admin_email']; ?><br>
<strong>Fehlgeschlagene Sendungen:</strong> <?php echo count($result['failed']); ?>
</div>
</div>
</div>
</div>
<?php include __DIR__ . '/footer.php'; ?>
+13 -9
View File
@@ -2,8 +2,10 @@
session_start(); session_start();
require_once("inc/config.inc.php"); require_once("inc/config.inc.php");
require_once("inc/functions.inc.php"); require_once("inc/functions.inc.php");
require_once("inc/vacation_absence.inc.php");
$user = check_user(); $user = check_user();
vacationAbsenceEnsureSchema($pdo);
if (!isset($_SESSION['userid'])) { if (!isset($_SESSION['userid'])) {
die("Kein Benutzer angemeldet."); die("Kein Benutzer angemeldet.");
@@ -21,7 +23,7 @@ if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST['vacation_id'])) {
$vacation_id = (int)$_POST['vacation_id']; $vacation_id = (int)$_POST['vacation_id'];
$action = $_POST['action']; $action = $_POST['action'];
$comment_admin = trim($_POST['comment_admin']); $comment_admin = trim((string)($_POST['comment_admin'] ?? ''));
if ($action == "genehmigen") { if ($action == "genehmigen") {
$status = "genehmigt"; $status = "genehmigt";
@@ -61,10 +63,10 @@ $antraege = $stmt->fetchAll();
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<h2>Urlaubsanträge genehmigen</h2> <h2>Abwesenheitsanträge genehmigen</h2>
<?php if ($message): ?> <?php if ($message): ?>
<div class="alert alert-success"><?php echo $message; ?></div> <div class="alert alert-success"><?php echo htmlspecialchars($message); ?></div>
<?php endif; ?> <?php endif; ?>
<?php if (count($antraege) == 0): ?> <?php if (count($antraege) == 0): ?>
@@ -74,6 +76,7 @@ $antraege = $stmt->fetchAll();
<table class="table table-bordered table-striped"> <table class="table table-bordered table-striped">
<tr> <tr>
<th>Mitarbeiter</th> <th>Mitarbeiter</th>
<th>Grund</th>
<th>Von</th> <th>Von</th>
<th>Bis</th> <th>Bis</th>
<th>Tage</th> <th>Tage</th>
@@ -84,14 +87,15 @@ $antraege = $stmt->fetchAll();
<?php foreach ($antraege as $a): ?> <?php foreach ($antraege as $a): ?>
<tr> <tr>
<td><?php echo htmlspecialchars($a['vorname'] . " " . $a['nachname']); ?></td> <td><?php echo htmlspecialchars($a['vorname'] . " " . $a['nachname']); ?></td>
<td><?php echo $a['start_date']; ?></td> <td><?php echo htmlspecialchars(vacationAbsenceReasonLabel($a['absence_reason'] ?? 'urlaub')); ?></td>
<td><?php echo $a['end_date']; ?></td> <td><?php echo htmlspecialchars((string)$a['start_date']); ?></td>
<td><?php echo $a['days']; ?></td> <td><?php echo htmlspecialchars((string)$a['end_date']); ?></td>
<td><?php echo htmlspecialchars($a['comment_user']); ?></td> <td><?php echo (int)$a['days']; ?></td>
<td><?php echo htmlspecialchars((string)($a['comment_user'] ?? '')); ?></td>
<td> <td>
<form method="post" style="display:inline;"> <form method="post" style="display:inline;">
<input type="hidden" name="vacation_id" value="<?php echo $a['id']; ?>"> <input type="hidden" name="vacation_id" value="<?php echo (int)$a['id']; ?>">
<input type="hidden" name="action" value="genehmigen"> <input type="hidden" name="action" value="genehmigen">
<button type="submit" class="btn btn-success btn-sm"> <button type="submit" class="btn btn-success btn-sm">
Genehmigen Genehmigen
@@ -99,7 +103,7 @@ $antraege = $stmt->fetchAll();
</form> </form>
<form method="post" style="display:inline;"> <form method="post" style="display:inline;">
<input type="hidden" name="vacation_id" value="<?php echo $a['id']; ?>"> <input type="hidden" name="vacation_id" value="<?php echo (int)$a['id']; ?>">
<input type="hidden" name="action" value="ablehnen"> <input type="hidden" name="action" value="ablehnen">
<button type="submit" class="btn btn-danger btn-sm"> <button type="submit" class="btn btn-danger btn-sm">
Ablehnen Ablehnen
+136 -47
View File
@@ -2,16 +2,36 @@
session_start(); session_start();
require_once("inc/config.inc.php"); require_once("inc/config.inc.php");
require_once("inc/functions.inc.php"); require_once("inc/functions.inc.php");
require_once("inc/vacation_absence.inc.php");
$user = check_user(); $user = check_user();
vacationAbsenceEnsureSchema($pdo);
if (!isset($_SESSION['userid'])) { if (!isset($_SESSION['userid'])) {
die("Kein Benutzer angemeldet."); die("Kein Benutzer angemeldet.");
} }
$user_id = $_SESSION['userid']; $user_id = (int)$_SESSION['userid'];
$canManageTeamVacations = can_manage_team_vacations();
$message = ""; $message = "";
$error = ""; $error = "";
$selected_user_id = $user_id;
$selected_absence_reason = vacationAbsenceDefaultReason();
$start_date = '';
$end_date = '';
$comment = '';
$selectableUsers = [];
if ($canManageTeamVacations) {
$stmtUsers = $pdo->prepare("
SELECT id, vorname, nachname, email
FROM users
WHERE zeiterfassung = 1
ORDER BY nachname, vorname
");
$stmtUsers->execute();
$selectableUsers = $stmtUsers->fetchAll(PDO::FETCH_ASSOC);
}
function calculateWorkingDays($start, $end) { function calculateWorkingDays($start, $end) {
$start = new DateTime($start); $start = new DateTime($start);
@@ -22,9 +42,8 @@ function calculateWorkingDays($start, $end) {
$period = new DatePeriod($start, $interval, $end); $period = new DatePeriod($start, $interval, $end);
$workingDays = 0; $workingDays = 0;
foreach ($period as $day) { foreach ($period as $day) {
if ($day->format('N') < 6) { // 1 (Mo) - 5 (Fr) if ($day->format('N') < 6) {
$workingDays++; $workingDays++;
} }
} }
@@ -32,23 +51,42 @@ function calculateWorkingDays($start, $end) {
return $workingDays; return $workingDays;
} }
if ($_SERVER["REQUEST_METHOD"] == "POST") { 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;
$start_date = $_POST['start_date']; $selectedUser = null;
$end_date = $_POST['end_date']; if ($selected_user_id <= 0) {
$comment = trim($_POST['comment']); $error = "Bitte einen Mitarbeiter auswaehlen.";
if (empty($start_date) || empty($end_date)) {
$error = "Bitte beide Datumsfelder ausfüllen.";
} elseif ($start_date > $end_date) {
$error = "Enddatum liegt vor dem Startdatum.";
} elseif ($start_date < date("Y-m-d")) {
$error = "Urlaub kann nicht in der Vergangenheit beantragt werden.";
} else { } else {
$stmtSelectedUser = $pdo->prepare("
SELECT id, vorname, nachname
FROM users
WHERE id = ?
AND zeiterfassung = 1
LIMIT 1
");
$stmtSelectedUser->execute([$selected_user_id]);
$selectedUser = $stmtSelectedUser->fetch(PDO::FETCH_ASSOC);
// Überschneidung prüfen if (!$selectedUser) {
$error = "Der ausgewaehlte Mitarbeiter wurde nicht gefunden.";
}
}
if ($error === "" && ($start_date === '' || $end_date === '')) {
$error = "Bitte beide Datumsfelder ausfuellen.";
} elseif ($error === "" && $start_date > $end_date) {
$error = "Enddatum liegt vor dem Startdatum.";
} 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(" $stmt = $pdo->prepare("
SELECT COUNT(*) FROM vacations SELECT COUNT(*)
FROM vacations
WHERE user_id = ? WHERE user_id = ?
AND status != 'abgelehnt' AND status != 'abgelehnt'
AND ( AND (
@@ -57,23 +95,30 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
OR (? BETWEEN start_date AND end_date) OR (? BETWEEN start_date AND end_date)
) )
"); ");
$stmt->execute([$user_id, $start_date, $end_date, $start_date, $end_date, $start_date]); $stmt->execute([$selected_user_id, $start_date, $end_date, $start_date, $end_date, $start_date]);
$exists = $stmt->fetchColumn(); $exists = (int)$stmt->fetchColumn();
if ($exists > 0) { if ($exists > 0) {
$error = "Der Zeitraum überschneidet sich mit einem bestehenden Antrag."; $error = "Der Zeitraum ueberschneidet sich mit einem bestehenden Antrag.";
} else { } else {
$days = calculateWorkingDays($start_date, $end_date); $days = calculateWorkingDays($start_date, $end_date);
$insert = $pdo->prepare(" $insert = $pdo->prepare("
INSERT INTO vacations (user_id, start_date, end_date, days, comment_user) INSERT INTO vacations (user_id, start_date, end_date, days, comment_user, absence_reason)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
"); ");
$insert->execute([$selected_user_id, $start_date, $end_date, $days, $comment, $selected_absence_reason]);
$insert->execute([$user_id, $start_date, $end_date, $days, $comment]); $reasonLabel = vacationAbsenceReasonLabel($selected_absence_reason);
if ($selected_user_id !== $user_id && $selectedUser) {
$message = "Abwesenheit fuer " . $selectedUser['vorname'] . " " . $selectedUser['nachname'] . " erfolgreich eingetragen ($days Werktage, Grund: " . $reasonLabel . ").";
} else {
if ($selected_absence_reason === 'urlaub') {
$message = "Urlaubsantrag erfolgreich eingereicht ($days Werktage)."; $message = "Urlaubsantrag erfolgreich eingereicht ($days Werktage).";
} else {
$message = "Abwesenheitsantrag erfolgreich eingereicht ($days Werktage, Grund: " . $reasonLabel . ").";
}
}
} }
} }
} }
@@ -85,57 +130,97 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
<div class="row"> <div class="row">
<div class="col-md-8 offset-md-2"> <div class="col-md-8 offset-md-2">
<h2>Urlaubsantrag</h2> <h2>Abwesenheitsantrag</h2>
<?php if ($error): ?> <?php if ($error): ?>
<div class="alert alert-danger"><?php echo $error; ?></div> <div class="alert alert-danger"><?php echo htmlspecialchars($error); ?></div>
<?php endif; ?> <?php endif; ?>
<?php if ($message): ?> <?php if ($message): ?>
<div class="alert alert-success"><?php echo $message; ?></div> <div class="alert alert-success"><?php echo htmlspecialchars($message); ?></div>
<?php endif; ?> <?php endif; ?>
<form method="post"> <form method="post">
<?php if ($canManageTeamVacations): ?>
<div class="form-group">
<label>Mitarbeiter:</label>
<select name="user_id" class="form-control" required>
<?php foreach ($selectableUsers as $employee): ?>
<?php $employeeId = (int)$employee['id']; ?>
<option value="<?php echo $employeeId; ?>" <?php echo ($selected_user_id === $employeeId) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars(trim($employee['nachname'] . ', ' . $employee['vorname'] . ' | ' . $employee['email'])); ?>
</option>
<?php endforeach; ?>
</select>
</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"> <div class="form-group">
<label>Von:</label> <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>
<div class="form-group"> <div class="form-group">
<label>Bis:</label> <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>
<div class="form-group"> <div class="form-group">
<label>Kommentar (optional):</label> <label>Kommentar (optional):</label>
<textarea name="comment" class="form-control"></textarea> <textarea name="comment" class="form-control"><?php echo htmlspecialchars($comment); ?></textarea>
</div> </div>
<br> <br>
<button type="submit" class="btn btn-primary btn-block"> <button type="submit" class="btn btn-primary btn-block">
Urlaub beantragen <?php echo $canManageTeamVacations ? 'Abwesenheit eintragen' : 'Abwesenheit beantragen'; ?>
</button> </button>
</form> </form>
<hr> <hr>
<h4>Meine Anträge</h4> <h4><?php echo $canManageTeamVacations ? 'Abwesenheitseintraege' : 'Meine Antraege'; ?></h4>
<?php <?php
$stmt = $pdo->prepare(" $listSql = "
SELECT * FROM vacations SELECT v.*, u.vorname, u.nachname
WHERE user_id = ? FROM vacations v
ORDER BY created_at DESC JOIN users u ON u.id = v.user_id
"); ";
$stmt->execute([$user_id]);
$antraege = $stmt->fetchAll(); if ($canManageTeamVacations) {
$listSql .= " ORDER BY v.created_at DESC";
$stmt = $pdo->prepare($listSql);
$stmt->execute();
} else {
$listSql .= " WHERE v.user_id = ? ORDER BY v.created_at DESC";
$stmt = $pdo->prepare($listSql);
$stmt->execute([$user_id]);
}
$antraege = $stmt->fetchAll(PDO::FETCH_ASSOC);
?> ?>
<table class="table table-bordered"> <table class="table table-bordered">
<tr> <tr>
<?php if ($canManageTeamVacations): ?>
<th>Mitarbeiter</th>
<?php endif; ?>
<th>Grund</th>
<th>Von</th> <th>Von</th>
<th>Bis</th> <th>Bis</th>
<th>Tage</th> <th>Tage</th>
@@ -145,14 +230,18 @@ $antraege = $stmt->fetchAll();
<?php foreach ($antraege as $a): ?> <?php foreach ($antraege as $a): ?>
<tr> <tr>
<td><?php echo $a['start_date']; ?></td> <?php if ($canManageTeamVacations): ?>
<td><?php echo $a['end_date']; ?></td> <td><?php echo htmlspecialchars(trim($a['vorname'] . ' ' . $a['nachname'])); ?></td>
<td><?php echo $a['days']; ?></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>
<td> <td>
<?php <?php
if ($a['status'] == 'beantragt') { if ($a['status'] === 'beantragt' || $a['status'] === null || $a['status'] === '') {
echo '<span class="badge badge-warning">Beantragt</span>'; echo '<span class="badge badge-warning">Beantragt</span>';
} elseif ($a['status'] == 'genehmigt') { } elseif ($a['status'] === 'genehmigt') {
echo '<span class="badge badge-success">Genehmigt</span>'; echo '<span class="badge badge-success">Genehmigt</span>';
} else { } else {
echo '<span class="badge badge-danger">Abgelehnt</span>'; echo '<span class="badge badge-danger">Abgelehnt</span>';
@@ -160,10 +249,10 @@ $antraege = $stmt->fetchAll();
?> ?>
</td> </td>
<td> <td>
<form method="post" action="deleteVacation.php" onsubmit="return confirm('Wirklich löschen?');"> <form method="post" action="deleteVacation.php" onsubmit="return confirm('Wirklich loeschen?');">
<input type="hidden" name="id" value="<?php echo $a['id']; ?>"> <input type="hidden" name="id" value="<?php echo (int)$a['id']; ?>">
<input type="hidden" name="referer" value="urlaubsantrag.php"> <input type="hidden" name="referer" value="urlaubsantrag.php">
<button type="submit" class="btn btn-sm btn-danger">Löschen</button> <button type="submit" class="btn btn-sm btn-danger">Loeschen</button>
</form> </form>
</td> </td>
</tr> </tr>
+37 -10
View File
@@ -13,14 +13,21 @@ include 'header.php';
<div class="container"> <div class="container">
<h2>Team Urlaubskalender</h2> <h2>Team Urlaubskalender</h2>
<p>Admin-Sicht auf den Teamkalender. Angezeigt werden nur Urlaub von Personen sowie Betriebsurlaub.</p>
<div id="calendar"></div> <div id="calendar"></div>
<br> <br>
<div> <div class="mb-3">
<span class="badge badge-success">genehmigt</span> <strong>Anzeige:</strong><br>
<span class="badge badge-warning">beantragt</span> <span class="badge badge-success">Urlaub</span>
<span class="badge badge-primary">Betriebsurlaub</span> <span class="badge badge-primary">Betriebsurlaub</span>
</div> </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;"> <div id="eventDetails" style="display:none;">
<h4>Details</h4> <h4>Details</h4>
<div id="detailsContent"></div> <div id="detailsContent"></div>
@@ -33,24 +40,44 @@ include 'header.php';
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar'); 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, { var calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth', initialView: 'dayGridMonth',
firstDay: 1, firstDay: 1,
height: 650, height: 650,
events: function(info, successCallback, failureCallback) { events: function(info, successCallback, failureCallback) {
var url = 'api/vacations.php?start=' + info.startStr + '&end=' + info.endStr; 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); }); fetch(url)
.then(function(res){ return res.json(); })
.then(function(data){ successCallback(data); })
.catch(function(err){ failureCallback(err); });
}, },
eventClick: function(info) { eventClick: function(info) {
var ev = info.event; var ev = info.event;
var props = ev.extendedProps; var props = ev.extendedProps || {};
var html = '<strong>' + ev.title + '</strong><br>' + ev.start.toLocaleDateString() + ' - ' + (new Date(ev.end).toLocaleDateString()) + '<br>'; var html = '<strong>' + ev.title + '</strong><br>' +
formatAllDayRange(ev.start, ev.end) + '<br>';
if (props.type === 'user') { if (props.type === 'user') {
html += 'Status: ' + (props.status || '') + '<br>'; html += 'Mitarbeiter: ' + (props.employee_name || '') + '<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') { } else if (props.type === 'company') {
html += 'Beschreibung: ' + (props.description || '') + '<br>'; html += 'Beschreibung: ' + (props.description || '') + '<br>';
} }
document.getElementById('detailsContent').innerHTML = html; document.getElementById('detailsContent').innerHTML = html;
document.getElementById('eventDetails').style.display = 'block'; document.getElementById('eventDetails').style.display = 'block';
} }
+37 -9
View File
@@ -3,7 +3,6 @@ session_start();
require_once('inc/config.inc.php'); require_once('inc/config.inc.php');
require_once('inc/functions.inc.php'); require_once('inc/functions.inc.php');
// allow any logged-in user to view the team calendar (read-only)
$user = check_user(); $user = check_user();
include 'header.php'; include 'header.php';
@@ -11,13 +10,21 @@ include 'header.php';
<div class="container"> <div class="container">
<h2>Team Urlaubskalender</h2> <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> <div id="calendar"></div>
<br> <br>
<div> <div class="mb-3">
<span class="badge badge-success">genehmigt</span> <strong>Anzeige:</strong><br>
<span class="badge badge-success">Urlaub</span>
<span class="badge badge-primary">Betriebsurlaub</span> <span class="badge badge-primary">Betriebsurlaub</span>
</div> </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;"> <div id="eventDetails" style="display:none;">
<h4>Details</h4> <h4>Details</h4>
<div id="detailsContent"></div> <div id="detailsContent"></div>
@@ -30,23 +37,44 @@ include 'header.php';
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
var calendarEl = document.getElementById('calendar'); 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, { var calendar = new FullCalendar.Calendar(calendarEl, {
initialView: 'dayGridMonth', initialView: 'dayGridMonth',
firstDay: 1, firstDay: 1,
height: 650, height: 650,
events: function(info, successCallback, failureCallback) { events: function(info, successCallback, failureCallback) {
var url = 'api/vacations.php?start=' + info.startStr + '&end=' + info.endStr + '&public=1&public_all=1'; 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); }); fetch(url)
.then(function(res){ return res.json(); })
.then(function(data){ successCallback(data); })
.catch(function(err){ failureCallback(err); });
}, },
eventClick: function(info) { eventClick: function(info) {
var ev = info.event; var ev = info.event;
var props = ev.extendedProps; var props = ev.extendedProps || {};
var html = '<strong>' + ev.title + '</strong><br>' + ev.start.toLocaleDateString() + ' - ' + (new Date(ev.end).toLocaleDateString()) + '<br>'; var html = '<strong>' + ev.title + '</strong><br>' +
formatAllDayRange(ev.start, ev.end) + '<br>';
if (props.type === 'user') { if (props.type === 'user') {
html += 'Mitarbeiter-ID: ' + (props.user_id || '') + '<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') { } else if (props.type === 'company') {
html += 'Beschreibung: ' + (props.description || '') + '<br>'; html += 'Beschreibung: ' + (props.description || '') + '<br>';
} }
document.getElementById('detailsContent').innerHTML = html; document.getElementById('detailsContent').innerHTML = html;
document.getElementById('eventDetails').style.display = 'block'; document.getElementById('eventDetails').style.display = 'block';
} }
+136 -22
View File
@@ -2,60 +2,167 @@
session_start(); session_start();
require_once('inc/config.inc.php'); require_once('inc/config.inc.php');
require_once('inc/functions.inc.php'); require_once('inc/functions.inc.php');
require_once __DIR__ . '/inc/vacation_absence.inc.php';
$user = check_user(); $user = check_user();
if (!is_admin_user()) { if (!is_admin_user()) {
die('Zugriff verweigert. Nur Chefs dürfen die Urlaubsübersicht sehen.'); 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'; include 'header.php';
// Jahr für Auswertung // Jahr für Auswertung
$year = date('Y'); $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 // Lade alle Mitarbeiter
$stmt = $pdo->prepare("SELECT id, vorname, nachname, email, urlaubstage FROM users ORDER BY nachname, vorname"); $stmt = $pdo->prepare("SELECT id, vorname, nachname, email, urlaubstage FROM users ORDER BY nachname, vorname");
$stmt->execute(); $stmt->execute();
$users = $stmt->fetchAll(); $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"> <div class="container">
<h2>Urlaubsübersicht (<?php echo $year; ?>)</h2> <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"> <table class="table table-striped table-bordered">
<thead> <thead>
<tr> <tr>
<th>Mitarbeiter</th> <th>Mitarbeiter</th>
<th>Email</th> <th>Email</th>
<th>Anspruch</th> <th>Anspruch</th>
<th>Genutzt (<?php echo $year; ?>)</th> <th>Urlaub genutzt (<?php echo $year; ?>)</th>
<th>Verbleibend</th> <th>Verbleibend</th>
<th>Ausstehend</th> <th>Alle Abwesenheiten</th>
<th>Bevorstehende Urlaube</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> </tr>
</thead> </thead>
<tbody> <tbody>
<?php foreach ($users as $u): <?php foreach ($users as $u):
$uid = $u['id']; $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; $entitlement = isset($u['urlaubstage']) ? (int)$u['urlaubstage'] : 0;
$used = (int)($statsByUser[$uid]['urlaub_used_days'] ?? 0);
$remaining = $entitlement - $used; $remaining = $entitlement - $used;
$totalAbsences = (int)($statsByUser[$uid]['total_absence_days'] ?? 0);
$pending = (int)($statsByUser[$uid]['pending_count'] ?? 0);
$upcoming = $upcomingByUser[$uid] ?? [];
?> ?>
<tr> <tr>
<td><?php echo htmlspecialchars($u['vorname'] . ' ' . $u['nachname']); ?></td> <td><?php echo htmlspecialchars($u['vorname'] . ' ' . $u['nachname']); ?></td>
@@ -63,11 +170,17 @@ $users = $stmt->fetchAll();
<td><?php echo $entitlement; ?></td> <td><?php echo $entitlement; ?></td>
<td><?php echo $used; ?></td> <td><?php echo $used; ?></td>
<td><?php echo $remaining; ?></td> <td><?php echo $remaining; ?></td>
<td><?php echo $totalAbsences; ?></td>
<td><?php echo $pending; ?></td> <td><?php echo $pending; ?></td>
<?php foreach ($reasonKeys as $reasonKey): ?>
<td><?php echo vacationOverviewReasonValue($statsByUser, (int)$uid, $reasonKey); ?></td>
<?php endforeach; ?>
<td> <td>
<?php if (count($upcoming) == 0) { echo '-'; } else { <?php if (count($upcoming) == 0) { echo '-'; } else {
foreach ($upcoming as $up) { foreach ($upcoming as $up) {
echo htmlspecialchars($up['start_date'] . ' → ' . $up['end_date'] . ' (' . $up['days'] . 'd) ' . ' [' . $up['status'] . ']'); $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>'; echo '<br>';
} }
} ?> } ?>
@@ -76,8 +189,9 @@ $users = $stmt->fetchAll();
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
</div>
<p>Hinweis: Ansprüche werden aus dem Feld <strong>users.urlaubstage</strong> gelesen. Falls dieses Feld leer ist, bitte in der Nutzerverwaltung pflegen.</p> <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> </div>