Reorganize into monorepo layout, move mitarbeiter-app to legacy reference
Consolidates the previously separate omsorgapp and omsorgCore repos (each had their own nested .git with GitHub history) plus the old root-level website/mitarbeiter-app into a single monorepo, matching the structure already documented in the root CLAUDE.md. Also moves the PHP employee app aside as omsorgWeb/mitarbeiter-app-legacy/ to serve as a template for a ground-up rewrite. Fixes .gitignore in the same pass: the config-secrets/uploads/data patterns were unanchored (relative to repo root, not depth-agnostic), so they silently stopped matching once the app moved under omsorgWeb/. Patterns are now **/-prefixed and cover both mitarbeiter-app and mitarbeiter-app-legacy, keeping DB/SMTP credentials and uploaded employee documents out of version control. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
8beb0fcf52
commit
b6c1389c55
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_admin();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ../pages/admin.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ── Login attempts ────────────────────────────────────────────────── */
|
||||
if (($_POST['_login_action'] ?? '') === 'clear') {
|
||||
db()->exec('DELETE FROM login_attempts');
|
||||
header('Location: ../pages/admin.php?tab=login&cleared=1');
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ── User actions ──────────────────────────────────────────────────── */
|
||||
if (!empty($_POST['_user_action'])) {
|
||||
$action = $_POST['_user_action'];
|
||||
$user_id = (int)($_POST['user_id'] ?? 0);
|
||||
$back = '../pages/admin.php?tab=nutzer';
|
||||
|
||||
switch ($action) {
|
||||
|
||||
case 'add_user': {
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$uname = trim($_POST['name'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$telefon = trim($_POST['telefon'] ?? '');
|
||||
$role = in_array($_POST['role'] ?? '', ['user', 'admin'], true) ? $_POST['role'] : 'user';
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if ($username === '' || $uname === '' || strlen($password) < 6) {
|
||||
header('Location: ' . $back . '&user_msg=' . urlencode('Benutzername, Name und Passwort (mind. 6 Zeichen) sind Pflichtfelder.'));
|
||||
exit;
|
||||
}
|
||||
try {
|
||||
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
|
||||
$stmt = db()->prepare(
|
||||
'INSERT INTO users (username, name, role, password_hash, active, created_at, email, telefon)
|
||||
VALUES (?, ?, ?, ?, 1, ?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([$username, $uname, $role, $hash, date('c'), $email, $telefon]);
|
||||
} catch (\PDOException $ex) {
|
||||
$msg = str_contains($ex->getMessage(), 'UNIQUE') ? 'Benutzername bereits vergeben.' : 'Datenbankfehler.';
|
||||
header('Location: ' . $back . '&user_msg=' . urlencode($msg));
|
||||
exit;
|
||||
}
|
||||
header('Location: ' . $back . '&user_ok=1&user_msg=' . urlencode('Nutzer erstellt.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
case 'edit_user': {
|
||||
$uname = trim($_POST['name'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$telefon = trim($_POST['telefon'] ?? '');
|
||||
$role = in_array($_POST['role'] ?? '', ['user', 'admin'], true) ? $_POST['role'] : 'user';
|
||||
$active = (int)(bool)($_POST['active'] ?? 0);
|
||||
|
||||
if ($user_id < 1 || $uname === '') {
|
||||
header('Location: ' . $back . '&user_msg=' . urlencode('Ungültige Eingabe.'));
|
||||
exit;
|
||||
}
|
||||
$stmt = db()->prepare(
|
||||
'UPDATE users SET name=?, email=?, telefon=?, role=?, active=? WHERE id=?'
|
||||
);
|
||||
$stmt->execute([$uname, $email, $telefon, $role, $active, $user_id]);
|
||||
header('Location: ' . $back . '&user_ok=1&user_msg=' . urlencode('Nutzer gespeichert.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
case 'change_password': {
|
||||
$password = $_POST['new_password'] ?? '';
|
||||
if ($user_id < 1 || strlen($password) < 6) {
|
||||
header('Location: ' . $back . '&user_msg=' . urlencode('Passwort muss mindestens 6 Zeichen lang sein.'));
|
||||
exit;
|
||||
}
|
||||
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
|
||||
$stmt = db()->prepare('UPDATE users SET password_hash=? WHERE id=?');
|
||||
$stmt->execute([$hash, $user_id]);
|
||||
header('Location: ' . $back . '&user_ok=1&user_msg=' . urlencode('Passwort geändert.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
case 'delete_user': {
|
||||
if ($user_id < 1) {
|
||||
header('Location: ' . $back . '&user_msg=' . urlencode('Ungültige Nutzer-ID.'));
|
||||
exit;
|
||||
}
|
||||
$stmt = db()->prepare('SELECT username FROM users WHERE id=?');
|
||||
$stmt->execute([$user_id]);
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) {
|
||||
header('Location: ' . $back . '&user_msg=' . urlencode('Nutzer nicht gefunden.'));
|
||||
exit;
|
||||
}
|
||||
if ($row['username'] === current_username()) {
|
||||
header('Location: ' . $back . '&user_msg=' . urlencode('Den eigenen Account kann man nicht löschen.'));
|
||||
exit;
|
||||
}
|
||||
$stmt = db()->prepare('DELETE FROM users WHERE id=?');
|
||||
$stmt->execute([$user_id]);
|
||||
header('Location: ' . $back . '&user_ok=1&user_msg=' . urlencode('Nutzer gelöscht.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
default:
|
||||
header('Location: ' . $back . '&user_msg=' . urlencode('Unbekannte Aktion.'));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Request actions ───────────────────────────────────────────────── */
|
||||
$request_id = (int)($_POST['request_id'] ?? 0);
|
||||
$action = $_POST['action'] ?? '';
|
||||
$admin_note = trim($_POST['admin_note'] ?? '');
|
||||
$type = $_POST['type'] ?? '';
|
||||
|
||||
$allowed_types = ['urlaubsantrag', 'abwesenheitsantrag', 'benefitsantrag', 'werben', 'fortbildungsantrag', 'stundennachweis'];
|
||||
|
||||
if (!in_array($type, $allowed_types, true)) {
|
||||
header('Location: ../pages/admin.php?error=' . urlencode('Ungültiger Antragstyp.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ── Delete request ────────────────────────────────────────────────── */
|
||||
if ($action === 'delete') {
|
||||
if ($request_id < 1) {
|
||||
header('Location: ../pages/admin.php?error=' . urlencode('Ungültige Antrags-ID.'));
|
||||
exit;
|
||||
}
|
||||
$table = 'requests_' . $type;
|
||||
$stmt = db()->prepare("SELECT * FROM $table WHERE id = ?");
|
||||
$stmt->execute([$request_id]);
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) {
|
||||
header('Location: ../pages/admin.php?error=' . urlencode('Antrag nicht gefunden.'));
|
||||
exit;
|
||||
}
|
||||
// Delete associated file for types that have uploads
|
||||
if (in_array($type, ['stundennachweis', 'fortbildungsantrag'], true) && !empty($row['filename'])) {
|
||||
$file_path = __DIR__ . '/../uploads/' . $row['filename'];
|
||||
if (is_file($file_path)) {
|
||||
unlink($file_path);
|
||||
}
|
||||
}
|
||||
$stmt = db()->prepare("DELETE FROM $table WHERE id = ?");
|
||||
$stmt->execute([$request_id]);
|
||||
header('Location: ../pages/admin.php?deleted=1');
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ── Status update ─────────────────────────────────────────────────── */
|
||||
if (!in_array($action, ['accepted', 'rejected'], true) || $request_id < 1) {
|
||||
header('Location: ../pages/admin.php?error=' . urlencode('Ungültige Aktion.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$table = 'requests_' . $type;
|
||||
$stmt = db()->prepare("SELECT id FROM $table WHERE id = ?");
|
||||
$stmt->execute([$request_id]);
|
||||
if (!$stmt->fetch()) {
|
||||
header('Location: ../pages/admin.php?error=' . urlencode('Antrag nicht gefunden.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = db()->prepare("UPDATE $table SET status = ?, admin_note = ?, updated_at = ? WHERE id = ?");
|
||||
$stmt->execute([$action, $admin_note, date('c'), $request_id]);
|
||||
|
||||
header('Location: ../pages/admin.php?type=' . urlencode($type) . '&id=' . $request_id . '&updated=1');
|
||||
exit;
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_login();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ../pages/dokumentenarchiv.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$id = (int) ($_POST['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
http_response_code(400);
|
||||
echo 'Ungültige Anfrage.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = db()->prepare('SELECT * FROM dokumente WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
$doc = $stmt->fetch();
|
||||
|
||||
if (!$doc) {
|
||||
http_response_code(404);
|
||||
echo 'Dokument nicht gefunden.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = current_user();
|
||||
if ((int) $doc['user_id'] !== (int) $user['id'] && !is_admin()) {
|
||||
http_response_code(403);
|
||||
echo 'Keine Berechtigung.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$file_path = dirname(__DIR__) . '/uploads/' . $doc['filename'];
|
||||
if (is_file($file_path)) {
|
||||
unlink($file_path);
|
||||
}
|
||||
|
||||
db()->prepare('DELETE FROM dokumente WHERE id = ?')->execute([$id]);
|
||||
|
||||
header('Location: ../pages/dokumentenarchiv.php?deleted=1');
|
||||
exit;
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/upload.php';
|
||||
require_admin();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ../pages/admin.php?tab=downloads');
|
||||
exit;
|
||||
}
|
||||
|
||||
$action = $_POST['_action'] ?? '';
|
||||
|
||||
/* ── Add ──────────────────────────────────────────────────────────────── */
|
||||
if ($action === 'add') {
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
|
||||
if ($title === '') {
|
||||
header('Location: ../pages/admin.php?tab=downloads&err=notitle');
|
||||
exit;
|
||||
}
|
||||
|
||||
$up = handle_upload($_FILES['file'] ?? [], [
|
||||
'allowed' => UPLOAD_TYPES_OFFICE,
|
||||
'dir' => dirname(__DIR__) . '/downloads/',
|
||||
'naming' => 'hash',
|
||||
'max_bytes' => 20 * 1024 * 1024,
|
||||
'messages' => [
|
||||
'missing' => 'nofile', 'upload' => 'nofile',
|
||||
'ext' => 'badext', 'mime' => 'badmime',
|
||||
'size' => 'toobig', 'move' => 'upload',
|
||||
],
|
||||
]);
|
||||
if (!$up['ok']) {
|
||||
header('Location: ../pages/admin.php?tab=downloads&err=' . $up['error']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$safe_name = $up['filename'];
|
||||
$original_name = $up['original_name'];
|
||||
|
||||
$sort_order = (int)db()->query('SELECT COALESCE(MAX(sort_order), 0) + 10 FROM downloads')->fetchColumn();
|
||||
$now = date('c');
|
||||
|
||||
$stmt = db()->prepare(
|
||||
'INSERT INTO downloads (title, description, filename, original_name, sort_order, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([$title, $description, $safe_name, $original_name, $sort_order, $now, $now]);
|
||||
|
||||
header('Location: ../pages/admin.php?tab=downloads&ok=added');
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ── Delete ───────────────────────────────────────────────────────────── */
|
||||
if ($action === 'delete') {
|
||||
$id = (int)($_POST['download_id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
header('Location: ../pages/admin.php?tab=downloads&err=invalid');
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = db()->prepare('SELECT filename FROM downloads WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if ($row) {
|
||||
db()->prepare('DELETE FROM downloads WHERE id = ?')->execute([$id]);
|
||||
@unlink(dirname(__DIR__) . '/downloads/' . $row['filename']);
|
||||
}
|
||||
|
||||
header('Location: ../pages/admin.php?tab=downloads&ok=deleted');
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Location: ../pages/admin.php?tab=downloads');
|
||||
exit;
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_admin();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') !== 'XMLHttpRequest') {
|
||||
echo json_encode(['ok' => false, 'error' => 'forbidden']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['ok' => false, 'error' => 'method']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$body = file_get_contents('php://input');
|
||||
$data = json_decode($body, true);
|
||||
$order = $data['order'] ?? null;
|
||||
|
||||
if (!is_array($order)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'invalid']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$ids = array_values(array_filter(array_map('intval', $order), fn($v) => $v > 0));
|
||||
|
||||
if (empty($ids)) {
|
||||
echo json_encode(['ok' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$now = date('c');
|
||||
$pos = 10;
|
||||
$stmt = db()->prepare('UPDATE downloads SET sort_order = ?, updated_at = ? WHERE id = ?');
|
||||
|
||||
db()->beginTransaction();
|
||||
foreach ($ids as $id) {
|
||||
$stmt->execute([$pos, $now, $id]);
|
||||
$pos += 10;
|
||||
}
|
||||
db()->commit();
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
exit;
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/upload.php';
|
||||
require_admin();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ../pages/admin.php?tab=einsatzanweisung');
|
||||
exit;
|
||||
}
|
||||
|
||||
verify_csrf();
|
||||
|
||||
$back = '../pages/admin.php?tab=einsatzanweisung';
|
||||
$action = $_POST['_action'] ?? '';
|
||||
$user_id = (int)($_POST['target_user_id'] ?? 0);
|
||||
|
||||
if ($user_id <= 0) {
|
||||
header('Location: ' . $back . '&err=' . urlencode('Ungültiger Nutzer.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = db()->prepare('SELECT id FROM users WHERE id = ?');
|
||||
$stmt->execute([$user_id]);
|
||||
if (!$stmt->fetch()) {
|
||||
header('Location: ' . $back . '&err=' . urlencode('Nutzer nicht gefunden.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'update_ort') {
|
||||
$ort = trim($_POST['ort'] ?? '');
|
||||
|
||||
db()->prepare(
|
||||
'INSERT INTO einsatzanweisung (user_id, ort)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET ort = excluded.ort'
|
||||
)->execute([$user_id, $ort]);
|
||||
|
||||
header('Location: ' . $back . '&ok=' . urlencode('Einsatzort gespeichert.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'upload_pdf') {
|
||||
$stmt = db()->prepare('SELECT u.username, e.filename AS old_filename FROM users u LEFT JOIN einsatzanweisung e ON e.user_id = u.id WHERE u.id = ?');
|
||||
$stmt->execute([$user_id]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
$dir = dirname(__DIR__) . '/uploads/';
|
||||
$up = handle_upload($_FILES['pdf'] ?? [], [
|
||||
'allowed' => UPLOAD_TYPES_PDF,
|
||||
'dir' => $dir,
|
||||
'prefix' => 'einsatzanweisung',
|
||||
'username' => $row['username'],
|
||||
'messages' => [
|
||||
'missing' => 'Bitte eine PDF-Datei auswählen.',
|
||||
'upload' => 'Fehler beim Hochladen.',
|
||||
'ext' => 'Nur PDF-Dateien erlaubt.',
|
||||
'mime' => 'Nur PDF-Dateien erlaubt.',
|
||||
'size' => 'Datei ist zu groß (max. 12 MB).',
|
||||
'move' => 'Fehler beim Speichern der Datei.',
|
||||
],
|
||||
]);
|
||||
if (!$up['ok']) {
|
||||
header('Location: ' . $back . '&err=' . urlencode($up['error']));
|
||||
exit;
|
||||
}
|
||||
|
||||
$original_name = $up['original_name'];
|
||||
$safe_name = $up['filename'];
|
||||
|
||||
if (!empty($row['old_filename'])) {
|
||||
$old_path = $dir . basename($row['old_filename']);
|
||||
if (is_file($old_path)) {
|
||||
unlink($old_path);
|
||||
}
|
||||
}
|
||||
|
||||
$now = date('c');
|
||||
db()->prepare(
|
||||
'INSERT INTO einsatzanweisung (user_id, filename, original_name, uploaded_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET
|
||||
filename = excluded.filename,
|
||||
original_name = excluded.original_name,
|
||||
uploaded_at = excluded.uploaded_at'
|
||||
)->execute([$user_id, $safe_name, $original_name, $now]);
|
||||
|
||||
header('Location: ' . $back . '&ok=' . urlencode('PDF erfolgreich hochgeladen.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Location: ' . $back);
|
||||
exit;
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/upload.php';
|
||||
require_admin();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ../pages/admin.php?tab=fortbildung');
|
||||
exit;
|
||||
}
|
||||
|
||||
$action = $_POST['_action'] ?? '';
|
||||
|
||||
/* ── Add ──────────────────────────────────────────────────────────────── */
|
||||
if ($action === 'add') {
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
|
||||
if ($title === '') {
|
||||
header('Location: ../pages/admin.php?tab=fortbildung&err=notitle');
|
||||
exit;
|
||||
}
|
||||
|
||||
$up = handle_upload($_FILES['file'] ?? [], [
|
||||
'allowed' => UPLOAD_TYPES_OFFICE,
|
||||
'dir' => dirname(__DIR__) . '/fortbildung-materials/',
|
||||
'naming' => 'hash',
|
||||
'max_bytes' => 20 * 1024 * 1024,
|
||||
'messages' => [
|
||||
'missing' => 'nofile', 'upload' => 'nofile',
|
||||
'ext' => 'badext', 'mime' => 'badmime',
|
||||
'size' => 'toobig', 'move' => 'upload',
|
||||
],
|
||||
]);
|
||||
if (!$up['ok']) {
|
||||
header('Location: ../pages/admin.php?tab=fortbildung&err=' . $up['error']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$safe_name = $up['filename'];
|
||||
$original_name = $up['original_name'];
|
||||
|
||||
$sort_order = (int)db()->query('SELECT COALESCE(MAX(sort_order), 0) + 10 FROM fortbildung_materials')->fetchColumn();
|
||||
$now = date('c');
|
||||
|
||||
$stmt = db()->prepare(
|
||||
'INSERT INTO fortbildung_materials (title, description, filename, original_name, sort_order, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([$title, $description, $safe_name, $original_name, $sort_order, $now, $now]);
|
||||
|
||||
header('Location: ../pages/admin.php?tab=fortbildung&ok=added');
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ── Delete ───────────────────────────────────────────────────────────── */
|
||||
if ($action === 'delete') {
|
||||
$id = (int)($_POST['material_id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
header('Location: ../pages/admin.php?tab=fortbildung&err=invalid');
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = db()->prepare('SELECT filename FROM fortbildung_materials WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if ($row) {
|
||||
db()->prepare('DELETE FROM fortbildung_materials WHERE id = ?')->execute([$id]);
|
||||
@unlink(dirname(__DIR__) . '/fortbildung-materials/' . $row['filename']);
|
||||
}
|
||||
|
||||
header('Location: ../pages/admin.php?tab=fortbildung&ok=deleted');
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Location: ../pages/admin.php?tab=fortbildung');
|
||||
exit;
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_admin();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') !== 'XMLHttpRequest') {
|
||||
echo json_encode(['ok' => false, 'error' => 'forbidden']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['ok' => false, 'error' => 'method']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$body = file_get_contents('php://input');
|
||||
$data = json_decode($body, true);
|
||||
$order = $data['order'] ?? null;
|
||||
|
||||
if (!is_array($order)) {
|
||||
echo json_encode(['ok' => false, 'error' => 'invalid']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$ids = array_values(array_filter(array_map('intval', $order), fn($v) => $v > 0));
|
||||
|
||||
if (empty($ids)) {
|
||||
echo json_encode(['ok' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$now = date('c');
|
||||
$pos = 10;
|
||||
$stmt = db()->prepare('UPDATE fortbildung_materials SET sort_order = ?, updated_at = ? WHERE id = ?');
|
||||
|
||||
db()->beginTransaction();
|
||||
foreach ($ids as $id) {
|
||||
$stmt->execute([$pos, $now, $id]);
|
||||
$pos += 10;
|
||||
}
|
||||
db()->commit();
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
exit;
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
$_SESSION = [];
|
||||
if (ini_get('session.use_cookies')) {
|
||||
$p = session_get_cookie_params();
|
||||
setcookie(session_name(), '', time() - 42000,
|
||||
$p['path'], $p['domain'], $p['secure'], $p['httponly']);
|
||||
}
|
||||
session_destroy();
|
||||
header('Location: ../index.php');
|
||||
exit;
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/db.php';
|
||||
require_admin();
|
||||
|
||||
$action = $_POST['_action'] ?? '';
|
||||
|
||||
if ($action === 'add') {
|
||||
$title = trim((string)($_POST['title'] ?? ''));
|
||||
$text = trim((string)($_POST['text'] ?? ''));
|
||||
$date = trim((string)($_POST['date'] ?? '')) ?: date('Y-m-d');
|
||||
if ($title === '' || $text === '') {
|
||||
header('Location: ../pages/admin.php?tab=news&err=empty');
|
||||
exit;
|
||||
}
|
||||
$stmt = db()->prepare(
|
||||
'INSERT INTO news (title, text, date, created_at) VALUES (?, ?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([$title, $text, $date, date('c')]);
|
||||
header('Location: ../pages/admin.php?tab=news&ok=added');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete') {
|
||||
$id = (int)($_POST['news_id'] ?? 0);
|
||||
if ($id > 0) {
|
||||
db()->prepare('DELETE FROM news WHERE id = ?')->execute([$id]);
|
||||
}
|
||||
header('Location: ../pages/admin.php?tab=news&ok=deleted');
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Location: ../pages/admin.php?tab=news');
|
||||
exit;
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/db.php';
|
||||
require_login();
|
||||
|
||||
$ajax = !empty($_POST['_ajax']);
|
||||
|
||||
function ajax_error(string $msg): never {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['ok' => false, 'error' => $msg]);
|
||||
exit;
|
||||
}
|
||||
function ajax_ok(): never {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['ok' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
if ($ajax) ajax_error('Ungültige Anfrage.');
|
||||
header('Location: ../pages/dienstplan.php'); exit;
|
||||
}
|
||||
|
||||
$user_id = current_user()['id'];
|
||||
$year = (int)($_POST['year'] ?? 0);
|
||||
$month = (int)($_POST['month'] ?? 0);
|
||||
|
||||
if ($year < 2020 || $year > 2099 || $month < 1 || $month > 12) {
|
||||
if ($ajax) ajax_error('Ungültiger Monat.');
|
||||
header('Location: ../pages/dienstplan.php'); exit;
|
||||
}
|
||||
|
||||
$back = '../pages/dienstplan.php?year=' . $year . '&month=' . $month;
|
||||
$month_str = sprintf('%04d-%02d', $year, $month);
|
||||
$schicht_ok = ['frueh', 'spaet', 'nacht'];
|
||||
$submitted = $_POST['schicht'] ?? [];
|
||||
|
||||
if (!is_array($submitted)) {
|
||||
header('Location: ' . $back . '&saved=1'); exit;
|
||||
}
|
||||
|
||||
$now = date('c');
|
||||
db()->beginTransaction();
|
||||
try {
|
||||
foreach ($submitted as $date => $schicht) {
|
||||
if (!is_string($date) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) continue;
|
||||
if (substr($date, 0, 7) !== $month_str) continue;
|
||||
[$y, $m, $d] = array_map('intval', explode('-', $date));
|
||||
if (!checkdate($m, $d, $y)) continue;
|
||||
|
||||
if (in_array($schicht, $schicht_ok, true)) {
|
||||
db()->prepare(
|
||||
'INSERT INTO dienstplan (user_id, date, schicht, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE schicht=VALUES(schicht), updated_at=VALUES(updated_at)'
|
||||
)->execute([$user_id, $date, $schicht, $now, $now]);
|
||||
} else {
|
||||
db()->prepare(
|
||||
'DELETE FROM dienstplan WHERE user_id = ? AND date = ?'
|
||||
)->execute([$user_id, $date]);
|
||||
}
|
||||
}
|
||||
db()->commit();
|
||||
} catch (\PDOException $ex) {
|
||||
if (db()->inTransaction()) db()->rollBack();
|
||||
$msg = 'Datenbankfehler: ' . $ex->getMessage();
|
||||
if ($ajax) ajax_error($msg);
|
||||
header('Location: ' . $back . '&error=' . urlencode($msg));
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($ajax) ajax_ok();
|
||||
header('Location: ' . $back . '&saved=1');
|
||||
exit;
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_login();
|
||||
|
||||
$config = _omsorgcore_config();
|
||||
$token = $_SESSION['omsorgcore_access_token'];
|
||||
$action = $_POST['action'] ?? '';
|
||||
$employeeId = $_SESSION['omsorgcore_profile']['employeeId'] ?? null;
|
||||
|
||||
if ($action === 'profile') {
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$telefon = trim($_POST['telefon'] ?? '');
|
||||
|
||||
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
header('Location: ../pages/settings.php?error=' . urlencode('Ungültige E-Mail-Adresse.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$employeeId) {
|
||||
header('Location: ../pages/settings.php?error=' . urlencode('Kein verknüpfter Mitarbeiter-Datensatz gefunden.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$current = omsorgcore_employees_get($config, $token, $employeeId);
|
||||
if (!$current['ok']) {
|
||||
header('Location: ../pages/settings.php?error=' . urlencode('Profil konnte nicht geladen werden.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$payload = $current['data'];
|
||||
$payload['email'] = $email;
|
||||
$payload['phoneNumber'] = $telefon;
|
||||
unset($payload['id']);
|
||||
|
||||
$result = omsorgcore_employees_update($config, $token, $employeeId, $payload);
|
||||
if (!$result['ok']) {
|
||||
header('Location: ../pages/settings.php?error=' . urlencode('Profil konnte nicht gespeichert werden.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
_refresh_profile();
|
||||
header('Location: ../pages/settings.php?saved=1');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'password') {
|
||||
$old = $_POST['old_password'] ?? '';
|
||||
$new = $_POST['new_password'] ?? '';
|
||||
$repeat = $_POST['new_password_repeat'] ?? '';
|
||||
|
||||
if (mb_strlen($new) < 8) {
|
||||
header('Location: ../pages/settings.php?error=' . urlencode('Neues Passwort muss mindestens 8 Zeichen haben.'));
|
||||
exit;
|
||||
}
|
||||
if ($new !== $repeat) {
|
||||
header('Location: ../pages/settings.php?error=' . urlencode('Die neuen Passwörter stimmen nicht überein.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$result = omsorgcore_change_password($config, $token, $old, $new);
|
||||
if (!$result['ok']) {
|
||||
$message = $result['status'] === 401 ? 'Aktuelles Passwort ist falsch.' : 'Passwort konnte nicht geändert werden.';
|
||||
header('Location: ../pages/settings.php?error=' . urlencode($message));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Serverseitig wurden alle Sessions widerrufen - neuer Login mit dem neuen Passwort ist nötig.
|
||||
_clear_omsorgcore_session();
|
||||
header('Location: ../index.php?pwchanged=1');
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Location: ../pages/settings.php');
|
||||
exit;
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/mail.php';
|
||||
require_login();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ../pages/abwesenheitsantrag.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$back = '../pages/abwesenheitsantrag.php';
|
||||
|
||||
$von = trim($_POST['von'] ?? '');
|
||||
$bis = trim($_POST['bis'] ?? '');
|
||||
$grund = trim($_POST['grund'] ?? '');
|
||||
$vertretung = trim($_POST['vertretung'] ?? '');
|
||||
$nachricht = trim($_POST['nachricht'] ?? '');
|
||||
|
||||
if ($von === '' || $bis === '' || $grund === '') {
|
||||
header('Location: ' . $back . '?error=' . urlencode('Von, Bis und Grund sind Pflichtfelder.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$allowed_grund = ['krankheit', 'arztbesuch', 'behoerdengang', 'sonderurlaub', 'elternzeit_pflegezeit', 'sonstiges'];
|
||||
if (!in_array($grund, $allowed_grund, true)) {
|
||||
header('Location: ' . $back . '?error=' . urlencode('Ungültiger Grund.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!strtotime($von) || !strtotime($bis)) {
|
||||
header('Location: ' . $back . '?error=' . urlencode('Ungültiges Datum.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
if (strtotime($von) > strtotime($bis)) {
|
||||
header('Location: ' . $back . '?error=' . urlencode('Das Startdatum muss vor dem Enddatum liegen.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$user_id = current_user()['id'];
|
||||
$now = date('c');
|
||||
|
||||
db()->prepare(
|
||||
'INSERT INTO requests_abwesenheitsantrag (user_id, status, admin_note, created_at, updated_at, von, bis, grund, vertretung, nachricht)
|
||||
VALUES (?, \'pending\', \'\', ?, ?, ?, ?, ?, ?, ?)'
|
||||
)->execute([$user_id, $now, $now, $von, $bis, $grund, $vertretung, $nachricht]);
|
||||
|
||||
$grund_labels = [
|
||||
'krankheit' => 'Krankheit',
|
||||
'arztbesuch' => 'Arztbesuch',
|
||||
'behoerdengang' => 'Behördengang',
|
||||
'sonderurlaub' => 'Sonderurlaub',
|
||||
'elternzeit_pflegezeit' => 'Elternzeit / Pflegezeit',
|
||||
'sonstiges' => 'Sonstiges',
|
||||
];
|
||||
|
||||
$config = require __DIR__ . '/../lib/config.php';
|
||||
smtp_send(
|
||||
$config,
|
||||
$config['mail_sabrina'],
|
||||
'Neuer Abwesenheitsantrag: ' . current_name(),
|
||||
"Ein Abwesenheitsantrag wurde eingereicht.\n\n"
|
||||
. 'Mitarbeiter: ' . current_name() . "\n"
|
||||
. 'Von: ' . $von . "\n"
|
||||
. 'Bis: ' . $bis . "\n"
|
||||
. 'Grund: ' . ($grund_labels[$grund] ?? $grund) . "\n"
|
||||
. 'Vertretung: ' . ($vertretung ?: '—') . "\n"
|
||||
. 'Nachricht: ' . ($nachricht ?: '—') . "\n\n"
|
||||
. "---\nGesendet über OMSORG Connect"
|
||||
);
|
||||
|
||||
header('Location: ' . $back . '?success=1');
|
||||
exit;
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/mail.php';
|
||||
require_login();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ../pages/benefitsantrag.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$back = '../pages/benefitsantrag.php';
|
||||
|
||||
$benefit = trim($_POST['benefit'] ?? '');
|
||||
$nachricht = trim($_POST['nachricht'] ?? '');
|
||||
|
||||
$allowed_benefits = ['yoga', 'autogenes_training', 'massage', 'tankgutschein', 'online_gutscheine'];
|
||||
if (!in_array($benefit, $allowed_benefits, true)) {
|
||||
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(400);
|
||||
echo json_encode(['ok' => false, 'error' => 'Bitte einen Benefit auswählen.']);
|
||||
exit;
|
||||
}
|
||||
header('Location: ' . $back . '?error=' . urlencode('Bitte einen Benefit auswählen.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$user_id = current_user()['id'];
|
||||
$now = date('c');
|
||||
|
||||
db()->prepare(
|
||||
'INSERT INTO requests_benefitsantrag (user_id, status, admin_note, created_at, updated_at, benefit, nachricht)
|
||||
VALUES (?, \'pending\', \'\', ?, ?, ?, ?)'
|
||||
)->execute([$user_id, $now, $now, $benefit, $nachricht]);
|
||||
|
||||
$benefit_labels = [
|
||||
'yoga' => 'Yoga',
|
||||
'autogenes_training' => 'Autogenes Training',
|
||||
'massage' => 'Massage',
|
||||
'tankgutschein' => 'Tankgutschein',
|
||||
'online_gutscheine' => 'Online-Gutscheine',
|
||||
];
|
||||
|
||||
$config = require __DIR__ . '/../lib/config.php';
|
||||
smtp_send(
|
||||
$config,
|
||||
$config['mail_info'],
|
||||
'Neuer Benefitsantrag: ' . current_name(),
|
||||
"Ein Benefitsantrag wurde eingereicht.\n\n"
|
||||
. 'Mitarbeiter: ' . current_name() . "\n"
|
||||
. 'Benefit: ' . ($benefit_labels[$benefit] ?? $benefit) . "\n"
|
||||
. 'Nachricht: ' . ($nachricht ?: '—') . "\n\n"
|
||||
. "---\nGesendet über OMSORG Connect"
|
||||
);
|
||||
|
||||
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['ok' => true, 'benefit' => $benefit]);
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Location: ' . $back . '?success=1');
|
||||
exit;
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/db.php';
|
||||
require_login();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ../pages/einsatzbewertung.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$back = '../pages/einsatzbewertung.php';
|
||||
|
||||
$einsatzort = trim($_POST['einsatzort'] ?? '');
|
||||
$von = trim($_POST['von'] ?? '');
|
||||
$bis = trim($_POST['bis'] ?? '');
|
||||
$bewertung = (int)($_POST['bewertung'] ?? 0);
|
||||
$wieder = isset($_POST['wieder']) ? 1 : 0;
|
||||
$feedback = trim($_POST['feedback'] ?? '');
|
||||
|
||||
if ($einsatzort === '') {
|
||||
header('Location: ' . $back . '?error=' . urlencode('Bitte den Einsatzort angeben.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($von === '' || $bis === '') {
|
||||
header('Location: ' . $back . '?error=' . urlencode('Von und Bis sind Pflichtfelder.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!strtotime($von) || !strtotime($bis)) {
|
||||
header('Location: ' . $back . '?error=' . urlencode('Ungültiges Datum.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
if (strtotime($von) > strtotime($bis)) {
|
||||
header('Location: ' . $back . '?error=' . urlencode('Das Startdatum muss vor dem Enddatum liegen.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($bewertung < 1 || $bewertung > 5) {
|
||||
header('Location: ' . $back . '?error=' . urlencode('Bitte eine Bewertung von 1 bis 5 Sternen auswählen.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$user_id = current_user()['id'];
|
||||
$now = date('c');
|
||||
|
||||
db()->prepare(
|
||||
'INSERT INTO einsatzbewertungen (user_id, einsatzort, von, bis, bewertung, wieder, feedback, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
)->execute([$user_id, $einsatzort, $von, $bis, $bewertung, $wieder, $feedback, $now]);
|
||||
|
||||
header('Location: ' . $back . '?success=1');
|
||||
exit;
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/upload.php';
|
||||
require_once __DIR__ . '/../lib/mail.php';
|
||||
require_login();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ../pages/fortbildungsantrag.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$back = '../pages/fortbildungsantrag.php';
|
||||
|
||||
$anliegen = trim($_POST['anliegen'] ?? '');
|
||||
$thema = trim($_POST['thema'] ?? '');
|
||||
$nachricht = trim($_POST['nachricht'] ?? '');
|
||||
|
||||
$allowed_anliegen = ['wbl_pdl', 'fortbildung', 'pflichtschulung'];
|
||||
if (!in_array($anliegen, $allowed_anliegen, true)) {
|
||||
redirect_error($back, 'Bitte ein Anliegen auswählen.');
|
||||
}
|
||||
|
||||
if ($thema === '') {
|
||||
redirect_error($back, 'Bitte ein Thema angeben.');
|
||||
}
|
||||
|
||||
$up = handle_upload($_FILES['datei'] ?? [], [
|
||||
'allowed' => UPLOAD_TYPES_DOCS,
|
||||
'dir' => dirname(__DIR__) . '/uploads/',
|
||||
'prefix' => 'fortbildung',
|
||||
'required' => false,
|
||||
]);
|
||||
if (!$up['ok']) {
|
||||
redirect_error($back, $up['error']);
|
||||
}
|
||||
|
||||
$filename = $up['filename'] ?? '';
|
||||
$original_name = $up['original_name'] ?? '';
|
||||
$mime = $up['mime'] ?? null;
|
||||
|
||||
$user_id = current_user()['id'];
|
||||
$now = date('c');
|
||||
|
||||
db()->prepare(
|
||||
'INSERT INTO requests_fortbildungsantrag
|
||||
(user_id, status, admin_note, anliegen, thema, nachricht, filename, original_name, created_at, updated_at)
|
||||
VALUES (?, \'pending\', \'\', ?, ?, ?, ?, ?, ?, ?)'
|
||||
)->execute([$user_id, $anliegen, $thema, $nachricht, $filename, $original_name, $now, $now]);
|
||||
|
||||
$anliegen_labels = [
|
||||
'wbl_pdl' => 'WBL / PDL',
|
||||
'fortbildung' => 'Fortbildung',
|
||||
'pflichtschulung' => 'Pflichtschulung',
|
||||
];
|
||||
|
||||
$config = require __DIR__ . '/../lib/config.php';
|
||||
$attach = ($filename !== '')
|
||||
? ['path' => dirname(__DIR__) . '/uploads/' . $filename, 'name' => $original_name, 'mime' => $mime]
|
||||
: [];
|
||||
smtp_send(
|
||||
$config,
|
||||
$config['mail_info'],
|
||||
'Neuer Fortbildungsantrag: ' . current_name(),
|
||||
"Ein Fortbildungsantrag wurde eingereicht.\n\n"
|
||||
. 'Mitarbeiter: ' . current_name() . "\n"
|
||||
. 'Anliegen: ' . ($anliegen_labels[$anliegen] ?? $anliegen) . "\n"
|
||||
. 'Thema: ' . $thema . "\n"
|
||||
. 'Nachricht: ' . ($nachricht ?: '—') . "\n\n"
|
||||
. "---\nGesendet über OMSORG Connect",
|
||||
$attach
|
||||
);
|
||||
|
||||
redirect_ok($back);
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/upload.php';
|
||||
require_once __DIR__ . '/../lib/mail.php';
|
||||
require_login();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ../pages/stundennachweis.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$back = '../pages/stundennachweis.php';
|
||||
$monat_m = trim($_POST['monat_m'] ?? '');
|
||||
$monat_j = trim($_POST['monat_j'] ?? '');
|
||||
|
||||
if (!preg_match('/^\d{2}$/', $monat_m) || !preg_match('/^\d{4}$/', $monat_j)
|
||||
|| (int)$monat_m < 1 || (int)$monat_m > 12
|
||||
|| (int)$monat_j < 2020 || (int)$monat_j > 2099) {
|
||||
redirect_error($back, 'Bitte einen gültigen Monat und ein gültiges Jahr auswählen.');
|
||||
}
|
||||
$monat = $monat_j . '-' . $monat_m;
|
||||
|
||||
$dir = dirname(__DIR__) . '/uploads/';
|
||||
$up = handle_upload($_FILES['datei'] ?? [], [
|
||||
'allowed' => UPLOAD_TYPES_PDF_IMG,
|
||||
'dir' => $dir,
|
||||
'prefix' => 'stundennachweis',
|
||||
'messages' => [
|
||||
'missing' => 'Bitte eine Datei hochladen.',
|
||||
'ext' => 'Dateityp nicht erlaubt. Erlaubt: PDF, JPG, PNG.',
|
||||
],
|
||||
]);
|
||||
if (!$up['ok']) {
|
||||
redirect_error($back, $up['error']);
|
||||
}
|
||||
|
||||
$safe_name = $up['filename'];
|
||||
$original_name = $up['original_name'];
|
||||
$mime = $up['mime'];
|
||||
|
||||
$user_id = current_user()['id'];
|
||||
$now = date('c');
|
||||
|
||||
db()->prepare(
|
||||
'INSERT INTO requests_stundennachweis
|
||||
(user_id, status, admin_note, monat, filename, original_name, created_at, updated_at)
|
||||
VALUES (?, \'pending\', \'\', ?, ?, ?, ?, ?)'
|
||||
)->execute([$user_id, $monat, $safe_name, $original_name, $now, $now]);
|
||||
|
||||
$config = require __DIR__ . '/../lib/config.php';
|
||||
smtp_send(
|
||||
$config,
|
||||
$config['mail_sabrina'],
|
||||
'Neuer Stundennachweis: ' . current_name(),
|
||||
"Ein Stundennachweis wurde hochgeladen.\n\n"
|
||||
. 'Mitarbeiter: ' . current_name() . "\n"
|
||||
. 'Monat: ' . $monat . "\n"
|
||||
. 'Datei: ' . $original_name . "\n\n"
|
||||
. "---\nGesendet über OMSORG Connect",
|
||||
['path' => $dir . $safe_name, 'name' => $original_name, 'mime' => $mime]
|
||||
);
|
||||
|
||||
redirect_ok($back);
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/upload.php';
|
||||
require_once __DIR__ . '/../lib/mail.php';
|
||||
require_login();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ../pages/urlaubsantrag.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$back = '../pages/urlaubsantrag.php';
|
||||
|
||||
verify_csrf();
|
||||
|
||||
$von = trim($_POST['von'] ?? '');
|
||||
$bis = trim($_POST['bis'] ?? '');
|
||||
$vertretung = trim($_POST['vertretung'] ?? '');
|
||||
$nachricht = trim($_POST['nachricht'] ?? '');
|
||||
|
||||
if ($von === '' || $bis === '') {
|
||||
redirect_error($back, 'Von und Bis sind Pflichtfelder.');
|
||||
}
|
||||
|
||||
if (!strtotime($von) || !strtotime($bis)) {
|
||||
redirect_error($back, 'Ungültiges Datum.');
|
||||
}
|
||||
|
||||
if (strtotime($von) > strtotime($bis)) {
|
||||
redirect_error($back, 'Das Startdatum muss vor dem Enddatum liegen.');
|
||||
}
|
||||
|
||||
$up = handle_upload($_FILES['datei'] ?? [], [
|
||||
'allowed' => UPLOAD_TYPES_DOCS,
|
||||
'dir' => dirname(__DIR__) . '/uploads/',
|
||||
'prefix' => 'urlaubsantrag',
|
||||
'required' => false,
|
||||
]);
|
||||
if (!$up['ok']) {
|
||||
redirect_error($back, $up['error']);
|
||||
}
|
||||
|
||||
$filename = $up['filename'] ?? '';
|
||||
$original_name = $up['original_name'] ?? '';
|
||||
$mime = $up['mime'] ?? null;
|
||||
|
||||
$user_id = current_user()['id'];
|
||||
$now = date('c');
|
||||
|
||||
db()->prepare(
|
||||
'INSERT INTO requests_urlaubsantrag (user_id, status, admin_note, created_at, updated_at, von, bis, vertretung, nachricht, filename, original_name)
|
||||
VALUES (?, \'pending\', \'\', ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
)->execute([$user_id, $now, $now, $von, $bis, $vertretung, $nachricht, $filename, $original_name]);
|
||||
|
||||
$config = require __DIR__ . '/../lib/config.php';
|
||||
$attach = ($filename !== '')
|
||||
? ['path' => dirname(__DIR__) . '/uploads/' . $filename, 'name' => $original_name, 'mime' => $mime]
|
||||
: [];
|
||||
smtp_send(
|
||||
$config,
|
||||
$config['mail_sabrina'],
|
||||
'Neuer Urlaubsantrag: ' . current_name(),
|
||||
"Ein Urlaubsantrag wurde eingereicht.\n\n"
|
||||
. 'Mitarbeiter: ' . current_name() . "\n"
|
||||
. 'Von: ' . $von . "\n"
|
||||
. 'Bis: ' . $bis . "\n"
|
||||
. 'Vertretung: ' . ($vertretung ?: '—') . "\n"
|
||||
. 'Nachricht: ' . ($nachricht ?: '—') . "\n\n"
|
||||
. "---\nGesendet über OMSORG Connect",
|
||||
$attach
|
||||
);
|
||||
|
||||
redirect_ok($back);
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/mail.php';
|
||||
require_login();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ../pages/werben.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$back = '../pages/werben.php';
|
||||
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$email = trim($_POST['email'] ?? '');
|
||||
$qualifikation = trim($_POST['qualifikation'] ?? '');
|
||||
$nachricht = trim($_POST['nachricht'] ?? '');
|
||||
|
||||
if ($name === '' || $email === '' || $qualifikation === '') {
|
||||
header('Location: ' . $back . '?error=' . urlencode('Name, E-Mail und Qualifikation sind Pflichtfelder.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
header('Location: ' . $back . '?error=' . urlencode('Bitte eine gültige E-Mail-Adresse eingeben.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$allowed_qual = ['1jahr', '3jahr'];
|
||||
if (!in_array($qualifikation, $allowed_qual, true)) {
|
||||
header('Location: ' . $back . '?error=' . urlencode('Ungültige Qualifikation.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$user_id = current_user()['id'];
|
||||
$now = date('c');
|
||||
|
||||
db()->prepare(
|
||||
'INSERT INTO requests_werben (user_id, status, admin_note, created_at, updated_at, name, email, qualifikation, nachricht)
|
||||
VALUES (?, \'pending\', \'\', ?, ?, ?, ?, ?, ?)'
|
||||
)->execute([$user_id, $now, $now, $name, $email, $qualifikation, $nachricht]);
|
||||
|
||||
$qual_labels = [
|
||||
'1jahr' => '1 Jährige Examinierte Pflegekraft',
|
||||
'3jahr' => '3 Jährige Examinierte Pflegekraft',
|
||||
];
|
||||
|
||||
$config = require __DIR__ . '/../lib/config.php';
|
||||
smtp_send(
|
||||
$config,
|
||||
$config['mail_info'],
|
||||
'Mitarbeiter werben: ' . current_name(),
|
||||
"Ein Mitarbeiter wurde empfohlen.\n\n"
|
||||
. 'Empfohlen von: ' . current_name() . "\n"
|
||||
. 'Name der empfohlenen Person: ' . $name . "\n"
|
||||
. 'E-Mail: ' . $email . "\n"
|
||||
. 'Art der Stelle: ' . ($qual_labels[$qualifikation] ?? $qualifikation) . "\n"
|
||||
. 'Nachricht: ' . ($nachricht ?: '—') . "\n\n"
|
||||
. "---\nGesendet über OMSORG Connect"
|
||||
);
|
||||
|
||||
header('Location: ' . $back . '?success=1');
|
||||
exit;
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/upload.php';
|
||||
require_login();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ../pages/settings.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$back = '../pages/settings.php';
|
||||
$user = current_user();
|
||||
$dir = dirname(__DIR__) . '/assets/avatars/';
|
||||
$config = _omsorgcore_config();
|
||||
$token = $_SESSION['omsorgcore_access_token'];
|
||||
$employeeId = $_SESSION['omsorgcore_profile']['employeeId'] ?? null;
|
||||
|
||||
function _set_avatar_file_name(array $config, string $token, string $employeeId, ?string $filename): bool {
|
||||
$current = omsorgcore_employees_get($config, $token, $employeeId);
|
||||
if (!$current['ok']) {
|
||||
return false;
|
||||
}
|
||||
$payload = $current['data'];
|
||||
$payload['avatarFileName'] = $filename;
|
||||
unset($payload['id']);
|
||||
|
||||
$result = omsorgcore_employees_update($config, $token, $employeeId, $payload);
|
||||
if ($result['ok']) {
|
||||
_refresh_profile();
|
||||
}
|
||||
return $result['ok'];
|
||||
}
|
||||
|
||||
if (!$employeeId) {
|
||||
redirect_error($back, 'Kein verknüpfter Mitarbeiter-Datensatz gefunden.');
|
||||
}
|
||||
|
||||
// Remove avatar
|
||||
if (!empty($_POST['remove_avatar'])) {
|
||||
if (!empty($user['avatar'])) {
|
||||
$old = $dir . basename($user['avatar']);
|
||||
if (is_file($old)) {
|
||||
unlink($old);
|
||||
}
|
||||
}
|
||||
_set_avatar_file_name($config, $token, $employeeId, null);
|
||||
redirect_ok($back, 'avatar_saved');
|
||||
}
|
||||
|
||||
$up = handle_upload($_FILES['avatar'] ?? [], [
|
||||
'allowed' => UPLOAD_TYPES_IMAGES,
|
||||
'dir' => $dir,
|
||||
'naming' => 'plain',
|
||||
'max_bytes' => 3 * 1024 * 1024,
|
||||
'messages' => [
|
||||
'missing' => 'Bitte ein Bild auswählen.',
|
||||
'upload' => 'Fehler beim Hochladen des Bildes.',
|
||||
'ext' => 'Nur Bilder erlaubt (JPG, PNG, GIF, WebP).',
|
||||
'mime' => 'Nur Bilder erlaubt (JPG, PNG, GIF, WebP).',
|
||||
'size' => 'Bild ist zu groß (max. 3 MB).',
|
||||
'move' => 'Fehler beim Speichern des Bildes.',
|
||||
],
|
||||
]);
|
||||
if (!$up['ok']) {
|
||||
redirect_error($back, $up['error']);
|
||||
}
|
||||
|
||||
$safe_name = $up['filename'];
|
||||
|
||||
// Delete old avatar after successful upload
|
||||
if (!empty($user['avatar'])) {
|
||||
$old = $dir . basename($user['avatar']);
|
||||
if (is_file($old)) {
|
||||
unlink($old);
|
||||
}
|
||||
}
|
||||
|
||||
_set_avatar_file_name($config, $token, $employeeId, $safe_name);
|
||||
|
||||
redirect_ok($back, 'avatar_saved');
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/upload.php';
|
||||
require_login();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: ../pages/dokumentenarchiv.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$back = '../pages/dokumentenarchiv.php';
|
||||
|
||||
$allowed_kategorien = ['Fortbildungsnachweis', 'Zeugnis', 'Bescheinigung', 'Sonstiges'];
|
||||
$kategorie = trim($_POST['kategorie'] ?? '');
|
||||
$beschreibung = trim($_POST['beschreibung'] ?? '');
|
||||
|
||||
if (!in_array($kategorie, $allowed_kategorien, true)) {
|
||||
redirect_error($back, 'Bitte eine Kategorie auswählen.');
|
||||
}
|
||||
|
||||
$filesize = $_FILES['datei']['size'] ?? 0;
|
||||
$up = handle_upload($_FILES['datei'] ?? [], [
|
||||
'allowed' => UPLOAD_TYPES_DOCS,
|
||||
'dir' => dirname(__DIR__) . '/uploads/',
|
||||
'prefix' => 'dokument',
|
||||
]);
|
||||
if (!$up['ok']) {
|
||||
redirect_error($back, $up['error']);
|
||||
}
|
||||
|
||||
$safe_name = $up['filename'];
|
||||
$original_name = $up['original_name'];
|
||||
|
||||
$user_id = current_user()['id'];
|
||||
$now = date('c');
|
||||
|
||||
db()->prepare(
|
||||
'INSERT INTO dokumente (user_id, kategorie, beschreibung, filename, original_name, filesize, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||
)->execute([$user_id, $kategorie, $beschreibung, $safe_name, $original_name, $filesize, $now]);
|
||||
|
||||
redirect_ok($back, 'ok');
|
||||
@@ -0,0 +1,381 @@
|
||||
/* ── Fonts ──────────────────────────────────────────────────────────── */
|
||||
@font-face{font-family:'Visby';src:url('font/visby/VisbyThin.otf') format('opentype');font-weight:100;font-style:normal;font-display:swap}
|
||||
@font-face{font-family:'Visby';src:url('font/visby/VisbyThin Italic.otf') format('opentype');font-weight:100;font-style:italic;font-display:swap}
|
||||
@font-face{font-family:'Visby';src:url('font/visby/VisbyLight.otf') format('opentype');font-weight:300;font-style:normal;font-display:swap}
|
||||
@font-face{font-family:'Visby';src:url('font/visby/VisbyLight Italic.otf') format('opentype');font-weight:300;font-style:italic;font-display:swap}
|
||||
@font-face{font-family:'Visby';src:url('font/visby/VisbyRegular.otf') format('opentype');font-weight:400;font-style:normal;font-display:swap}
|
||||
@font-face{font-family:'Visby';src:url('font/visby/VisbyItalic.otf') format('opentype');font-weight:400;font-style:italic;font-display:swap}
|
||||
@font-face{font-family:'Visby';src:url('font/visby/VisbyMedium.otf') format('opentype');font-weight:500;font-style:normal;font-display:swap}
|
||||
@font-face{font-family:'Visby';src:url('font/visby/VisbyMedium Italic.otf') format('opentype');font-weight:500;font-style:italic;font-display:swap}
|
||||
@font-face{font-family:'Visby';src:url('font/visby/VisbySemibold.otf') format('opentype');font-weight:600;font-style:normal;font-display:swap}
|
||||
@font-face{font-family:'Visby';src:url('font/visby/VisbySemibold Italic.otf') format('opentype');font-weight:600;font-style:italic;font-display:swap}
|
||||
@font-face{font-family:'Visby';src:url('font/visby/VisbyBold.otf') format('opentype');font-weight:700;font-style:normal;font-display:swap}
|
||||
@font-face{font-family:'Visby';src:url('font/visby/VisbyBold Italic.otf') format('opentype');font-weight:700;font-style:italic;font-display:swap}
|
||||
@font-face{font-family:'Visby';src:url('font/visby/VisbyExtrabold.otf') format('opentype');font-weight:800;font-style:normal;font-display:swap}
|
||||
@font-face{font-family:'Visby';src:url('font/visby/VisbyExtrabold Italic.otf') format('opentype');font-weight:800;font-style:italic;font-display:swap}
|
||||
@font-face{font-family:'Visby';src:url('font/visby/VisbyHeavy.otf') format('opentype');font-weight:900;font-style:normal;font-display:swap}
|
||||
@font-face{font-family:'Visby';src:url('font/visby/VisbyHeavy Italic.otf') format('opentype');font-weight:900;font-style:italic;font-display:swap}
|
||||
@font-face{font-family:'KindelSerif';src:url('font/kindelserif/KindelSerif_PersonalUse.otf') format('opentype');font-weight:400;font-style:normal;font-display:swap}
|
||||
|
||||
/* ── Design tokens (identical to main app) ─────────────────────────── */
|
||||
:root{
|
||||
--cyan:#10d7e6;--cyan2:#36f2ff;--dark:#031522;
|
||||
--glass:rgba(0,35,51,.74);--line:rgba(170,240,250,.42);
|
||||
--text:#f7ffff;--muted:#d7f6f8;
|
||||
--sidebar-w:240px;
|
||||
}
|
||||
|
||||
/* ── Reset & base ───────────────────────────────────────────────────── */
|
||||
*{box-sizing:border-box}
|
||||
html{scroll-behavior:smooth;background:#031522;overflow-x:hidden}
|
||||
body{
|
||||
margin:0;min-height:100vh;color:var(--text);
|
||||
font-family:'Visby',Arial,Helvetica,sans-serif;line-height:1.45;
|
||||
background:#031522 url('assets/omsorg-bg-full.png') center top/cover fixed no-repeat;
|
||||
overflow-x:hidden;
|
||||
}
|
||||
body::before{
|
||||
content:"";position:fixed;inset:0;z-index:-2;
|
||||
background:linear-gradient(180deg,rgba(0,9,23,.08),rgba(0,9,23,.28) 58%,rgba(0,9,23,.52));
|
||||
pointer-events:none;
|
||||
}
|
||||
body::after{
|
||||
content:"";position:fixed;inset:0;z-index:-1;opacity:.35;
|
||||
background-image:radial-gradient(circle,rgba(0,216,230,.75) 0 2px,transparent 3px);
|
||||
background-size:24px 24px;
|
||||
mask-image:radial-gradient(circle at 0% 34%,#000 0 0,transparent 18%),
|
||||
radial-gradient(circle at 100% 88%,#000 0 0,transparent 17%);
|
||||
pointer-events:none;
|
||||
}
|
||||
a{color:inherit;text-decoration:none}
|
||||
img{display:block;max-width:100%}
|
||||
|
||||
/* ── Glass card ─────────────────────────────────────────────────────── */
|
||||
.glass{
|
||||
border:1px solid var(--line);border-radius:22px;
|
||||
background:linear-gradient(145deg,rgba(0,35,51,.78),rgba(8,105,122,.28));
|
||||
box-shadow:0 22px 70px rgba(0,0,0,.28);
|
||||
backdrop-filter:blur(16px);
|
||||
}
|
||||
|
||||
/* ── Buttons ────────────────────────────────────────────────────────── */
|
||||
.btn{
|
||||
display:inline-flex;align-items:center;justify-content:center;
|
||||
gap:10px;border:1px solid rgba(255,255,255,.24);border-radius:13px;
|
||||
background:linear-gradient(135deg,#25e2e9,#079aaa);
|
||||
color:#fff;font-weight:900;padding:14px 18px;cursor:pointer;
|
||||
box-shadow:0 14px 38px rgba(0,215,230,.18);font:inherit;
|
||||
}
|
||||
|
||||
/* ── Form elements ──────────────────────────────────────────────────── */
|
||||
form{display:grid;gap:14px}
|
||||
label{display:grid;gap:7px;font-weight:800;color:#fff}
|
||||
input,select,textarea{
|
||||
width:100%;border:1px solid rgba(170,240,250,.36);border-radius:12px;
|
||||
padding:14px;background:rgba(2,11,22,.54);color:#fff;font:inherit;
|
||||
}
|
||||
|
||||
/* ── Alerts ─────────────────────────────────────────────────────────── */
|
||||
.error{
|
||||
padding:12px 14px;border:1px solid rgba(255,130,130,.45);
|
||||
border-radius:12px;background:rgba(255,0,0,.13);color:#fff;
|
||||
}
|
||||
.success{
|
||||
padding:12px 14px;border:1px solid rgba(54,242,255,.45);
|
||||
border-radius:12px;background:rgba(16,215,230,.1);color:#fff;
|
||||
}
|
||||
.hint{color:var(--muted);font-size:.9rem}
|
||||
.settings-section{margin-top:36px;padding-top:28px;border-top:1px solid rgba(170,240,250,.18)}
|
||||
.settings-avatar-preview{width:80px;height:80px;border-radius:50%;object-fit:cover;border:2px solid rgba(170,240,250,.3)}
|
||||
.settings-avatar-placeholder{width:80px;height:80px;border-radius:50%;display:grid;place-items:center;background:linear-gradient(135deg,#20e1ea,#078fa3);font-weight:900;font-size:2rem;color:#fff;flex-shrink:0}
|
||||
|
||||
/* ── Login page ─────────────────────────────────────────────────────── */
|
||||
.login-page{display:grid;place-items:center;min-height:100vh;padding:26px}
|
||||
.login-card{width:min(100%,450px);padding:30px}
|
||||
.nav-toggle{display:none}
|
||||
.nav-backdrop{display:none;position:fixed;inset:0;z-index:39;background:rgba(0,0,0,.55)}
|
||||
|
||||
/* ── Dashboard layout ───────────────────────────────────────────────── */
|
||||
.dashboard-layout{
|
||||
display:grid;
|
||||
grid-template-columns:1fr;
|
||||
padding-left:var(--sidebar-w);
|
||||
min-height:100vh;
|
||||
}
|
||||
|
||||
/* ── Sidebar ────────────────────────────────────────────────────────── */
|
||||
.sidebar{
|
||||
position:fixed;top:0;left:0;width:var(--sidebar-w);height:100vh;overflow-y:auto;
|
||||
display:flex;flex-direction:column;gap:0;
|
||||
border-radius:0;border-top:none;border-bottom:none;border-left:none;
|
||||
padding:0;
|
||||
}
|
||||
.sidebar-brand{
|
||||
padding:22px 20px 16px;
|
||||
border-bottom:1px solid rgba(170,240,250,.18);
|
||||
}
|
||||
.sidebar-brand img{width:160px;height:auto}
|
||||
.sidebar-app-name{
|
||||
display:block;margin-top:6px;
|
||||
color:var(--cyan2);text-transform:uppercase;
|
||||
letter-spacing:.16em;font-size:.74rem;font-weight:900;
|
||||
}
|
||||
.sidebar-user{
|
||||
display:grid;grid-template-columns:40px 1fr;gap:12px;
|
||||
align-items:center;padding:16px 20px;
|
||||
border-bottom:1px solid rgba(170,240,250,.12);
|
||||
}
|
||||
.sidebar-avatar{
|
||||
width:40px;height:40px;display:grid;place-items:center;
|
||||
border-radius:50%;background:linear-gradient(135deg,#20e1ea,#078fa3);
|
||||
font-weight:900;font-size:1.1rem;color:#fff;
|
||||
}
|
||||
.sidebar-avatar img{width:100%;height:100%;border-radius:50%;object-fit:cover}
|
||||
.sidebar-user b{display:block;font-size:.92rem}
|
||||
.sidebar-user small{display:block;color:var(--muted);font-size:.78rem;margin-top:2px}
|
||||
|
||||
.sidebar-nav{padding:16px 12px 0;flex:1}
|
||||
.sidebar-nav-label{
|
||||
margin:0 8px 8px;color:var(--cyan2);
|
||||
text-transform:uppercase;letter-spacing:.12em;font-size:.7rem;font-weight:900;
|
||||
}
|
||||
.sidebar-nav a{
|
||||
display:flex;align-items:center;gap:10px;
|
||||
border-radius:13px;padding:11px 12px;
|
||||
color:#ebfdff;font-weight:800;font-size:.92rem;
|
||||
border:1px solid transparent;margin-bottom:4px;
|
||||
transition:background .15s,border-color .15s;
|
||||
}
|
||||
.sidebar-nav a:hover{
|
||||
background:rgba(255,255,255,.075);
|
||||
border-color:rgba(170,240,250,.18);
|
||||
}
|
||||
.sidebar-nav a.active{
|
||||
background:linear-gradient(135deg,rgba(32,225,234,.22),rgba(7,143,163,.18));
|
||||
border-color:rgba(54,242,255,.45);
|
||||
color:#fff;
|
||||
}
|
||||
.nav-icon{font-size:1rem;opacity:.85}
|
||||
|
||||
.sidebar-footer{
|
||||
padding:16px 20px;
|
||||
border-top:1px solid rgba(170,240,250,.12);
|
||||
margin-top:auto;
|
||||
}
|
||||
.sidebar-logout{
|
||||
display:block;text-align:center;
|
||||
border:1px solid rgba(170,240,250,.28);
|
||||
border-radius:13px;padding:11px;
|
||||
background:rgba(255,255,255,.06);
|
||||
font-weight:900;font-size:.88rem;
|
||||
transition:background .15s;
|
||||
}
|
||||
.sidebar-logout:hover{background:rgba(255,255,255,.12)}
|
||||
|
||||
/* ── Main content ───────────────────────────────────────────────────── */
|
||||
.main-content{
|
||||
overflow-y:auto;padding:48px 40px;min-height:100vh;
|
||||
}
|
||||
.main-inner{max-width:860px}
|
||||
.main-inner:has(.page-split){max-width:none}
|
||||
.main-inner:has(.dp-grid){max-width:none}
|
||||
.main-title{
|
||||
font-family:'KindelSerif',Georgia,serif;font-size:2.8rem;
|
||||
line-height:1;margin:0 0 12px;color:#fff;
|
||||
}
|
||||
.main-title span{color:var(--cyan)}
|
||||
.main-sub{color:var(--muted);font-size:1.05rem;margin:0}
|
||||
|
||||
/* ── Requests ───────────────────────────────────────────────────────── */
|
||||
.badge{display:inline-block;padding:3px 10px;border-radius:8px;font-size:.82rem;font-weight:800}
|
||||
.badge-pending {background:rgba(255,200,0,.18);border:1px solid rgba(255,200,0,.6);color:#ffe566}
|
||||
.badge-accepted{background:rgba(16,215,230,.15);border:1px solid rgba(54,242,255,.55);color:var(--cyan2)}
|
||||
.badge-rejected{background:rgba(255,60,60,.15);border:1px solid rgba(255,100,100,.55);color:#ff8080}
|
||||
|
||||
.table-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}
|
||||
.monat-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||||
.requests-table{width:100%;border-collapse:collapse;margin-top:20px}
|
||||
.requests-table th{
|
||||
text-align:left;padding:10px 12px;color:var(--cyan2);
|
||||
font-size:.78rem;text-transform:uppercase;letter-spacing:.08em;
|
||||
border-bottom:1px solid rgba(170,240,250,.2);font-weight:900;
|
||||
}
|
||||
.requests-table td{padding:12px;border-bottom:1px solid rgba(170,240,250,.08);vertical-align:middle}
|
||||
.requests-table tr:hover td{background:rgba(255,255,255,.03)}
|
||||
|
||||
.type-tabs{display:flex;gap:8px;flex-wrap:wrap;margin:24px 0 0}
|
||||
.type-tab{
|
||||
padding:9px 18px;border-radius:11px;border:1px solid rgba(170,240,250,.22);
|
||||
background:rgba(255,255,255,.05);color:#ebfdff;font-weight:800;font-size:.9rem;
|
||||
cursor:pointer;transition:background .15s,border-color .15s;
|
||||
}
|
||||
.type-tab:hover{background:rgba(255,255,255,.1);border-color:rgba(170,240,250,.4)}
|
||||
.type-tab.active{
|
||||
background:linear-gradient(135deg,rgba(32,225,234,.22),rgba(7,143,163,.18));
|
||||
border-color:rgba(54,242,255,.45);color:#fff;
|
||||
}
|
||||
|
||||
.status-tabs{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:24px}
|
||||
.status-tab{
|
||||
padding:8px 16px;border-radius:10px;border:1px solid rgba(170,240,250,.2);
|
||||
background:rgba(255,255,255,.05);color:#ebfdff;font-weight:800;font-size:.88rem;
|
||||
transition:background .15s,border-color .15s;
|
||||
}
|
||||
.status-tab:hover{background:rgba(255,255,255,.1)}
|
||||
.status-tab.active{
|
||||
background:rgba(16,215,230,.18);border-color:rgba(54,242,255,.55);color:#fff;
|
||||
}
|
||||
|
||||
.detail-card{padding:28px;margin-top:24px}
|
||||
.detail-field{margin-bottom:16px}
|
||||
.detail-field-label{font-size:.8rem;text-transform:uppercase;letter-spacing:.1em;color:var(--cyan2);font-weight:900;margin-bottom:4px}
|
||||
.detail-field-value{color:#fff;font-size:.97rem}
|
||||
|
||||
.btn-danger{background:linear-gradient(135deg,#e94560,#a01a2e)}
|
||||
.btn-ghost{background:transparent;border:1px solid rgba(170,240,250,.35);color:#cff;box-shadow:none}
|
||||
|
||||
/* ── Split layout (list left, form right) ───────────────────────────── */
|
||||
.page-split{
|
||||
display:grid;
|
||||
grid-template-columns:1fr 400px;
|
||||
gap:32px;
|
||||
align-items:start;
|
||||
margin-top:28px;
|
||||
}
|
||||
.page-split-list h2{
|
||||
font-family:'KindelSerif',Georgia,serif;font-size:1.3rem;margin:0 0 16px;color:#fff;
|
||||
}
|
||||
.page-split-form h2{
|
||||
font-family:'KindelSerif',Georgia,serif;font-size:1.3rem;margin:0 0 20px;
|
||||
}
|
||||
|
||||
/* ── Responsive ─────────────────────────────────────────────────────── */
|
||||
@media(max-width:860px){
|
||||
/* Body-Offset für fixierten Header */
|
||||
body{padding-top:54px}
|
||||
.dashboard-layout{grid-template-columns:1fr;padding-left:0;width:100%;max-width:100%;overflow-x:hidden}
|
||||
|
||||
/* Sidebar = fixierter kompakter Header, 54px hoch */
|
||||
.sidebar{
|
||||
position:fixed;top:0;left:0;right:0;z-index:40;
|
||||
height:54px;overflow:hidden;
|
||||
flex-direction:column;gap:0;
|
||||
border-radius:0;border-right:none;
|
||||
border-bottom:1px solid var(--line);
|
||||
}
|
||||
|
||||
/* Brand-Zeile: Logo + Hamburger-Button */
|
||||
.sidebar-brand{
|
||||
display:flex;align-items:center;
|
||||
height:54px;flex-shrink:0;
|
||||
padding:0 14px;
|
||||
border-bottom:none;
|
||||
}
|
||||
.sidebar-brand img{width:80px}
|
||||
.sidebar-app-name{display:none}
|
||||
|
||||
/* Hamburger-Button */
|
||||
.nav-toggle{
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
margin-left:auto;flex-shrink:0;
|
||||
width:44px;height:44px;
|
||||
border:1px solid var(--line);border-radius:12px;
|
||||
background:rgba(255,255,255,.08);
|
||||
color:#fff;font-size:1.3rem;cursor:pointer;
|
||||
transition:background .15s;
|
||||
}
|
||||
.nav-toggle:hover{background:rgba(255,255,255,.15)}
|
||||
|
||||
/* Nutzerblock + Nav + Footer: standardmäßig versteckt */
|
||||
.sidebar-user{display:none}
|
||||
.sidebar-nav{display:none;padding:12px 12px 0;flex-direction:column}
|
||||
.sidebar-nav-label{display:block}
|
||||
.sidebar-footer{display:none}
|
||||
.nav-label{display:inline}
|
||||
|
||||
/* Geöffnet: Vollbild-Overlay */
|
||||
body.nav-open .sidebar{height:100vh;overflow-y:auto}
|
||||
body.nav-open .sidebar-user{display:grid}
|
||||
body.nav-open .sidebar-nav{display:flex}
|
||||
body.nav-open .sidebar-footer{display:block}
|
||||
body.nav-open .nav-backdrop{display:block}
|
||||
|
||||
.main-content{padding:20px 16px;min-height:auto;width:100%;max-width:100%;overflow-x:hidden}
|
||||
.main-inner{width:100%;max-width:100%}
|
||||
.main-title{font-size:1.9rem}
|
||||
.page-split{grid-template-columns:1fr}
|
||||
.page-split-list,.page-split-form{min-width:0;width:100%}
|
||||
.page-split-form{order:-1}
|
||||
.requests-table,.users-table{display:block;overflow-x:auto;-webkit-overflow-scrolling:touch}
|
||||
}
|
||||
|
||||
/* ── Downloads page ─────────────────────────────────────────────────── */
|
||||
.downloads-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:20px;margin-top:28px}
|
||||
.download-card{padding:24px;display:flex;flex-direction:column;gap:14px}
|
||||
.download-card-title{font-family:'KindelSerif',Georgia,serif;font-size:1.15rem;margin:0;color:#fff}
|
||||
.download-card-desc{color:var(--muted);font-size:.92rem;margin:0;flex:1}
|
||||
.download-btn{align-self:flex-start;padding:10px 18px;font-size:.9rem}
|
||||
|
||||
/* ── Downloads admin list ─────────────────────────────────────────────── */
|
||||
.dl-sort-item{display:flex;align-items:center;gap:12px;padding:12px 14px;border-radius:12px;border:1px solid rgba(170,240,250,.14);background:rgba(0,35,51,.44);margin-bottom:8px;cursor:grab;transition:background .15s}
|
||||
.dl-sort-item:active{cursor:grabbing}
|
||||
.dl-sort-item:hover{background:rgba(0,35,51,.7)}
|
||||
.dl-sort-item.drag-over{border-color:rgba(54,242,255,.55);background:rgba(16,215,230,.08)}
|
||||
.dl-drag-handle{color:var(--muted);font-size:1.2rem;user-select:none;flex-shrink:0;line-height:1}
|
||||
.dl-item-info{flex:1;min-width:0}
|
||||
.dl-item-title{font-weight:800;font-size:.93rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.dl-item-filename{color:var(--muted);font-size:.8rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:2px}
|
||||
|
||||
/* ── Dienstplan calendar ──────────────────────────────────────────────── */
|
||||
.dp-ort{margin:0 0 20px;font-size:.95rem;color:var(--muted)}
|
||||
.dp-ort strong{color:#ebfdff}
|
||||
.dp-nav{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px;flex-wrap:wrap;gap:12px}
|
||||
.dp-nav-title{font-family:'KindelSerif',Georgia,serif;font-size:1.6rem;color:#fff;margin:0}
|
||||
.dp-nav-btn{padding:8px 18px;border-radius:10px;border:1px solid rgba(170,240,250,.3);background:rgba(255,255,255,.06);color:#ebfdff;font-weight:800;font-size:.88rem;cursor:pointer;text-decoration:none;transition:background .15s}
|
||||
.dp-nav-btn:hover{background:rgba(255,255,255,.12)}
|
||||
.dp-save-bar{display:flex;justify-content:flex-end;align-items:center;gap:16px;margin-bottom:20px}
|
||||
.dp-grid-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch}
|
||||
.dp-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:6px;min-width:480px}
|
||||
.dp-weekday{text-align:center;padding:8px 4px;color:var(--cyan2);font-size:.72rem;text-transform:uppercase;letter-spacing:.1em;font-weight:900}
|
||||
.dp-cell{min-height:80px;border-radius:12px;padding:8px;border:1px solid rgba(170,240,250,.14);background:rgba(0,35,51,.44);display:flex;flex-direction:column;gap:6px}
|
||||
.dp-cell-empty{background:transparent;border-color:transparent;min-height:0}
|
||||
.dp-cell-today{border-color:rgba(54,242,255,.55);background:rgba(16,215,230,.08)}
|
||||
.dp-cell-frueh{background:rgba(54,242,100,.1);border-color:rgba(54,242,100,.38)}
|
||||
.dp-cell-spaet{background:rgba(255,180,50,.1);border-color:rgba(255,180,50,.38)}
|
||||
.dp-cell-nacht{background:rgba(130,80,255,.14);border-color:rgba(130,80,255,.42)}
|
||||
.dp-day-num{font-size:.78rem;font-weight:900;color:var(--muted);line-height:1}
|
||||
.dp-cell-today .dp-day-num{color:var(--cyan2)}
|
||||
.dp-select{width:100%;border:1px solid rgba(170,240,250,.28);border-radius:8px;padding:5px 6px;background:rgba(2,11,22,.62);color:#fff;font:inherit;font-size:.8rem;cursor:pointer}
|
||||
.dp-badge{display:block;padding:3px 8px;border-radius:7px;font-size:.75rem;font-weight:900;text-align:center;word-break:break-word}
|
||||
.dp-badge-frueh{background:rgba(54,242,100,.15);border:1px solid rgba(54,242,100,.5);color:#6eff9a}
|
||||
.dp-badge-spaet{background:rgba(255,180,50,.15);border:1px solid rgba(255,180,50,.5);color:#ffc84a}
|
||||
.dp-badge-nacht{background:rgba(130,80,255,.18);border:1px solid rgba(130,80,255,.5);color:#b89cff}
|
||||
.dp-overlay-urlaub{background:rgba(16,215,230,.18);border:1px solid rgba(54,242,255,.5);color:var(--cyan2)}
|
||||
.dp-overlay-abwesenheit{background:rgba(255,200,0,.15);border:1px solid rgba(255,200,0,.5);color:#ffe566}
|
||||
.dp-user-select-wrap{margin-bottom:28px}
|
||||
.dp-user-select-wrap select{max-width:320px;border-radius:12px;padding:12px;font-weight:800}
|
||||
.dp-free{font-size:.75rem;color:rgba(215,246,248,.35)}
|
||||
@media(max-width:680px){
|
||||
.dp-grid{gap:3px}
|
||||
.dp-cell{min-height:60px;padding:5px}
|
||||
.dp-weekday{font-size:.62rem}
|
||||
.dp-day-num{font-size:.7rem}
|
||||
.dp-select{font-size:.72rem;padding:4px}
|
||||
}
|
||||
@media(max-width:480px){
|
||||
.monat-grid{grid-template-columns:1fr}
|
||||
.main-content{padding:18px 14px}
|
||||
.main-title{font-size:1.6rem}
|
||||
.btn-sm{padding:11px 13px}
|
||||
.detail-card{padding:16px}
|
||||
.modal-box{padding:20px 16px}
|
||||
.admin-tabs{flex-wrap:wrap;gap:6px;margin-bottom:20px}
|
||||
.admin-tab{padding:8px 14px;font-size:.82rem}
|
||||
.type-tab,.status-tab{padding:7px 12px;font-size:.82rem}
|
||||
.dp-cell{min-height:44px;padding:4px 3px}
|
||||
.dp-weekday{font-size:.58rem;padding:6px 2px}
|
||||
.dp-badge{font-size:.65rem;padding:2px 4px}
|
||||
.dp-select{font-size:.68rem;padding:3px 2px}
|
||||
.requests-table,.users-table{display:block;overflow-x:auto;-webkit-overflow-scrolling:touch}
|
||||
.login-card{padding:22px 18px}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
@@ -0,0 +1,38 @@
|
||||
1001Fonts Free For Personal Use License (FFP)
|
||||
|
||||
Preamble
|
||||
In this license, 'KindelSerif' refers to the given .zip file, which may contain one or numerous fonts. These fonts can be of any type (.ttf, .otf, ...) and together they form a 'font family' or in short a 'typeface'.
|
||||
|
||||
1. Copyright
|
||||
KindelSerif is the intellectual property of its respective author, provided it is original, and is protected by copyright laws in many parts of the world.
|
||||
|
||||
2. Personal Use
|
||||
KindelSerif may be downloaded and used free of charge for personal use, as long as the usage is not racist or illegal. Personal use refers to all usage that does not generate financial income in a business manner, for instance:
|
||||
|
||||
- personal scrapbooking for yourself
|
||||
- recreational websites and blogs for friends and family
|
||||
- prints such as flyers, posters, t-shirts for churches, charities, and non-profit organizations
|
||||
|
||||
3. Commercial Use
|
||||
Commercial use is not allowed without prior written permission from the respective author. Please contact the author to ask for commercial licensing. Commercial use refers to usage in a business environment, including:
|
||||
|
||||
- business cards, logos, advertising, websites, mobile apps for companies
|
||||
- t-shirts, books, apparel that will be sold for money
|
||||
- flyers, posters for events that charge admission
|
||||
- freelance graphic design work
|
||||
- anything that will generate direct or indirect income
|
||||
|
||||
4. Modification
|
||||
KindelSerif may not be modified, altered, adapted or built upon without written permission by its respective author. This pertains all files within the downloadable font zip-file.
|
||||
|
||||
5. Conversion
|
||||
KindelSerif may be converted to other formats such as WOFF, SVG or EOT webfonts, as long as the font is not modified in any other way, such as changing names or altering individual glyphs.
|
||||
|
||||
6. Distribution
|
||||
While KindelSerif may freely be copied and passed along to other individuals for private use as its original downloadable zip-file, it may not be sold or published without written permission by its respective author.
|
||||
|
||||
7. Embedding
|
||||
KindelSerif may be embedded into an application such as a web- or mobile app, as long as the application is of personal use and does not distribute KindelSerif, such as offering it as a download.
|
||||
|
||||
8. Disclaimer
|
||||
KindelSerif is offered 'as is' without any warranty. 1001fonts.com and the respective author of KindelSerif shall not be liable for any damage derived from using this typeface. By using KindelSerif you agree to the terms of this license.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/lib/auth.php';
|
||||
|
||||
if (is_logged_in()) {
|
||||
header('Location: pages/dashboard.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
verify_csrf();
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if ($username !== '' && $password !== '') {
|
||||
$result = omsorgcore_login(_omsorgcore_config(), $username, $password);
|
||||
|
||||
if ($result['ok']) {
|
||||
session_regenerate_id(true);
|
||||
_apply_token_pair($result['data']);
|
||||
_refresh_profile();
|
||||
|
||||
header('Location: ' . (must_change_password() ? 'pages/change-password.php' : 'pages/dashboard.php'));
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($result['status'] === 429) {
|
||||
$error = 'Zu viele Fehlversuche. Bitte warte ca. 10 Minuten.';
|
||||
} elseif ($result['status'] === 0) {
|
||||
$error = 'Server nicht erreichbar. Bitte später erneut versuchen.';
|
||||
} else {
|
||||
$error = 'Benutzername oder Passwort falsch.';
|
||||
}
|
||||
} else {
|
||||
$error = 'Benutzername oder Passwort falsch.';
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>OMSORG Felix – Login</title>
|
||||
<link rel="stylesheet" href="app.css">
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
<meta name="theme-color" content="#1a3a5c">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="login-page">
|
||||
<div class="login-card glass">
|
||||
<div class="brand" style="text-align:center">
|
||||
<img src="assets/omsorg-wordmark-new.png" alt="OMSORG" style="width:210px;margin:0 auto 22px">
|
||||
</div>
|
||||
|
||||
<h2 style="font-family:'KindelSerif',Georgia,serif;font-size:1.7rem;margin:0 0 6px">Mitarbeiter-App</h2>
|
||||
<p class="hint" style="margin:0 0 22px">Melde dich mit deinem Benutzerkonto an.</p>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="error" style="margin-bottom:16px"><?= e($error) ?></div>
|
||||
<?php elseif (isset($_GET['pwchanged'])): ?>
|
||||
<div class="success" style="margin-bottom:16px">Passwort geändert. Bitte melde dich mit deinem neuen Passwort an.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post">
|
||||
<?= csrf_field() ?>
|
||||
<label>
|
||||
Benutzername
|
||||
<input type="text" name="username" autocomplete="username" required autofocus
|
||||
value="<?= e($_POST['username'] ?? '') ?>">
|
||||
</label>
|
||||
<label>
|
||||
Passwort
|
||||
<input type="password" name="password" autocomplete="current-password" required>
|
||||
</label>
|
||||
<button type="submit" class="btn" style="width:100%;margin-top:6px">Anmelden</button>
|
||||
</form>
|
||||
|
||||
<p class="hint" style="text-align:center;margin-top:14px">
|
||||
<a href="pages/forgot-password.php">Passwort vergessen?</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) navigator.serviceWorker.register('service-worker.js');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Require all denied
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
// Auth (Login, Passwort, Rechte) läuft jetzt vollständig über omsorgCore (siehe lib/omsorgCoreClient.php) -
|
||||
// kein lokaler Credential-/Rollen-Check mehr, `password_hash` wird beim Cutover gelöscht (siehe
|
||||
// migrate-users-to-core.php).
|
||||
//
|
||||
// Die lokale `users`-Tabelle bleibt trotzdem bestehen, aber nur noch als **Lesecache** für Name/
|
||||
// E-Mail/Telefon/Avatar/Aktiv-Status - Dutzende bestehende Seiten (admin.php, dienstplan.php, ...)
|
||||
// joinen darauf für Anzeige/Filter, das ist bewusst nicht Teil dieser Migrationsrunde (siehe Plan,
|
||||
// Abschnitt "Out of Scope"). Der Cache wird bei jedem Login/Session-Refresh aus omsorgCore
|
||||
// nachgezogen (_refresh_profile()) und bei jeder Admin-Aktion sofort mitgeschrieben - omsorgCore
|
||||
// bleibt die Quelle der Wahrheit, die lokale Zeile ist nur eine synchron gehaltene Kopie plus die
|
||||
// stabile Integer-`id`, die die restlichen (noch nicht migrierten) Tabellen als Fremdschlüssel nutzen.
|
||||
require_once __DIR__ . '/db.php';
|
||||
require_once __DIR__ . '/omsorgCoreClient.php';
|
||||
|
||||
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|
||||
|| (($_SERVER['SERVER_PORT'] ?? null) == 443);
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 0,
|
||||
'path' => '/',
|
||||
'httponly' => true,
|
||||
'secure' => $secure,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
session_name('OMSORG_FELIX_APP');
|
||||
session_start();
|
||||
|
||||
function _omsorgcore_config(): array {
|
||||
static $config = null;
|
||||
if ($config === null) {
|
||||
$config = require __DIR__ . '/config.php';
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
|
||||
// Stellt sicher, dass $_SESSION['omsorgcore_access_token'] gültig ist - erneuert bei Bedarf still
|
||||
// über den Refresh-Token (Server-seitig, der Browser bekommt nie einen Token zu sehen).
|
||||
function _ensure_fresh_token(): bool {
|
||||
if (empty($_SESSION['omsorgcore_access_token']) || empty($_SESSION['omsorgcore_refresh_token'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$expiresAt = $_SESSION['omsorgcore_access_expires_at'] ?? null;
|
||||
$stillValid = $expiresAt && strtotime($expiresAt) > time() + 30; // 30s Puffer
|
||||
if ($stillValid) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$result = omsorgcore_refresh(_omsorgcore_config(), $_SESSION['omsorgcore_refresh_token']);
|
||||
if (!$result['ok']) {
|
||||
_clear_omsorgcore_session();
|
||||
return false;
|
||||
}
|
||||
|
||||
_apply_token_pair($result['data']);
|
||||
return true;
|
||||
}
|
||||
|
||||
function _apply_token_pair(array $data): void {
|
||||
$_SESSION['omsorgcore_access_token'] = $data['accessToken'];
|
||||
$_SESSION['omsorgcore_refresh_token'] = $data['refreshToken'];
|
||||
$_SESSION['omsorgcore_access_expires_at'] = $data['expiresAt'];
|
||||
$_SESSION['omsorgcore_must_change_password'] = !empty($data['mustChangePassword']);
|
||||
}
|
||||
|
||||
function _clear_omsorgcore_session(): void {
|
||||
unset(
|
||||
$_SESSION['omsorgcore_access_token'],
|
||||
$_SESSION['omsorgcore_refresh_token'],
|
||||
$_SESSION['omsorgcore_access_expires_at'],
|
||||
$_SESSION['omsorgcore_must_change_password'],
|
||||
$_SESSION['omsorgcore_profile'],
|
||||
$_SESSION['felix_local_user_id']
|
||||
);
|
||||
}
|
||||
|
||||
// Schreibt Name/E-Mail/Telefon/Avatar/Aktiv-Status in die lokale users-Zeile (per omsorgcore_user_id
|
||||
// gefunden, siehe migrate-users-to-core.php) - hält den Lesecache für die noch nicht migrierten
|
||||
// Seiten aktuell. Legt die Zeile an, falls sie fehlt (z. B. gerade erst über admin.php angelegter User).
|
||||
function _sync_local_user_cache(string $omsorgcoreUserId, string $username, array $profile): int {
|
||||
$name = trim(($profile['firstName'] ?? '') . ' ' . ($profile['lastName'] ?? '')) ?: $username;
|
||||
|
||||
$stmt = db()->prepare('SELECT id FROM users WHERE omsorgcore_user_id = ?');
|
||||
$stmt->execute([$omsorgcoreUserId]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
$role = $profile['role'] ?? '';
|
||||
|
||||
if ($row) {
|
||||
db()->prepare('UPDATE users SET username=?, name=?, role=?, email=?, telefon=?, avatar=?, active=1 WHERE id=?')
|
||||
->execute([$username, $name, $role, $profile['email'] ?? '', $profile['phoneNumber'] ?? '', $profile['avatarFileName'] ?? null, $row['id']]);
|
||||
return (int)$row['id'];
|
||||
}
|
||||
|
||||
db()->prepare(
|
||||
'INSERT INTO users (username, name, role, password_hash, active, created_at, email, telefon, avatar, omsorgcore_user_id, omsorgcore_employee_id)
|
||||
VALUES (?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?)'
|
||||
)->execute([
|
||||
$username, $name, $role, '', date('c'),
|
||||
$profile['email'] ?? '', $profile['phoneNumber'] ?? '', $profile['avatarFileName'] ?? null,
|
||||
$omsorgcoreUserId, $profile['employeeId'] ?? null,
|
||||
]);
|
||||
return (int)db()->lastInsertId();
|
||||
}
|
||||
|
||||
// Lädt/cached /api/auth/me in der Session (Username/Rolle/Rechte/Employee-Basisdaten) und hält den
|
||||
// lokalen Lesecache (siehe oben) aktuell. Wird nach Login und bei jedem is_logged_in()-Aufruf ohne
|
||||
// gültigen Cache erneuert.
|
||||
function _refresh_profile(): bool {
|
||||
$result = omsorgcore_me(_omsorgcore_config(), $_SESSION['omsorgcore_access_token']);
|
||||
if (!$result['ok']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$_SESSION['omsorgcore_profile'] = $result['data'];
|
||||
$_SESSION['felix_local_user_id'] = _sync_local_user_cache(
|
||||
$result['data']['userId'], $result['data']['username'], $result['data']
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function is_logged_in(): bool {
|
||||
if (!_ensure_fresh_token()) {
|
||||
return false;
|
||||
}
|
||||
if (empty($_SESSION['omsorgcore_profile'])) {
|
||||
return _refresh_profile();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function must_change_password(): bool {
|
||||
return !empty($_SESSION['omsorgcore_must_change_password']);
|
||||
}
|
||||
|
||||
function require_login(): void {
|
||||
if (!is_logged_in()) {
|
||||
header('Location: ../index.php');
|
||||
exit;
|
||||
}
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST)) {
|
||||
verify_csrf();
|
||||
}
|
||||
if (must_change_password() && basename($_SERVER['SCRIPT_NAME']) !== 'change-password.php') {
|
||||
header('Location: change-password.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
function csrf_token(): string {
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
return $_SESSION['csrf_token'];
|
||||
}
|
||||
|
||||
function csrf_field(): string {
|
||||
return '<input type="hidden" name="csrf_token" value="' . csrf_token() . '">';
|
||||
}
|
||||
|
||||
function verify_csrf(): void {
|
||||
$token = $_POST['csrf_token'] ?? '';
|
||||
if (!hash_equals(csrf_token(), $token)) {
|
||||
http_response_code(403);
|
||||
exit('Ungültige Anfrage.');
|
||||
}
|
||||
}
|
||||
|
||||
function require_admin(): void {
|
||||
require_login();
|
||||
if (!is_admin()) {
|
||||
header('Location: ../pages/dashboard.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
function is_admin(): bool {
|
||||
return ($_SESSION['omsorgcore_profile']['role'] ?? null) === 'Administrator';
|
||||
}
|
||||
|
||||
// Unverändert wie vorher: current_user()['id']/['name']/['email']/['telefon']/['avatar'] kommen aus
|
||||
// der lokalen users-Zeile (jetzt ein Lesecache, siehe oben) - bestehende Seiten laufen unverändert.
|
||||
function current_user(): array|false {
|
||||
if (empty($_SESSION['felix_local_user_id'])) {
|
||||
return false;
|
||||
}
|
||||
$stmt = db()->prepare('SELECT * FROM users WHERE id = ?');
|
||||
$stmt->execute([$_SESSION['felix_local_user_id']]);
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) {
|
||||
return false;
|
||||
}
|
||||
$row['role'] = $_SESSION['omsorgcore_profile']['role'] ?? $row['role'];
|
||||
return $row;
|
||||
}
|
||||
|
||||
function current_username(): string {
|
||||
return $_SESSION['omsorgcore_profile']['username'] ?? '';
|
||||
}
|
||||
|
||||
function current_name(): string {
|
||||
$user = current_user();
|
||||
return $user ? $user['name'] : '';
|
||||
}
|
||||
|
||||
function e(string $s): string {
|
||||
return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
// Vorlage: nach config.secret.php kopieren und mit echten Werten füllen.
|
||||
// config.secret.php niemals ins Repo/Backup geben.
|
||||
return [
|
||||
'db_user' => 'DEIN_DB_BENUTZER',
|
||||
'db_password' => 'DEIN_DB_PASSWORT',
|
||||
'smtp_password' => 'DEIN_SMTP_PASSWORT',
|
||||
// Produktiv auf die echte omsorgCore-URL überschreiben (Default in config.php ist localhost, nur für lokale Entwicklung):
|
||||
// 'omsorg_core_url' => 'https://core.omsorg-pflegedienste.de',
|
||||
];
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
function db(): PDO {
|
||||
static $pdo = null;
|
||||
if ($pdo === null) {
|
||||
$c = require __DIR__ . '/config.php';
|
||||
$pdo = new PDO(
|
||||
$c['db_dsn'],
|
||||
$c['db_user'],
|
||||
$c['db_password'],
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]
|
||||
);
|
||||
if ($c['db_driver'] === 'sqlite') {
|
||||
$pdo->exec('PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;');
|
||||
}
|
||||
_run_migrations($pdo);
|
||||
}
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
function _table_exists(PDO $pdo, string $table, string $driver): bool {
|
||||
if ($driver === 'sqlite') {
|
||||
return (bool) $pdo->query(
|
||||
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=" . $pdo->quote($table)
|
||||
)->fetchColumn();
|
||||
}
|
||||
return (bool) $pdo->query(
|
||||
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name=" . $pdo->quote($table)
|
||||
)->fetchColumn();
|
||||
}
|
||||
|
||||
function _run_migrations(PDO $pdo): void {
|
||||
$c = require __DIR__ . '/config.php';
|
||||
$driver = $c['db_driver'];
|
||||
|
||||
$migrationTableExisted = _table_exists($pdo, 'schema_migrations', $driver);
|
||||
|
||||
$pdo->exec("
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||
applied_at VARCHAR(32) NOT NULL
|
||||
)
|
||||
");
|
||||
|
||||
$files = glob(__DIR__ . '/../migrations/*.php');
|
||||
if (!$files) return;
|
||||
sort($files);
|
||||
|
||||
// Existing DB without migration table: stamp all known migrations as applied.
|
||||
if (!$migrationTableExisted && _table_exists($pdo, 'users', $driver)) {
|
||||
$insertIgnore = $driver === 'sqlite' ? 'INSERT OR IGNORE' : 'INSERT IGNORE';
|
||||
$stmt = $pdo->prepare(
|
||||
"$insertIgnore INTO schema_migrations (version, applied_at) VALUES (?, ?)"
|
||||
);
|
||||
foreach ($files as $file) {
|
||||
$stmt->execute([basename($file, '.php'), date('c')]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$applied = $pdo->query('SELECT version FROM schema_migrations')
|
||||
->fetchAll(PDO::FETCH_COLUMN);
|
||||
$applied = array_flip($applied);
|
||||
|
||||
$useTransactions = ($driver === 'sqlite');
|
||||
|
||||
foreach ($files as $file) {
|
||||
$version = basename($file, '.php');
|
||||
if (isset($applied[$version])) continue;
|
||||
|
||||
$migration = require $file;
|
||||
if ($useTransactions) $pdo->beginTransaction();
|
||||
try {
|
||||
foreach ((array) $migration['up'] as $sql) {
|
||||
$pdo->exec($sql);
|
||||
}
|
||||
$pdo->prepare(
|
||||
'INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)'
|
||||
)->execute([$version, date('c')]);
|
||||
if ($useTransactions) $pdo->commit();
|
||||
} catch (Throwable $e) {
|
||||
if ($useTransactions && $pdo->inTransaction()) $pdo->rollBack();
|
||||
throw new RuntimeException(
|
||||
"Migration {$version} failed: " . $e->getMessage(), 0, $e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
function layout_start(string $title, string $current_page, string $extra_head = ''): void {
|
||||
$name = current_name();
|
||||
$admin = is_admin();
|
||||
$user = current_user();
|
||||
$nav = [
|
||||
'dashboard' => ['href' => 'dashboard.php', 'icon' => '⊞', 'label' => 'Dashboard'],
|
||||
'stundennachweis' => ['href' => 'stundennachweis.php', 'icon' => '🕐', 'label' => 'Stundennachweis'],
|
||||
'urlaubsantrag' => ['href' => 'urlaubsantrag.php', 'icon' => '🌴', 'label' => 'Urlaubsantrag'],
|
||||
'dienstplan' => ['href' => 'dienstplan.php', 'icon' => '📋', 'label' => 'Dienstplan'],
|
||||
'downloads' => ['href' => 'downloads.php', 'icon' => '📥', 'label' => 'Downloads'],
|
||||
'einsatzanweisung' => ['href' => 'einsatzanweisung.php', 'icon' => '📄', 'label' => 'Einsatzanweisung'],
|
||||
'benefitsantrag' => ['href' => 'benefitsantrag.php', 'icon' => '🎁', 'label' => 'Benefitsantrag'],
|
||||
'abwesenheitsantrag' => ['href' => 'abwesenheitsantrag.php', 'icon' => '📅', 'label' => 'Abwesenheitsantrag'],
|
||||
'fortbildungsantrag' => ['href' => 'fortbildungsantrag.php', 'icon' => '🎓', 'label' => 'Fortbildungsantrag'],
|
||||
'dokumentenarchiv' => ['href' => 'dokumentenarchiv.php', 'icon' => '🗂', 'label' => 'Dokumentenarchiv'],
|
||||
'einsatzbewertung' => ['href' => 'einsatzbewertung.php', 'icon' => '⭐', 'label' => 'Einsatzbewertung'],
|
||||
'werben' => ['href' => 'werben.php', 'icon' => '🤝', 'label' => 'Mitarbeiter werben'],
|
||||
'settings' => ['href' => 'settings.php', 'icon' => '⚙', 'label' => 'Einstellungen'],
|
||||
];
|
||||
if ($admin) {
|
||||
$nav['admin'] = ['href' => 'admin.php', 'icon' => '🛡', 'label' => 'Verwaltung'];
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title><?= e($title) ?></title>
|
||||
<link rel="stylesheet" href="../app.css">
|
||||
<link rel="manifest" href="../manifest.webmanifest">
|
||||
<meta name="theme-color" content="#1a3a5c">
|
||||
<meta name="csrf-token" content="<?= csrf_token() ?>">
|
||||
<?= $extra_head ?>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="dashboard-layout">
|
||||
|
||||
<aside class="sidebar glass">
|
||||
<div class="sidebar-brand">
|
||||
<img src="../assets/omsorg-wordmark-new.png" alt="OMSORG">
|
||||
<span class="sidebar-app-name">Mitarbeiter-App</span>
|
||||
<button class="nav-toggle" type="button" aria-label="Menü öffnen" aria-expanded="false">☰</button>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-user">
|
||||
<div class="sidebar-avatar">
|
||||
<?php if (!empty($user['avatar'])): ?>
|
||||
<img src="../assets/avatars/<?= e($user['avatar']) ?>" alt="<?= e($name) ?>">
|
||||
<?php else: ?>
|
||||
<?= e(mb_substr($name, 0, 1)) ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div>
|
||||
<b><?= e($name) ?></b>
|
||||
<small><?= $admin ? 'Admin' : 'Mitarbeiter' ?></small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<p class="sidebar-nav-label">Navigation</p>
|
||||
<?php foreach ($nav as $key => $item): ?>
|
||||
<a href="<?= e($item['href']) ?>"<?= $current_page === $key ? ' class="active"' : '' ?>>
|
||||
<span class="nav-icon"><?= $item['icon'] ?></span><span class="nav-label"> <?= e($item['label']) ?></span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<a href="../actions/logout.php" class="sidebar-logout">Abmelden</a>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="nav-backdrop" onclick="closeNav()"></div>
|
||||
|
||||
<main class="main-content">
|
||||
<div class="main-inner">
|
||||
<?php
|
||||
}
|
||||
|
||||
function layout_end(): void {
|
||||
?>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) navigator.serviceWorker.register('../service-worker.js');
|
||||
function closeNav(){
|
||||
document.body.classList.remove('nav-open');
|
||||
const btn=document.querySelector('.nav-toggle');
|
||||
if(btn){btn.textContent='☰';btn.setAttribute('aria-expanded','false');}
|
||||
}
|
||||
const navToggle=document.querySelector('.nav-toggle');
|
||||
if(navToggle){
|
||||
navToggle.addEventListener('click',function(){
|
||||
const open=document.body.classList.toggle('nav-open');
|
||||
this.textContent=open?'✕':'☰';
|
||||
this.setAttribute('aria-expanded',open);
|
||||
});
|
||||
}
|
||||
document.addEventListener('keydown',e=>{if(e.key==='Escape')closeNav();});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
/**
|
||||
* Sends an email with an optional file attachment via SMTP.
|
||||
*
|
||||
* @param array $cfg Config array with smtp_host, smtp_port, smtp_user, smtp_password, mail_from
|
||||
* @param string $to Recipient address
|
||||
* @param string $subject Subject (plain UTF-8, will be encoded)
|
||||
* @param string $body Plain-text body
|
||||
* @param array $attach Optional: ['path' => '/abs/path', 'name' => 'display.pdf', 'mime' => 'application/pdf']
|
||||
* @return array ['ok' => bool, 'log' => string]
|
||||
*/
|
||||
function smtp_send(array $cfg, string $to, string $subject, string $body, array $attach = []): array
|
||||
{
|
||||
$log = '';
|
||||
|
||||
$read = function ($conn) use (&$log) {
|
||||
$line = '';
|
||||
while ($chunk = fgets($conn, 512)) {
|
||||
$log .= '<< ' . $chunk;
|
||||
$line = $chunk;
|
||||
if ($chunk[3] === ' ') break; // last line of multi-line response
|
||||
}
|
||||
return $line;
|
||||
};
|
||||
|
||||
$write = function ($conn, string $cmd) use (&$log) {
|
||||
$log .= '>> ' . $cmd;
|
||||
fwrite($conn, $cmd);
|
||||
};
|
||||
|
||||
$code = fn(string $line) => (int)substr($line, 0, 3);
|
||||
|
||||
try {
|
||||
$host = trim($cfg['smtp_host']);
|
||||
$port = (int)$cfg['smtp_port'];
|
||||
$ssl = ($port === 465);
|
||||
|
||||
$conn = @fsockopen(($ssl ? 'ssl://' : '') . $host, $port, $errno, $errstr, 10);
|
||||
if (!$conn) {
|
||||
return ['ok' => false, 'log' => "Connect failed: $errstr ($errno)"];
|
||||
}
|
||||
stream_set_timeout($conn, 10);
|
||||
|
||||
$line = $read($conn);
|
||||
if ($code($line) !== 220) throw new RuntimeException("Unexpected greeting: $line");
|
||||
|
||||
$write($conn, "EHLO omsorg-connect\r\n");
|
||||
$capabilities = '';
|
||||
while ($chunk = fgets($conn, 512)) {
|
||||
$log .= '<< ' . $chunk;
|
||||
$capabilities .= $chunk;
|
||||
if ($chunk[3] === ' ') break;
|
||||
}
|
||||
if ($code($capabilities) !== 250) throw new RuntimeException("EHLO failed");
|
||||
|
||||
if (!$ssl && str_contains($capabilities, 'STARTTLS')) {
|
||||
$write($conn, "STARTTLS\r\n");
|
||||
$line = $read($conn);
|
||||
if ($code($line) !== 220) throw new RuntimeException("STARTTLS failed: $line");
|
||||
if (!stream_socket_enable_crypto($conn, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
|
||||
throw new RuntimeException("TLS handshake failed");
|
||||
}
|
||||
$write($conn, "EHLO omsorg-connect\r\n");
|
||||
while ($chunk = fgets($conn, 512)) {
|
||||
$log .= '<< ' . $chunk;
|
||||
if ($chunk[3] === ' ') break;
|
||||
}
|
||||
}
|
||||
|
||||
$write($conn, "AUTH LOGIN\r\n");
|
||||
$line = $read($conn);
|
||||
if ($code($line) !== 334) throw new RuntimeException("AUTH LOGIN failed: $line");
|
||||
|
||||
$write($conn, base64_encode($cfg['smtp_user']) . "\r\n");
|
||||
$line = $read($conn);
|
||||
if ($code($line) !== 334) throw new RuntimeException("Username rejected: $line");
|
||||
|
||||
$write($conn, base64_encode($cfg['smtp_password']) . "\r\n");
|
||||
$line = $read($conn);
|
||||
if ($code($line) !== 235) throw new RuntimeException("Password rejected: $line");
|
||||
|
||||
$write($conn, "MAIL FROM:<{$cfg['mail_from']}>\r\n");
|
||||
$line = $read($conn);
|
||||
if ($code($line) !== 250) throw new RuntimeException("MAIL FROM failed: $line");
|
||||
|
||||
$write($conn, "RCPT TO:<$to>\r\n");
|
||||
$line = $read($conn);
|
||||
if ($code($line) !== 250) throw new RuntimeException("RCPT TO failed: $line");
|
||||
|
||||
$write($conn, "DATA\r\n");
|
||||
$line = $read($conn);
|
||||
if ($code($line) !== 354) throw new RuntimeException("DATA failed: $line");
|
||||
|
||||
$enc_subject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
|
||||
$boundary = 'omsorg_' . md5(uniqid((string)mt_rand(), true));
|
||||
|
||||
$headers = "From: OMSORG Connect <{$cfg['mail_from']}>\r\n";
|
||||
$headers .= "To: $to\r\n";
|
||||
$headers .= "Subject: $enc_subject\r\n";
|
||||
$headers .= "MIME-Version: 1.0\r\n";
|
||||
|
||||
if ($attach) {
|
||||
$headers .= "Content-Type: multipart/mixed; boundary=\"$boundary\"\r\n";
|
||||
$msg = $headers . "\r\n";
|
||||
$msg .= "--$boundary\r\n";
|
||||
$msg .= "Content-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n";
|
||||
$msg .= $body . "\r\n\r\n";
|
||||
$safe_name = preg_replace('/[^A-Za-z0-9._-]/', '_', $attach['name']);
|
||||
$file_data = chunk_split(base64_encode(file_get_contents($attach['path'])));
|
||||
$msg .= "--$boundary\r\n";
|
||||
$msg .= "Content-Type: {$attach['mime']}; name=\"$safe_name\"\r\n";
|
||||
$msg .= "Content-Transfer-Encoding: base64\r\n";
|
||||
$msg .= "Content-Disposition: attachment; filename=\"$safe_name\"\r\n\r\n";
|
||||
$msg .= $file_data . "\r\n";
|
||||
$msg .= "--$boundary--";
|
||||
} else {
|
||||
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
|
||||
$msg = $headers . "\r\n" . $body;
|
||||
}
|
||||
|
||||
// Dot-stuffing: lines starting with '.' must be doubled
|
||||
$msg = str_replace("\n.", "\n..", $msg);
|
||||
$write($conn, $msg . "\r\n.\r\n");
|
||||
$line = $read($conn);
|
||||
if ($code($line) !== 250) throw new RuntimeException("Message rejected: $line");
|
||||
|
||||
$write($conn, "QUIT\r\n");
|
||||
$read($conn);
|
||||
fclose($conn);
|
||||
|
||||
return ['ok' => true, 'log' => $log];
|
||||
|
||||
} catch (RuntimeException $e) {
|
||||
if (isset($conn) && is_resource($conn)) fclose($conn);
|
||||
return ['ok' => false, 'log' => $log . "\nError: " . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
// Einzige Stelle, die HTTP-Details von omsorgCore kennt (Basis-URL, Header, JSON-(De)Kodierung) -
|
||||
// analog zu httpClient.cjs/authClient.cjs in omsorgapp. Alle Aufrufer bekommen ein einheitliches
|
||||
// Ergebnis-Array: ['ok' => bool, 'status' => int, 'data' => array|null].
|
||||
|
||||
function omsorgcore_request(array $config, string $method, string $path, ?array $body = null, ?string $accessToken = null): array
|
||||
{
|
||||
$url = rtrim($config['omsorg_core_url'], '/') . $path;
|
||||
|
||||
$headers = ['Content-Type: application/json'];
|
||||
if ($accessToken !== null) {
|
||||
$headers[] = 'Authorization: Bearer ' . $accessToken;
|
||||
}
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_CUSTOMREQUEST => $method,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
CURLOPT_POSTFIELDS => $body !== null ? json_encode($body) : null,
|
||||
]);
|
||||
|
||||
$responseBody = curl_exec($ch);
|
||||
if ($responseBody === false) {
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
return ['ok' => false, 'status' => 0, 'data' => null, 'error' => $error];
|
||||
}
|
||||
|
||||
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$data = null;
|
||||
if ($responseBody !== '') {
|
||||
$decoded = json_decode($responseBody, true);
|
||||
$data = json_last_error() === JSON_ERROR_NONE ? $decoded : null;
|
||||
}
|
||||
|
||||
return ['ok' => $status >= 200 && $status < 300, 'status' => $status, 'data' => $data];
|
||||
}
|
||||
|
||||
// --- Auth ---
|
||||
|
||||
function omsorgcore_login(array $config, string $username, string $password): array
|
||||
{
|
||||
return omsorgcore_request($config, 'POST', '/api/auth/login', ['username' => $username, 'password' => $password]);
|
||||
}
|
||||
|
||||
function omsorgcore_refresh(array $config, string $refreshToken): array
|
||||
{
|
||||
return omsorgcore_request($config, 'POST', '/api/auth/refresh', ['refreshToken' => $refreshToken]);
|
||||
}
|
||||
|
||||
function omsorgcore_logout(array $config, string $refreshToken): array
|
||||
{
|
||||
return omsorgcore_request($config, 'POST', '/api/auth/logout', ['refreshToken' => $refreshToken]);
|
||||
}
|
||||
|
||||
function omsorgcore_me(array $config, string $accessToken): array
|
||||
{
|
||||
return omsorgcore_request($config, 'GET', '/api/auth/me', null, $accessToken);
|
||||
}
|
||||
|
||||
function omsorgcore_change_password(array $config, string $accessToken, string $currentPassword, string $newPassword): array
|
||||
{
|
||||
return omsorgcore_request($config, 'POST', '/api/auth/change-password', [
|
||||
'currentPassword' => $currentPassword,
|
||||
'newPassword' => $newPassword,
|
||||
], $accessToken);
|
||||
}
|
||||
|
||||
function omsorgcore_forgot_password_request(array $config, string $username): array
|
||||
{
|
||||
return omsorgcore_request($config, 'POST', '/api/auth/forgot-password/request', ['username' => $username]);
|
||||
}
|
||||
|
||||
function omsorgcore_forgot_password_verify(array $config, string $username, string $pin): array
|
||||
{
|
||||
return omsorgcore_request($config, 'POST', '/api/auth/forgot-password/verify', ['username' => $username, 'pin' => $pin]);
|
||||
}
|
||||
|
||||
function omsorgcore_forgot_password_reset(array $config, string $resetToken, string $newPassword): array
|
||||
{
|
||||
return omsorgcore_request($config, 'POST', '/api/auth/forgot-password/reset', [
|
||||
'resetToken' => $resetToken,
|
||||
'newPassword' => $newPassword,
|
||||
]);
|
||||
}
|
||||
|
||||
// --- Users ---
|
||||
|
||||
function omsorgcore_users_list(array $config, string $accessToken): array
|
||||
{
|
||||
return omsorgcore_request($config, 'GET', '/api/users', null, $accessToken);
|
||||
}
|
||||
|
||||
function omsorgcore_users_create(array $config, string $accessToken, array $payload): array
|
||||
{
|
||||
return omsorgcore_request($config, 'POST', '/api/users', $payload, $accessToken);
|
||||
}
|
||||
|
||||
function omsorgcore_users_update(array $config, string $accessToken, string $userId, array $payload): array
|
||||
{
|
||||
return omsorgcore_request($config, 'PUT', '/api/users/' . urlencode($userId), $payload, $accessToken);
|
||||
}
|
||||
|
||||
function omsorgcore_users_reset_password(array $config, string $accessToken, string $userId, array $payload): array
|
||||
{
|
||||
return omsorgcore_request($config, 'POST', '/api/users/' . urlencode($userId) . '/reset-password', $payload, $accessToken);
|
||||
}
|
||||
|
||||
// --- Roles ---
|
||||
|
||||
function omsorgcore_roles_list(array $config, string $accessToken): array
|
||||
{
|
||||
return omsorgcore_request($config, 'GET', '/api/roles', null, $accessToken);
|
||||
}
|
||||
|
||||
function omsorgcore_roles_create(array $config, string $accessToken, string $name): array
|
||||
{
|
||||
return omsorgcore_request($config, 'POST', '/api/roles', ['name' => $name], $accessToken);
|
||||
}
|
||||
|
||||
// --- Employees (für die Datenmigration gebraucht - Employee je PHP-User finden/anlegen) ---
|
||||
|
||||
function omsorgcore_employees_list(array $config, string $accessToken, int $page = 1, int $pageSize = 100): array
|
||||
{
|
||||
return omsorgcore_request($config, 'GET', '/api/employees?page=' . $page . '&pageSize=' . $pageSize, null, $accessToken);
|
||||
}
|
||||
|
||||
function omsorgcore_employees_get(array $config, string $accessToken, string $employeeId): array
|
||||
{
|
||||
return omsorgcore_request($config, 'GET', '/api/employees/' . urlencode($employeeId), null, $accessToken);
|
||||
}
|
||||
|
||||
function omsorgcore_employees_create(array $config, string $accessToken, array $payload): array
|
||||
{
|
||||
return omsorgcore_request($config, 'POST', '/api/employees', $payload, $accessToken);
|
||||
}
|
||||
|
||||
function omsorgcore_employees_update(array $config, string $accessToken, string $employeeId, array $payload): array
|
||||
{
|
||||
return omsorgcore_request($config, 'PUT', '/api/employees/' . urlencode($employeeId), $payload, $accessToken);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/auth.php';
|
||||
|
||||
// Gemeinsame Endung→MIME-Whitelists.
|
||||
const UPLOAD_TYPES_DOCS = [
|
||||
'pdf' => ['application/pdf'],
|
||||
'doc' => ['application/msword'],
|
||||
'docx' => ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
|
||||
'jpg' => ['image/jpeg'],
|
||||
'jpeg' => ['image/jpeg'],
|
||||
'png' => ['image/png'],
|
||||
];
|
||||
|
||||
const UPLOAD_TYPES_PDF_IMG = [
|
||||
'pdf' => ['application/pdf'],
|
||||
'jpg' => ['image/jpeg'],
|
||||
'jpeg' => ['image/jpeg'],
|
||||
'png' => ['image/png'],
|
||||
];
|
||||
|
||||
const UPLOAD_TYPES_IMAGES = [
|
||||
'jpg' => ['image/jpeg'],
|
||||
'jpeg' => ['image/jpeg'],
|
||||
'png' => ['image/png'],
|
||||
'gif' => ['image/gif'],
|
||||
'webp' => ['image/webp'],
|
||||
];
|
||||
|
||||
const UPLOAD_TYPES_PDF = [
|
||||
'pdf' => ['application/pdf'],
|
||||
];
|
||||
|
||||
const UPLOAD_TYPES_OFFICE = [
|
||||
'pdf' => ['application/pdf'],
|
||||
'doc' => ['application/msword'],
|
||||
'docx' => ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
|
||||
'xls' => ['application/vnd.ms-excel'],
|
||||
'xlsx' => ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
|
||||
'ppt' => ['application/vnd.ms-powerpoint'],
|
||||
'pptx' => ['application/vnd.openxmlformats-officedocument.presentationml.presentation'],
|
||||
'txt' => ['text/plain'],
|
||||
'zip' => ['application/zip', 'application/x-zip-compressed'],
|
||||
];
|
||||
|
||||
/**
|
||||
* Validiert und speichert eine hochgeladene Datei.
|
||||
*
|
||||
* @param array $file Eintrag aus $_FILES, z.B. $_FILES['datei']
|
||||
* @param array $opts allowed (ext=>mimes, Pflicht), dir (Pflicht), prefix,
|
||||
* max_bytes (Default 12 MB), required (Default true),
|
||||
* naming ('dated' [Default] = Datum_Username_Prefix_Rand,
|
||||
* 'plain' = Username_Rand, 'hash' = nur Rand),
|
||||
* username (Default current_username(); für 'dated'/'plain'),
|
||||
* messages (Überschreibt Standardtexte: missing/upload/ext/mime/size/move)
|
||||
* @return array ['ok'=>bool, 'error'=>?string, 'filename'=>?string,
|
||||
* 'original_name'=>?string, 'mime'=>?string]
|
||||
*/
|
||||
function handle_upload(array $file, array $opts): array {
|
||||
$allowed = $opts['allowed'];
|
||||
$dir = rtrim($opts['dir'], '/') . '/';
|
||||
$maxBytes = $opts['max_bytes'] ?? 12 * 1024 * 1024;
|
||||
$required = $opts['required'] ?? true;
|
||||
$naming = $opts['naming'] ?? 'dated';
|
||||
$prefix = $opts['prefix'] ?? 'datei';
|
||||
$username = $opts['username'] ?? current_username();
|
||||
|
||||
$msg = array_merge([
|
||||
'missing' => 'Bitte eine Datei auswählen.',
|
||||
'upload' => 'Fehler beim Hochladen der Datei.',
|
||||
'ext' => 'Dateityp nicht erlaubt.',
|
||||
'mime' => 'Dateityp nicht erlaubt.',
|
||||
'size' => 'Datei ist zu groß (max. 12 MB).',
|
||||
'move' => 'Fehler beim Hochladen der Datei.',
|
||||
], $opts['messages'] ?? []);
|
||||
|
||||
$err = $file['error'] ?? UPLOAD_ERR_NO_FILE;
|
||||
|
||||
if ($err === UPLOAD_ERR_NO_FILE || !isset($file['tmp_name'])) {
|
||||
if ($required) return ['ok' => false, 'error' => $msg['missing']];
|
||||
return ['ok' => true, 'error' => null, 'filename' => null, 'original_name' => null, 'mime' => null];
|
||||
}
|
||||
if ($err !== UPLOAD_ERR_OK) {
|
||||
return ['ok' => false, 'error' => $msg['upload']];
|
||||
}
|
||||
|
||||
$originalName = $file['name'];
|
||||
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
|
||||
if (!isset($allowed[$ext])) {
|
||||
return ['ok' => false, 'error' => $msg['ext']];
|
||||
}
|
||||
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$mime = $finfo->file($file['tmp_name']);
|
||||
if (!in_array($mime, $allowed[$ext], true)) {
|
||||
return ['ok' => false, 'error' => $msg['mime']];
|
||||
}
|
||||
|
||||
if ($file['size'] > $maxBytes) {
|
||||
return ['ok' => false, 'error' => $msg['size']];
|
||||
}
|
||||
|
||||
$safeName = match ($naming) {
|
||||
'hash' => bin2hex(random_bytes(16)) . '.' . $ext,
|
||||
'plain' => $username . '_' . bin2hex(random_bytes(8)) . '.' . $ext,
|
||||
default => date('Y-m-d') . '_' . $username . '_' . $prefix . '_' . bin2hex(random_bytes(8)) . '.' . $ext,
|
||||
};
|
||||
|
||||
if (!is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
if (!move_uploaded_file($file['tmp_name'], $dir . $safeName)) {
|
||||
return ['ok' => false, 'error' => $msg['move']];
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'error' => null,
|
||||
'filename' => $safeName,
|
||||
'original_name' => $originalName,
|
||||
'mime' => $mime,
|
||||
];
|
||||
}
|
||||
|
||||
function redirect_error(string $back, string $msg): never {
|
||||
header('Location: ' . $back . '?error=' . urlencode($msg));
|
||||
exit;
|
||||
}
|
||||
|
||||
function redirect_ok(string $back, string $param = 'success', string $val = '1'): never {
|
||||
header('Location: ' . $back . '?' . $param . '=' . $val);
|
||||
exit;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "OMSORG Mitarbeiter-App",
|
||||
"short_name": "OMSORG",
|
||||
"start_url": "./",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#1a3a5c",
|
||||
"icons": [
|
||||
{ "src": "../assets/omsorg-logo-new.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "../assets/omsorg-logo-new.png", "sizes": "512x512", "type": "image/png" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Require all denied
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
return [
|
||||
'description' => 'Initial schema',
|
||||
'up' => [
|
||||
"CREATE TABLE IF NOT EXISTS users (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(255) NOT NULL UNIQUE,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(32) NOT NULL DEFAULT 'user',
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at VARCHAR(32) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL DEFAULT '',
|
||||
telefon VARCHAR(64) NOT NULL DEFAULT ''
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS requests_urlaubsantrag (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
admin_note TEXT NOT NULL,
|
||||
created_at VARCHAR(32) NOT NULL,
|
||||
updated_at VARCHAR(32) NOT NULL,
|
||||
von VARCHAR(32) NOT NULL DEFAULT '',
|
||||
bis VARCHAR(32) NOT NULL DEFAULT '',
|
||||
vertretung VARCHAR(255) NOT NULL DEFAULT '',
|
||||
nachricht TEXT NOT NULL
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS requests_abwesenheitsantrag (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
admin_note TEXT NOT NULL,
|
||||
created_at VARCHAR(32) NOT NULL,
|
||||
updated_at VARCHAR(32) NOT NULL,
|
||||
von VARCHAR(32) NOT NULL DEFAULT '',
|
||||
bis VARCHAR(32) NOT NULL DEFAULT '',
|
||||
grund VARCHAR(64) NOT NULL DEFAULT '',
|
||||
vertretung VARCHAR(255) NOT NULL DEFAULT '',
|
||||
nachricht TEXT NOT NULL
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS requests_benefitsantrag (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
admin_note TEXT NOT NULL,
|
||||
created_at VARCHAR(32) NOT NULL,
|
||||
updated_at VARCHAR(32) NOT NULL,
|
||||
benefit VARCHAR(64) NOT NULL DEFAULT '',
|
||||
nachricht TEXT NOT NULL
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS requests_werben (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
admin_note TEXT NOT NULL,
|
||||
created_at VARCHAR(32) NOT NULL,
|
||||
updated_at VARCHAR(32) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
email VARCHAR(255) NOT NULL DEFAULT '',
|
||||
qualifikation VARCHAR(64) NOT NULL DEFAULT '',
|
||||
nachricht TEXT NOT NULL
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS dienstplan (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
date VARCHAR(16) NOT NULL,
|
||||
schicht VARCHAR(16) NOT NULL,
|
||||
created_at VARCHAR(32) NOT NULL,
|
||||
updated_at VARCHAR(32) NOT NULL,
|
||||
UNIQUE KEY uq_user_date (user_id, date)
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS downloads (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
filename VARCHAR(255) NOT NULL,
|
||||
original_name VARCHAR(255) NOT NULL,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_at VARCHAR(32) NOT NULL,
|
||||
updated_at VARCHAR(32) NOT NULL
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS fortbildung_materials (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
filename VARCHAR(255) NOT NULL,
|
||||
original_name VARCHAR(255) NOT NULL,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_at VARCHAR(32) NOT NULL,
|
||||
updated_at VARCHAR(32) NOT NULL
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS requests_fortbildungsantrag (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
admin_note TEXT NOT NULL,
|
||||
anliegen TEXT NOT NULL,
|
||||
thema TEXT NOT NULL,
|
||||
nachricht TEXT NOT NULL,
|
||||
filename VARCHAR(255) NOT NULL DEFAULT '',
|
||||
original_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
created_at VARCHAR(32) NOT NULL,
|
||||
updated_at VARCHAR(32) NOT NULL
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS requests_stundennachweis (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
admin_note TEXT NOT NULL,
|
||||
monat VARCHAR(16) NOT NULL,
|
||||
filename VARCHAR(255) NOT NULL DEFAULT '',
|
||||
original_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
created_at VARCHAR(32) NOT NULL,
|
||||
updated_at VARCHAR(32) NOT NULL
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS einsatzbewertungen (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
einsatzort VARCHAR(255) NOT NULL,
|
||||
von VARCHAR(32) NOT NULL,
|
||||
bis VARCHAR(32) NOT NULL,
|
||||
bewertung TINYINT NOT NULL,
|
||||
wieder TINYINT(1) NOT NULL DEFAULT 0,
|
||||
feedback TEXT NOT NULL,
|
||||
created_at VARCHAR(32) NOT NULL
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS dokumente (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
kategorie VARCHAR(255) NOT NULL DEFAULT 'Sonstiges',
|
||||
beschreibung TEXT NOT NULL,
|
||||
filename VARCHAR(255) NOT NULL,
|
||||
original_name VARCHAR(255) NOT NULL,
|
||||
filesize INT NOT NULL DEFAULT 0,
|
||||
created_at VARCHAR(32) NOT NULL
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS news (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
date VARCHAR(16) NOT NULL,
|
||||
created_at VARCHAR(32) NOT NULL
|
||||
)",
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
return [
|
||||
'description' => 'Add avatar column to users',
|
||||
'up' => [
|
||||
'ALTER TABLE users ADD COLUMN avatar VARCHAR(255) DEFAULT NULL',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
return [
|
||||
'description' => 'Einsatzanweisung table',
|
||||
'up' => [
|
||||
"CREATE TABLE IF NOT EXISTS einsatzanweisung (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL UNIQUE,
|
||||
filename VARCHAR(255) NOT NULL DEFAULT '',
|
||||
original_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
ort VARCHAR(255) NOT NULL DEFAULT '',
|
||||
uploaded_at VARCHAR(32) NOT NULL DEFAULT ''
|
||||
)",
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
return [
|
||||
'description' => 'Login attempts table for IP-based rate limiting',
|
||||
'up' => [
|
||||
"CREATE TABLE IF NOT EXISTS login_attempts (
|
||||
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
ip VARCHAR(45) NOT NULL DEFAULT '',
|
||||
username VARCHAR(190) NOT NULL DEFAULT '',
|
||||
attempted_at VARCHAR(32) NOT NULL DEFAULT ''
|
||||
)",
|
||||
"CREATE INDEX idx_login_attempts_ip_time ON login_attempts (ip, attempted_at)",
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
return [
|
||||
'description' => 'Add filename/original_name to requests_urlaubsantrag',
|
||||
'up' => [
|
||||
"ALTER TABLE requests_urlaubsantrag ADD COLUMN filename VARCHAR(255) NOT NULL DEFAULT ''",
|
||||
"ALTER TABLE requests_urlaubsantrag ADD COLUMN original_name VARCHAR(255) NOT NULL DEFAULT ''",
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
// Additiv: fügt Spalten hinzu, die migrate-users-to-core.php befüllt, um jede lokale users-Zeile
|
||||
// (Fremdschlüssel-Ziel für dienstplan/requests_*/dokumente/einsatzanweisung/einsatzbewertungen) auf
|
||||
// die neue omsorgCore-Identität abzubilden. Die Tabelle `users` bleibt danach bestehen, wird aber zum
|
||||
// reinen Lesecache (Name/E-Mail/Telefon/Avatar/Aktiv-Status) für die noch nicht migrierten Seiten -
|
||||
// omsorgCore ist die Quelle der Wahrheit, siehe lib/auth.php. `password_hash` wird erst am Ende von
|
||||
// migrate-users-to-core.php gelöscht (destruktiv, deshalb nicht Teil dieser automatisch laufenden
|
||||
// Migration - erst NACHDEM alle User erfolgreich nach omsorgCore übernommen wurden).
|
||||
return [
|
||||
'description' => 'Add omsorgCore identity mapping columns to users (prep for migrate-users-to-core.php)',
|
||||
'up' => [
|
||||
'ALTER TABLE users ADD COLUMN omsorgcore_user_id CHAR(36) DEFAULT NULL',
|
||||
'ALTER TABLE users ADD COLUMN omsorgcore_employee_id CHAR(36) DEFAULT NULL',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/db.php';
|
||||
require_once __DIR__ . '/../lib/layout.php';
|
||||
require_login();
|
||||
|
||||
$user_id = current_user()['id'];
|
||||
|
||||
$stmt = db()->prepare('SELECT * FROM requests_abwesenheitsantrag WHERE user_id = ? ORDER BY created_at DESC');
|
||||
$stmt->execute([$user_id]);
|
||||
$requests = $stmt->fetchAll();
|
||||
|
||||
$grund_labels = [
|
||||
'krankheit' => 'Krankheit',
|
||||
'arztbesuch' => 'Arztbesuch',
|
||||
'behoerdengang' => 'Behördengang',
|
||||
'sonderurlaub' => 'Sonderurlaub',
|
||||
'elternzeit_pflegezeit' => 'Elternzeit / Pflegezeit',
|
||||
'sonstiges' => 'Sonstiges',
|
||||
];
|
||||
|
||||
$status_labels = ['pending' => 'Ausstehend', 'accepted' => 'Akzeptiert', 'rejected' => 'Abgelehnt'];
|
||||
|
||||
layout_start('OMSORG – Abwesenheitsantrag', 'abwesenheitsantrag');
|
||||
?>
|
||||
<style>
|
||||
.modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.55);display:none;align-items:center;justify-content:center;z-index:1000}
|
||||
.modal-box{background:var(--glass-bg,#1e2235);border:1px solid var(--border,rgba(255,255,255,.1));border-radius:14px;padding:28px 32px;max-width:480px;width:90%;position:relative}
|
||||
.modal-box h3{margin:0 0 20px;font-family:'KindelSerif',Georgia,serif;font-size:1.3rem}
|
||||
.modal-close{position:absolute;top:12px;right:16px;background:none;border:none;font-size:1.5rem;cursor:pointer;color:var(--muted,#888);line-height:1}
|
||||
.modal-dl{display:grid;grid-template-columns:auto 1fr;gap:8px 16px;font-size:.95rem}
|
||||
.modal-dl dt{color:var(--muted,#888);white-space:nowrap}
|
||||
.modal-dl dd{margin:0;font-weight:500}
|
||||
.btn-sm{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:9px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.08);color:#fff;font-weight:600;font-size:.82rem;cursor:pointer;font:inherit;transition:background .15s}
|
||||
.btn-sm:hover{background:rgba(255,255,255,.15)}
|
||||
</style>
|
||||
|
||||
<h1 class="main-title">📅 Abwesenheitsantrag</h1>
|
||||
<p class="main-sub">Melde eine geplante Abwesenheit. Das Startdatum muss vor dem Enddatum liegen.</p>
|
||||
|
||||
<?php if (isset($_GET['success'])): ?>
|
||||
<div class="success" style="margin-top:16px">Antrag erfolgreich eingereicht.</div>
|
||||
<?php elseif (isset($_GET['error'])): ?>
|
||||
<div class="error" style="margin-top:16px"><?= e(urldecode($_GET['error'])) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div id="abw-modal" class="modal-backdrop" onclick="if(event.target===this)this.style.display='none'">
|
||||
<div class="modal-box glass">
|
||||
<button class="modal-close" onclick="document.getElementById('abw-modal').style.display='none'">×</button>
|
||||
<h3>Antrag Details</h3>
|
||||
<dl class="modal-dl">
|
||||
<dt>Von</dt> <dd id="abw-von">–</dd>
|
||||
<dt>Bis</dt> <dd id="abw-bis">–</dd>
|
||||
<dt>Grund</dt> <dd id="abw-grund">–</dd>
|
||||
<dt>Vertretung</dt> <dd id="abw-vertretung">–</dd>
|
||||
<dt>Nachricht</dt> <dd id="abw-nachricht">–</dd>
|
||||
<dt>Eingereicht</dt><dd id="abw-eingereicht">–</dd>
|
||||
<dt>Status</dt> <dd id="abw-status">–</dd>
|
||||
<dt>Admin-Notiz</dt><dd id="abw-note">–</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-split">
|
||||
|
||||
<div class="page-split-list">
|
||||
<h2>Meine Anträge</h2>
|
||||
<?php if (empty($requests)): ?>
|
||||
<p style="color:var(--muted)">Noch keine Anträge gestellt.</p>
|
||||
<?php else: ?>
|
||||
<table class="requests-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Von</th>
|
||||
<th>Bis</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($requests as $r): ?>
|
||||
<tr>
|
||||
<td><?= e(date('d.m.Y', strtotime($r['von']))) ?></td>
|
||||
<td><?= e(date('d.m.Y', strtotime($r['bis']))) ?></td>
|
||||
<td><span class="badge badge-<?= e($r['status']) ?>"><?= e($status_labels[$r['status']] ?? $r['status']) ?></span></td>
|
||||
<td>
|
||||
<button class="btn-sm js-abw-details"
|
||||
data-von="<?= e(date('d.m.Y', strtotime($r['von']))) ?>"
|
||||
data-bis="<?= e(date('d.m.Y', strtotime($r['bis']))) ?>"
|
||||
data-grund="<?= e($grund_labels[$r['grund']] ?? $r['grund']) ?>"
|
||||
data-vertretung="<?= e($r['vertretung'] !== '' ? $r['vertretung'] : '–') ?>"
|
||||
data-nachricht="<?= e($r['nachricht'] !== '' ? $r['nachricht'] : '–') ?>"
|
||||
data-eingereicht="<?= e(date('d.m.Y', strtotime($r['created_at']))) ?>"
|
||||
data-status-label="<?= e($status_labels[$r['status']] ?? $r['status']) ?>"
|
||||
data-status-key="<?= e($r['status']) ?>"
|
||||
data-note="<?= e($r['admin_note'] !== '' ? $r['admin_note'] : '–') ?>"
|
||||
>Details</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="page-split-form">
|
||||
<div class="glass detail-card">
|
||||
<h2>Neuer Abwesenheitsantrag</h2>
|
||||
<form action="../actions/submit-abwesenheitsantrag.php" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<label>
|
||||
<span>Von <span style="color:#ff8080">*</span></span>
|
||||
<input type="date" name="von" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Bis <span style="color:#ff8080">*</span></span>
|
||||
<input type="date" name="bis" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Grund <span style="color:#ff8080">*</span></span>
|
||||
<select name="grund" required>
|
||||
<option value="">– Bitte wählen –</option>
|
||||
<option value="krankheit">Krankheit</option>
|
||||
<option value="arztbesuch">Arztbesuch</option>
|
||||
<option value="behoerdengang">Behördengang</option>
|
||||
<option value="sonderurlaub">Sonderurlaub</option>
|
||||
<option value="elternzeit_pflegezeit">Elternzeit / Pflegezeit</option>
|
||||
<option value="sonstiges">Sonstiges</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Vertretung</span>
|
||||
<input type="text" name="vertretung" placeholder="Name der Vertretung">
|
||||
</label>
|
||||
<label>
|
||||
Nachricht
|
||||
<textarea name="nachricht" rows="4" placeholder="Optionale Anmerkungen"></textarea>
|
||||
</label>
|
||||
<button type="submit" class="btn" style="margin-top:6px">Antrag einreichen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('click', function(e) {
|
||||
var btn = e.target.closest('.js-abw-details');
|
||||
if (!btn) return;
|
||||
var d = btn.dataset;
|
||||
document.getElementById('abw-von').textContent = d.von;
|
||||
document.getElementById('abw-bis').textContent = d.bis;
|
||||
document.getElementById('abw-grund').textContent = d.grund;
|
||||
document.getElementById('abw-vertretung').textContent = d.vertretung;
|
||||
document.getElementById('abw-nachricht').textContent = d.nachricht;
|
||||
document.getElementById('abw-eingereicht').textContent = d.eingereicht;
|
||||
document.getElementById('abw-status').innerHTML =
|
||||
'<span class="badge badge-' + d.statusKey + '">' + d.statusLabel + '</span>';
|
||||
document.getElementById('abw-note').textContent = d.note;
|
||||
document.getElementById('abw-modal').style.display = 'flex';
|
||||
});
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') document.getElementById('abw-modal').style.display = 'none';
|
||||
});
|
||||
</script>
|
||||
<?php layout_end(); ?>
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/db.php';
|
||||
require_once __DIR__ . '/../lib/layout.php';
|
||||
require_admin();
|
||||
|
||||
// Fetch all active users
|
||||
$users = db()->query(
|
||||
'SELECT id, name FROM users WHERE active = 1 ORDER BY name ASC'
|
||||
)->fetchAll();
|
||||
|
||||
$now = new DateTimeImmutable('today');
|
||||
$year = isset($_GET['year']) ? (int)$_GET['year'] : (int)$now->format('Y');
|
||||
$month = isset($_GET['month']) ? (int)$_GET['month'] : (int)$now->format('n');
|
||||
$year = max(2020, min(2099, $year));
|
||||
$month = max(1, min(12, $month));
|
||||
|
||||
// Resolve selected user
|
||||
$selected_id = isset($_GET['user_id']) ? (int)$_GET['user_id'] : 0;
|
||||
$selected_user = null;
|
||||
foreach ($users as $u) {
|
||||
if ((int)$u['id'] === $selected_id) { $selected_user = $u; break; }
|
||||
}
|
||||
if (!$selected_user && !empty($users)) {
|
||||
$selected_user = $users[0];
|
||||
$selected_id = (int)$selected_user['id'];
|
||||
}
|
||||
|
||||
$first = new DateTimeImmutable(sprintf('%04d-%02d-01', $year, $month));
|
||||
$month_start = $first->format('Y-m-d');
|
||||
$month_end = $first->modify('last day of this month')->format('Y-m-d');
|
||||
$days = (int)$first->format('t');
|
||||
|
||||
$prev = $first->modify('-1 month');
|
||||
$next = $first->modify('+1 month');
|
||||
$prev_url = '?user_id=' . $selected_id . '&year=' . $prev->format('Y') . '&month=' . $prev->format('n');
|
||||
$next_url = '?user_id=' . $selected_id . '&year=' . $next->format('Y') . '&month=' . $next->format('n');
|
||||
|
||||
// Load shifts
|
||||
$schichten = [];
|
||||
if ($selected_id) {
|
||||
$stmt = db()->prepare(
|
||||
'SELECT date, schicht FROM dienstplan WHERE user_id = ? AND date >= ? AND date <= ?'
|
||||
);
|
||||
$stmt->execute([$selected_id, $month_start, $month_end]);
|
||||
foreach ($stmt->fetchAll() as $row) {
|
||||
$schichten[$row['date']] = $row['schicht'];
|
||||
}
|
||||
}
|
||||
|
||||
// Load overlays
|
||||
$overlays = [];
|
||||
if ($selected_id) {
|
||||
$stmt = db()->prepare("
|
||||
SELECT von, bis, 'urlaub' AS typ
|
||||
FROM requests_urlaubsantrag
|
||||
WHERE user_id = ? AND status = 'accepted' AND bis >= ? AND von <= ?
|
||||
UNION ALL
|
||||
SELECT von, bis, 'abwesenheit' AS typ
|
||||
FROM requests_abwesenheitsantrag
|
||||
WHERE user_id = ? AND status = 'accepted' AND bis >= ? AND von <= ?
|
||||
");
|
||||
$stmt->execute([$selected_id, $month_start, $month_end, $selected_id, $month_start, $month_end]);
|
||||
foreach ($stmt->fetchAll() as $row) {
|
||||
$cursor = new DateTimeImmutable($row['von']);
|
||||
$end = new DateTimeImmutable($row['bis']);
|
||||
while ($cursor <= $end) {
|
||||
$key = $cursor->format('Y-m-d');
|
||||
if ($key >= $month_start && $key <= $month_end) {
|
||||
$overlays[$key] = $row['typ'];
|
||||
}
|
||||
$cursor = $cursor->modify('+1 day');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$today_str = $now->format('Y-m-d');
|
||||
$schicht_labels = ['frueh' => 'Früh', 'spaet' => 'Spät', 'nacht' => 'Nacht'];
|
||||
$weekdays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
||||
$months_de = ['', 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
||||
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'];
|
||||
|
||||
layout_start('Dienstpläne', 'admin-dienstplan');
|
||||
?>
|
||||
|
||||
<h1 class="main-title">Dienstpläne</h1>
|
||||
|
||||
<div class="dp-user-select-wrap">
|
||||
<form method="get">
|
||||
<input type="hidden" name="year" value="<?= $year ?>">
|
||||
<input type="hidden" name="month" value="<?= $month ?>">
|
||||
<label class="form-label" for="user_id">Mitarbeiter</label>
|
||||
<select name="user_id" id="user_id" onchange="this.form.submit()">
|
||||
<?php foreach ($users as $u): ?>
|
||||
<option value="<?= (int)$u['id'] ?>" <?= (int)$u['id'] === $selected_id ? 'selected' : '' ?>>
|
||||
<?= e($u['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php if ($selected_user): ?>
|
||||
|
||||
<div class="dp-nav">
|
||||
<div style="display:flex;gap:10px;align-items:center">
|
||||
<a href="<?= e($prev_url) ?>" class="dp-nav-btn">←</a>
|
||||
<h2 class="dp-nav-title"><?= e($months_de[$month] . ' ' . $year) ?></h2>
|
||||
<a href="<?= e($next_url) ?>" class="dp-nav-btn">→</a>
|
||||
</div>
|
||||
<span style="color:var(--muted);font-size:.9rem"><?= e($selected_user['name']) ?></span>
|
||||
</div>
|
||||
|
||||
<div class="dp-grid">
|
||||
<?php foreach ($weekdays as $wd): ?>
|
||||
<div class="dp-weekday"><?= e($wd) ?></div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php
|
||||
$lead = (int)$first->format('N') - 1;
|
||||
for ($i = 0; $i < $lead; $i++): ?>
|
||||
<div class="dp-cell dp-cell-empty"></div>
|
||||
<?php endfor; ?>
|
||||
|
||||
<?php for ($d = 1; $d <= $days; $d++):
|
||||
$date_key = sprintf('%04d-%02d-%02d', $year, $month, $d);
|
||||
$is_today = ($date_key === $today_str);
|
||||
$overlay = $overlays[$date_key] ?? null;
|
||||
$schicht = $schichten[$date_key] ?? null;
|
||||
$cell_cls = 'dp-cell' . ($is_today ? ' dp-cell-today' : '');
|
||||
?>
|
||||
<div class="<?= $cell_cls ?>">
|
||||
<span class="dp-day-num"><?= $d ?></span>
|
||||
<?php if ($overlay): ?>
|
||||
<span class="dp-badge dp-overlay-<?= e($overlay) ?>">
|
||||
<?= $overlay === 'urlaub' ? 'Urlaub' : 'Abwesenheit' ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php if ($schicht): ?>
|
||||
<span class="dp-badge dp-badge-<?= e($schicht) ?>">
|
||||
<?= e($schicht_labels[$schicht]) ?>
|
||||
</span>
|
||||
<?php elseif (!$overlay): ?>
|
||||
<span class="dp-free">–</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endfor; ?>
|
||||
|
||||
<?php
|
||||
$total = $lead + $days;
|
||||
$trail = (7 - ($total % 7)) % 7;
|
||||
for ($i = 0; $i < $trail; $i++): ?>
|
||||
<div class="dp-cell dp-cell-empty"></div>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
<p style="color:var(--muted)">Keine aktiven Mitarbeiter vorhanden.</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
layout_end();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/db.php';
|
||||
require_once __DIR__ . '/../lib/layout.php';
|
||||
require_login();
|
||||
|
||||
$user_id = current_user()['id'];
|
||||
|
||||
$stmt = db()->prepare('SELECT * FROM requests_benefitsantrag WHERE user_id = ? ORDER BY created_at DESC');
|
||||
$stmt->execute([$user_id]);
|
||||
$requests = $stmt->fetchAll();
|
||||
|
||||
$benefit_labels = [
|
||||
'yoga' => 'Yoga',
|
||||
'autogenes_training' => 'Autogenes Training',
|
||||
'massage' => 'Massage',
|
||||
'tankgutschein' => 'Tankgutschein',
|
||||
'online_gutscheine' => 'Online-Gutscheine',
|
||||
];
|
||||
|
||||
$status_labels = ['pending' => 'Ausstehend', 'accepted' => 'Akzeptiert', 'rejected' => 'Abgelehnt'];
|
||||
|
||||
layout_start('OMSORG – Benefitsantrag', 'benefitsantrag');
|
||||
?>
|
||||
<style>
|
||||
.modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.55);display:none;align-items:center;justify-content:center;z-index:1000}
|
||||
.modal-box{background:var(--glass-bg,#1e2235);border:1px solid var(--border,rgba(255,255,255,.1));border-radius:14px;padding:28px 32px;max-width:480px;width:90%;position:relative}
|
||||
.modal-box h3{margin:0 0 20px;font-family:'KindelSerif',Georgia,serif;font-size:1.3rem}
|
||||
.modal-close{position:absolute;top:12px;right:16px;background:none;border:none;font-size:1.5rem;cursor:pointer;color:var(--muted,#888);line-height:1}
|
||||
.modal-dl{display:grid;grid-template-columns:auto 1fr;gap:8px 16px;font-size:.95rem}
|
||||
.modal-dl dt{color:var(--muted,#888);white-space:nowrap}
|
||||
.modal-dl dd{margin:0;font-weight:500}
|
||||
.btn-sm{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:9px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.08);color:#fff;font-weight:600;font-size:.82rem;cursor:pointer;font:inherit;transition:background .15s}
|
||||
.btn-sm:hover{background:rgba(255,255,255,.15)}
|
||||
</style>
|
||||
|
||||
<h1 class="main-title">🎁 Benefitsantrag</h1>
|
||||
<p class="main-sub">Beantrage einen Mitarbeiterbenefit deiner Wahl.</p>
|
||||
|
||||
<?php if (isset($_GET['success'])): ?>
|
||||
<div class="success" style="margin-top:16px">Antrag erfolgreich eingereicht.</div>
|
||||
<?php elseif (isset($_GET['error'])): ?>
|
||||
<div class="error" style="margin-top:16px"><?= e(urldecode($_GET['error'])) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div id="ben-modal" class="modal-backdrop" onclick="if(event.target===this)this.style.display='none'">
|
||||
<div class="modal-box glass">
|
||||
<button class="modal-close" onclick="document.getElementById('ben-modal').style.display='none'">×</button>
|
||||
<h3>Antrag Details</h3>
|
||||
<dl class="modal-dl">
|
||||
<dt>Benefit</dt> <dd id="ben-benefit">–</dd>
|
||||
<dt>Nachricht</dt> <dd id="ben-nachricht">–</dd>
|
||||
<dt>Eingereicht</dt><dd id="ben-eingereicht">–</dd>
|
||||
<dt>Status</dt> <dd id="ben-status">–</dd>
|
||||
<dt>Admin-Notiz</dt><dd id="ben-note">–</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="liberty-modal" class="modal-backdrop" onclick="if(event.target===this)this.style.display='none'">
|
||||
<div class="modal-box glass" style="max-width:600px;padding:16px;text-align:center">
|
||||
<button class="modal-close" onclick="document.getElementById('liberty-modal').style.display='none'">×</button>
|
||||
<div class="success" style="margin:0 0 14px">Antrag erfolgreich eingereicht.</div>
|
||||
<img src="../assets/Liberty.jpeg" alt="Liberty" style="width:100%;border-radius:10px;display:block">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-split">
|
||||
|
||||
<div class="page-split-list">
|
||||
<h2>Meine Anträge</h2>
|
||||
<?php if (empty($requests)): ?>
|
||||
<p style="color:var(--muted)">Noch keine Anträge gestellt.</p>
|
||||
<?php else: ?>
|
||||
<table class="requests-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Benefit</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($requests as $r): ?>
|
||||
<tr>
|
||||
<td style="font-weight:800"><?= e($benefit_labels[$r['benefit']] ?? $r['benefit']) ?></td>
|
||||
<td><span class="badge badge-<?= e($r['status']) ?>"><?= e($status_labels[$r['status']] ?? $r['status']) ?></span></td>
|
||||
<td>
|
||||
<button class="btn-sm js-ben-details"
|
||||
data-benefit="<?= e($benefit_labels[$r['benefit']] ?? $r['benefit']) ?>"
|
||||
data-nachricht="<?= e($r['nachricht'] !== '' ? $r['nachricht'] : '–') ?>"
|
||||
data-eingereicht="<?= e(date('d.m.Y', strtotime($r['created_at']))) ?>"
|
||||
data-status-label="<?= e($status_labels[$r['status']] ?? $r['status']) ?>"
|
||||
data-status-key="<?= e($r['status']) ?>"
|
||||
data-note="<?= e($r['admin_note'] !== '' ? $r['admin_note'] : '–') ?>"
|
||||
>Details</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="page-split-form">
|
||||
<div class="glass detail-card">
|
||||
<h2>Neuer Benefitsantrag</h2>
|
||||
<form id="benefits-form" action="../actions/submit-benefitsantrag.php" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<label>
|
||||
<span>Benefit <span style="color:#ff8080">*</span></span>
|
||||
<select name="benefit" required>
|
||||
<option value="">– Bitte wählen –</option>
|
||||
<option value="yoga">Yoga</option>
|
||||
<option value="autogenes_training">Autogenes Training</option>
|
||||
<option value="massage">Massage</option>
|
||||
<option value="tankgutschein">Tankgutschein</option>
|
||||
<option value="online_gutscheine">Online-Gutscheine</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Nachricht
|
||||
<textarea name="nachricht" rows="4" placeholder="Optionale Anmerkungen"></textarea>
|
||||
</label>
|
||||
<div id="benefits-form-error" class="error" style="display:none;margin-top:8px"></div>
|
||||
<button type="submit" class="btn" style="margin-top:6px">Antrag einreichen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
var LIBERTY_BENEFITS = ['autogenes_training', 'massage'];
|
||||
|
||||
document.getElementById('benefits-form').addEventListener('submit', function(e) {
|
||||
var benefit = this.querySelector('[name="benefit"]').value;
|
||||
if (!LIBERTY_BENEFITS.includes(benefit)) return; // normal submit for other benefits
|
||||
e.preventDefault();
|
||||
var form = this;
|
||||
var btn = form.querySelector('[type="submit"]');
|
||||
btn.disabled = true;
|
||||
var errEl = document.getElementById('benefits-form-error');
|
||||
errEl.style.display = 'none';
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: new FormData(form),
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
}).then(function(r) { return r.json(); }).then(function(data) {
|
||||
btn.disabled = false;
|
||||
if (data.ok) {
|
||||
form.reset();
|
||||
document.getElementById('liberty-modal').style.display = 'flex';
|
||||
} else {
|
||||
errEl.textContent = data.error || 'Ein Fehler ist aufgetreten.';
|
||||
errEl.style.display = 'block';
|
||||
}
|
||||
}).catch(function() {
|
||||
btn.disabled = false;
|
||||
errEl.textContent = 'Ein Fehler ist aufgetreten. Bitte erneut versuchen.';
|
||||
errEl.style.display = 'block';
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
var btn = e.target.closest('.js-ben-details');
|
||||
if (!btn) return;
|
||||
var d = btn.dataset;
|
||||
document.getElementById('ben-benefit').textContent = d.benefit;
|
||||
document.getElementById('ben-nachricht').textContent = d.nachricht;
|
||||
document.getElementById('ben-eingereicht').textContent= d.eingereicht;
|
||||
document.getElementById('ben-status').innerHTML =
|
||||
'<span class="badge badge-' + d.statusKey + '">' + d.statusLabel + '</span>';
|
||||
document.getElementById('ben-note').textContent = d.note;
|
||||
document.getElementById('ben-modal').style.display = 'flex';
|
||||
});
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
document.getElementById('ben-modal').style.display = 'none';
|
||||
document.getElementById('liberty-modal').style.display = 'none';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?php layout_end(); ?>
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_login();
|
||||
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$current = $_POST['current_password'] ?? '';
|
||||
$new = $_POST['new_password'] ?? '';
|
||||
$repeat = $_POST['new_password_repeat'] ?? '';
|
||||
|
||||
if (strlen($new) < 8) {
|
||||
$error = 'Das neue Passwort muss mindestens 8 Zeichen lang sein.';
|
||||
} elseif ($new !== $repeat) {
|
||||
$error = 'Die Passwörter stimmen nicht überein.';
|
||||
} else {
|
||||
$result = omsorgcore_change_password(_omsorgcore_config(), $_SESSION['omsorgcore_access_token'], $current, $new);
|
||||
|
||||
if ($result['ok']) {
|
||||
// Serverseitig wurden alle Sessions widerrufen (siehe UserService.ChangeOwnPasswordAsync) -
|
||||
// die aktuelle Session ist damit tot, neuer Login mit dem neuen Passwort ist nötig.
|
||||
_clear_omsorgcore_session();
|
||||
header('Location: ../index.php?pwchanged=1');
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = $result['status'] === 401
|
||||
? 'Aktuelles Passwort ist falsch.'
|
||||
: 'Passwort konnte nicht geändert werden.';
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>OMSORG Felix – Passwort ändern</title>
|
||||
<link rel="stylesheet" href="../app.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="login-page">
|
||||
<div class="login-card glass">
|
||||
<h2 style="font-family:'KindelSerif',Georgia,serif;font-size:1.5rem;margin:0 0 6px">Passwort ändern erforderlich</h2>
|
||||
<p class="hint" style="margin:0 0 22px">
|
||||
Für deinen Account wurde ein initiales Passwort vergeben. Lege jetzt dein eigenes Passwort fest,
|
||||
bevor du weiterarbeiten kannst.
|
||||
</p>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="error" style="margin-bottom:16px"><?= e($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post">
|
||||
<?= csrf_field() ?>
|
||||
<label>
|
||||
Aktuelles Passwort
|
||||
<input type="password" name="current_password" autocomplete="current-password" required autofocus>
|
||||
</label>
|
||||
<label>
|
||||
Neues Passwort
|
||||
<input type="password" name="new_password" autocomplete="new-password" required minlength="8">
|
||||
</label>
|
||||
<label>
|
||||
Neues Passwort wiederholen
|
||||
<input type="password" name="new_password_repeat" autocomplete="new-password" required minlength="8">
|
||||
</label>
|
||||
<button type="submit" class="btn" style="width:100%;margin-top:6px">Passwort ändern</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/db.php';
|
||||
require_once __DIR__ . '/../lib/layout.php';
|
||||
require_login();
|
||||
|
||||
$name = current_name();
|
||||
$user_id = current_user()['id'];
|
||||
|
||||
$news_items = db()->query('SELECT * FROM news ORDER BY date DESC, id DESC')->fetchAll();
|
||||
|
||||
$grund_labels = [
|
||||
'arztbesuch' => 'Arztbesuch',
|
||||
'behoerdengang' => 'Behördengang',
|
||||
'sonderurlaub' => 'Sonderurlaub',
|
||||
'elternzeit_pflegezeit' => 'Elternzeit / Pflegezeit',
|
||||
'sonstiges' => 'Sonstiges',
|
||||
];
|
||||
$benefit_labels = [
|
||||
'massage' => 'Massage',
|
||||
'fitnessstudio' => 'Fitnessstudio',
|
||||
'tankgutschein' => 'Tankgutschein',
|
||||
'fortbildung' => 'Fortbildung',
|
||||
];
|
||||
|
||||
$rows = [];
|
||||
|
||||
$stmt = db()->prepare('SELECT id, status, created_at, von, bis, vertretung, nachricht FROM requests_urlaubsantrag WHERE user_id=? ORDER BY created_at DESC');
|
||||
$stmt->execute([$user_id]);
|
||||
foreach ($stmt->fetchAll() as $r) {
|
||||
$rows[] = ['type' => 'urlaub', 'type_label' => 'Urlaubsantrag', 'icon' => '🌴'] + $r + ['detail' => $r['von'] . ' – ' . $r['bis']];
|
||||
}
|
||||
|
||||
$stmt = db()->prepare('SELECT id, status, created_at, von, bis, grund, vertretung, nachricht FROM requests_abwesenheitsantrag WHERE user_id=? ORDER BY created_at DESC');
|
||||
$stmt->execute([$user_id]);
|
||||
foreach ($stmt->fetchAll() as $r) {
|
||||
$rows[] = ['type' => 'abwesenheit', 'type_label' => 'Abwesenheitsantrag', 'icon' => '📅'] + $r + ['detail' => ($grund_labels[$r['grund']] ?? $r['grund']) . ': ' . $r['von'] . ' – ' . $r['bis']];
|
||||
}
|
||||
|
||||
$stmt = db()->prepare('SELECT id, status, created_at, benefit, nachricht FROM requests_benefitsantrag WHERE user_id=? ORDER BY created_at DESC');
|
||||
$stmt->execute([$user_id]);
|
||||
foreach ($stmt->fetchAll() as $r) {
|
||||
$rows[] = ['type' => 'benefits', 'type_label' => 'Benefitsantrag', 'icon' => '🎁'] + $r + ['detail' => $benefit_labels[$r['benefit']] ?? $r['benefit']];
|
||||
}
|
||||
|
||||
$stmt = db()->prepare('SELECT id, status, created_at, name, email, qualifikation, nachricht FROM requests_werben WHERE user_id=? ORDER BY created_at DESC');
|
||||
$stmt->execute([$user_id]);
|
||||
foreach ($stmt->fetchAll() as $r) {
|
||||
$rows[] = ['type' => 'werben', 'type_label' => 'Mitarbeiter werben', 'icon' => '🤝'] + $r + ['detail' => $r['name'] . ($r['qualifikation'] ? ' (' . $r['qualifikation'] . ')' : '')];
|
||||
}
|
||||
|
||||
usort($rows, fn($a, $b) => strcmp($b['created_at'], $a['created_at']));
|
||||
|
||||
$pending_count = count(array_filter($rows, fn($r) => $r['status'] === 'pending'));
|
||||
|
||||
$status_map = ['pending' => 'Ausstehend', 'accepted' => 'Angenommen', 'rejected' => 'Abgelehnt'];
|
||||
|
||||
layout_start('OMSORG Felix – Dashboard', 'dashboard');
|
||||
?>
|
||||
<h1 class="main-title">Willkommen, <span><?= e($name) ?></span>!</h1>
|
||||
<p class="main-sub">Hier siehst du alle deine Anträge auf einen Blick.</p>
|
||||
|
||||
<?php if (!empty($news_items)): ?>
|
||||
<div style="margin:28px 0 8px">
|
||||
<h2 style="font-family:'KindelSerif',Georgia,serif;font-size:1.5rem;margin:0 0 14px">📢 News & Ankündigungen</h2>
|
||||
<?php foreach ($news_items as $n): ?>
|
||||
<div class="glass" style="padding:20px 24px;border-radius:18px;margin-bottom:12px">
|
||||
<div style="color:var(--cyan2);font-size:.82rem;font-weight:900;margin-bottom:5px">
|
||||
<?= e(date('d.m.Y', strtotime($n['date']))) ?>
|
||||
</div>
|
||||
<strong style="font-size:1.05rem;display:block;margin-bottom:6px"><?= e($n['title']) ?></strong>
|
||||
<p style="margin:0;color:var(--muted);line-height:1.6;font-size:.95rem"><?= nl2br(e($n['text'])) ?></p>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($pending_count > 0): ?>
|
||||
<p style="margin:18px 0 0;color:var(--muted)">
|
||||
<?= $pending_count === 1 ? '1 Antrag ist noch' : $pending_count . ' Anträge sind noch' ?> <strong style="color:#ffe566">ausstehend</strong>.
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (empty($rows)): ?>
|
||||
<p style="margin-top:32px;color:var(--muted)">Du hast noch keine Anträge gestellt.</p>
|
||||
<?php else: ?>
|
||||
|
||||
<div class="type-tabs" id="type-tabs">
|
||||
<button class="type-tab active" data-type="all">Alle (<?= count($rows) ?>)</button>
|
||||
<?php
|
||||
$types = [];
|
||||
foreach ($rows as $r) {
|
||||
$types[$r['type']] = ['label' => $r['type_label'], 'icon' => $r['icon'], 'count' => ($types[$r['type']]['count'] ?? 0) + 1];
|
||||
}
|
||||
foreach ($types as $type => $info): ?>
|
||||
<button class="type-tab" data-type="<?= e($type) ?>"><?= $info['icon'] ?> <?= e($info['label']) ?> (<?= $info['count'] ?>)</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<table class="requests-table" style="margin-top:16px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Art</th>
|
||||
<th>Details</th>
|
||||
<th>Eingereicht</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $r): ?>
|
||||
<tr data-type="<?= e($r['type']) ?>">
|
||||
<td><?= $r['icon'] ?> <?= e($r['type_label']) ?></td>
|
||||
<td style="color:var(--muted);font-size:.93rem"><?= e($r['detail']) ?></td>
|
||||
<td style="color:var(--muted);font-size:.88rem;white-space:nowrap"><?= e(date('d.m.Y', strtotime($r['created_at']))) ?></td>
|
||||
<td><span class="badge badge-<?= e($r['status']) ?>"><?= e($status_map[$r['status']] ?? $r['status']) ?></span></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
document.getElementById('type-tabs').addEventListener('click', function(e) {
|
||||
const btn = e.target.closest('.type-tab');
|
||||
if (!btn) return;
|
||||
this.querySelectorAll('.type-tab').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
const type = btn.dataset.type;
|
||||
document.querySelectorAll('.requests-table tbody tr').forEach(row => {
|
||||
row.style.display = (type === 'all' || row.dataset.type === type) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php endif; ?>
|
||||
<?php layout_end(); ?>
|
||||
|
||||
<div id="pwa-install-banner" style="display:none;position:fixed;bottom:0;left:0;right:0;background:#1a3a5c;color:#fff;padding:.85rem 1rem;z-index:8000;align-items:center;gap:.75rem;box-shadow:0 -2px 12px rgba(0,0,0,.25);">
|
||||
<span style="flex:1;font-size:.95rem;">📲 <strong>OMSORG App</strong> zum Home-Bildschirm hinzufügen</span>
|
||||
<button id="pwa-install-confirm" type="button" style="background:#fff;color:#1a3a5c;border:none;border-radius:8px;padding:.45rem 1rem;font-weight:700;cursor:pointer;white-space:nowrap;">Installieren</button>
|
||||
<button id="pwa-install-dismiss" type="button" style="background:transparent;color:#fff;border:none;font-size:1.2rem;cursor:pointer;padding:0 .25rem;line-height:1;">✕</button>
|
||||
</div>
|
||||
|
||||
<div id="pwa-install-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9000;align-items:flex-end;justify-content:center;padding:1rem;" role="dialog" aria-modal="true">
|
||||
<div style="background:#fff;border-radius:16px;padding:1.5rem;width:100%;max-width:440px;color:#1a3a5c;">
|
||||
<p style="margin:0 0 1.25rem;font-weight:700;font-size:1.1rem;text-align:center;">📲 App installieren</p>
|
||||
<div style="background:#f0f4ff;border-radius:12px;padding:1rem 1.1rem;margin-bottom:.75rem;">
|
||||
<p style="margin:0 0 .4rem;font-weight:700;font-size:.95rem;">🤖 Android (Chrome)</p>
|
||||
<p style="margin:0;font-size:.9rem;line-height:1.6;color:#444;">Tippe oben rechts auf das <strong>Menü ⋮</strong> und dann auf<br><strong>„App installieren"</strong> oder <strong>„Zum Startbildschirm hinzufügen"</strong>.</p>
|
||||
</div>
|
||||
<div style="background:#f0f4ff;border-radius:12px;padding:1rem 1.1rem;margin-bottom:1.25rem;">
|
||||
<p style="margin:0 0 .4rem;font-weight:700;font-size:.95rem;">🍎 iPhone / iPad (Safari)</p>
|
||||
<p style="margin:0;font-size:.9rem;line-height:1.6;color:#444;">Tippe unten auf das <strong>Teilen-Symbol ⬆</strong> und dann auf<br><strong>„Zum Home-Bildschirm"</strong>.</p>
|
||||
</div>
|
||||
<button id="pwa-modal-close" style="display:block;width:100%;background:#1a3a5c;color:#fff;border:none;border-radius:8px;padding:.65rem;cursor:pointer;font-size:.95rem;font-weight:600;">Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
if(window.matchMedia('(display-mode: standalone)').matches)return;
|
||||
if(document.cookie.split(';').some(c=>c.trim()==='pwa_dismissed=1'))return;
|
||||
const banner=document.getElementById('pwa-install-banner');
|
||||
const modal=document.getElementById('pwa-install-modal');
|
||||
let deferred=null;
|
||||
window.addEventListener('beforeinstallprompt',function(e){
|
||||
e.preventDefault();
|
||||
deferred=e;
|
||||
});
|
||||
banner.style.display='flex';
|
||||
document.getElementById('pwa-install-confirm').addEventListener('click',function(){
|
||||
if(deferred){
|
||||
deferred.prompt();
|
||||
banner.style.display='none';
|
||||
} else {
|
||||
banner.style.display='none';
|
||||
modal.style.display='flex';
|
||||
}
|
||||
});
|
||||
function dismiss(){
|
||||
banner.style.display='none';
|
||||
document.cookie='pwa_dismissed=1;path=/;max-age='+60*60*24*365;
|
||||
}
|
||||
document.getElementById('pwa-install-dismiss').addEventListener('click',dismiss);
|
||||
document.getElementById('pwa-modal-close').addEventListener('click',function(){
|
||||
modal.style.display='none';
|
||||
dismiss();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/db.php';
|
||||
require_once __DIR__ . '/../lib/layout.php';
|
||||
require_login();
|
||||
|
||||
$user = current_user();
|
||||
$user_id = $user['id'];
|
||||
|
||||
$now = new DateTimeImmutable('today');
|
||||
$year = isset($_GET['year']) ? (int)$_GET['year'] : (int)$now->format('Y');
|
||||
$month = isset($_GET['month']) ? (int)$_GET['month'] : (int)$now->format('n');
|
||||
$year = max(2020, min(2099, $year));
|
||||
$month = max(1, min(12, $month));
|
||||
|
||||
$first = new DateTimeImmutable(sprintf('%04d-%02d-01', $year, $month));
|
||||
$month_start = $first->format('Y-m-d');
|
||||
$month_end = $first->modify('last day of this month')->format('Y-m-d');
|
||||
$days = (int)$first->format('t');
|
||||
|
||||
$prev = $first->modify('-1 month');
|
||||
$next = $first->modify('+1 month');
|
||||
$prev_url = '?year=' . $prev->format('Y') . '&month=' . $prev->format('n');
|
||||
$next_url = '?year=' . $next->format('Y') . '&month=' . $next->format('n');
|
||||
|
||||
$stmt = db()->prepare(
|
||||
'SELECT date, schicht FROM dienstplan WHERE user_id = ? AND date >= ? AND date <= ?'
|
||||
);
|
||||
$stmt->execute([$user_id, $month_start, $month_end]);
|
||||
$schichten = [];
|
||||
foreach ($stmt->fetchAll() as $row) {
|
||||
$schichten[$row['date']] = $row['schicht'];
|
||||
}
|
||||
|
||||
$stmt = db()->prepare("
|
||||
SELECT von, bis, 'urlaub' AS typ
|
||||
FROM requests_urlaubsantrag
|
||||
WHERE user_id = ? AND status = 'accepted' AND bis >= ? AND von <= ?
|
||||
UNION ALL
|
||||
SELECT von, bis, 'abwesenheit' AS typ
|
||||
FROM requests_abwesenheitsantrag
|
||||
WHERE user_id = ? AND status = 'accepted' AND bis >= ? AND von <= ?
|
||||
");
|
||||
$stmt->execute([$user_id, $month_start, $month_end, $user_id, $month_start, $month_end]);
|
||||
$overlays = [];
|
||||
foreach ($stmt->fetchAll() as $row) {
|
||||
$cursor = new DateTimeImmutable($row['von']);
|
||||
$end = new DateTimeImmutable($row['bis']);
|
||||
while ($cursor <= $end) {
|
||||
$key = $cursor->format('Y-m-d');
|
||||
if ($key >= $month_start && $key <= $month_end) {
|
||||
$overlays[$key] = $row['typ'];
|
||||
}
|
||||
$cursor = $cursor->modify('+1 day');
|
||||
}
|
||||
}
|
||||
|
||||
$today_str = $now->format('Y-m-d');
|
||||
|
||||
$ea_stmt = db()->prepare('SELECT ort FROM einsatzanweisung WHERE user_id = ?');
|
||||
$ea_stmt->execute([$user_id]);
|
||||
$ea_row = $ea_stmt->fetch();
|
||||
$ea_ort = ($ea_row && $ea_row['ort'] !== '') ? $ea_row['ort'] : '';
|
||||
|
||||
$schicht_labels = ['frueh' => 'Früh', 'spaet' => 'Spät', 'nacht' => 'Nacht'];
|
||||
$weekdays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
||||
$months_de = ['', 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
||||
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'];
|
||||
|
||||
layout_start('Dienstplan', 'dienstplan');
|
||||
?>
|
||||
|
||||
<h1 class="main-title">Dienstplan</h1>
|
||||
|
||||
<?php if ($ea_ort !== ''): ?>
|
||||
<p class="dp-ort">📍 Einsatzort: <strong><?= e($ea_ort) ?></strong></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="dp-nav">
|
||||
<div style="display:flex;gap:10px;align-items:center">
|
||||
<a href="<?= e($prev_url) ?>" class="dp-nav-btn">←</a>
|
||||
<h2 class="dp-nav-title"><?= e($months_de[$month] . ' ' . $year) ?></h2>
|
||||
<a href="<?= e($next_url) ?>" class="dp-nav-btn">→</a>
|
||||
</div>
|
||||
<span id="dp-status" style="font-size:.85rem;color:var(--muted);transition:opacity .4s"></span>
|
||||
</div>
|
||||
|
||||
<div class="dp-grid-wrap">
|
||||
<div class="dp-grid">
|
||||
<?php foreach ($weekdays as $wd): ?>
|
||||
<div class="dp-weekday"><?= e($wd) ?></div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php
|
||||
$lead = (int)$first->format('N') - 1;
|
||||
for ($i = 0; $i < $lead; $i++): ?>
|
||||
<div class="dp-cell dp-cell-empty"></div>
|
||||
<?php endfor; ?>
|
||||
|
||||
<?php for ($d = 1; $d <= $days; $d++):
|
||||
$date_key = sprintf('%04d-%02d-%02d', $year, $month, $d);
|
||||
$is_today = ($date_key === $today_str);
|
||||
$overlay = $overlays[$date_key] ?? null;
|
||||
$schicht = $schichten[$date_key] ?? 'leer';
|
||||
$cell_cls = 'dp-cell';
|
||||
if (!$overlay && $schicht !== 'leer') $cell_cls .= ' dp-cell-' . $schicht;
|
||||
if ($is_today) $cell_cls .= ' dp-cell-today';
|
||||
?>
|
||||
<div class="<?= $cell_cls ?>">
|
||||
<span class="dp-day-num"><?= $d ?></span>
|
||||
<?php if ($overlay): ?>
|
||||
<span class="dp-badge dp-overlay-<?= e($overlay) ?>">
|
||||
<?= $overlay === 'urlaub' ? 'Urlaub' : 'Abwesenheit' ?>
|
||||
</span>
|
||||
<?php if ($schicht !== 'leer'): ?>
|
||||
<span class="dp-badge dp-badge-<?= e($schicht) ?>" style="opacity:.45">
|
||||
<?= e($schicht_labels[$schicht]) ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<select data-date="<?= e($date_key) ?>" class="dp-select">
|
||||
<option value="leer" <?= $schicht === 'leer' ? 'selected' : '' ?>>Frei</option>
|
||||
<option value="frueh" <?= $schicht === 'frueh' ? 'selected' : '' ?>>Früh</option>
|
||||
<option value="spaet" <?= $schicht === 'spaet' ? 'selected' : '' ?>>Spät</option>
|
||||
<option value="nacht" <?= $schicht === 'nacht' ? 'selected' : '' ?>>Nacht</option>
|
||||
</select>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endfor; ?>
|
||||
|
||||
<?php
|
||||
$total = $lead + $days;
|
||||
$trail = (7 - ($total % 7)) % 7;
|
||||
for ($i = 0; $i < $trail; $i++): ?>
|
||||
<div class="dp-cell dp-cell-empty"></div>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
var YEAR = <?= (int)$year ?>;
|
||||
var MONTH = <?= (int)$month ?>;
|
||||
|
||||
function colorCell(sel){
|
||||
var cell = sel.closest('.dp-cell');
|
||||
cell.classList.remove('dp-cell-frueh','dp-cell-spaet','dp-cell-nacht');
|
||||
if(sel.value !== 'leer') cell.classList.add('dp-cell-' + sel.value);
|
||||
}
|
||||
|
||||
var statusEl = document.getElementById('dp-status');
|
||||
var statusTimer;
|
||||
function showStatus(ok){
|
||||
clearTimeout(statusTimer);
|
||||
statusEl.style.opacity = '1';
|
||||
statusEl.style.color = ok ? '#6eff9a' : '#ff9a9a';
|
||||
statusEl.textContent = ok ? 'Gespeichert' : 'Fehler beim Speichern';
|
||||
statusTimer = setTimeout(function(){ statusEl.style.opacity = '0'; }, 1800);
|
||||
}
|
||||
|
||||
document.querySelectorAll('.dp-select').forEach(function(sel){
|
||||
colorCell(sel);
|
||||
sel.addEventListener('change', function(){
|
||||
var date = this.dataset.date;
|
||||
var schicht = this.value;
|
||||
colorCell(this);
|
||||
|
||||
var body = new URLSearchParams();
|
||||
body.append('year', YEAR);
|
||||
body.append('month', MONTH);
|
||||
body.append('schicht[' + date + ']', schicht);
|
||||
body.append('_ajax', '1');
|
||||
body.append('csrf_token', document.querySelector('meta[name="csrf-token"]').content);
|
||||
|
||||
fetch('../actions/save-dienstplan.php', {method:'POST', body:body})
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(d){ showStatus(d.ok); })
|
||||
.catch(function(){ showStatus(false); });
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
layout_end();
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_login();
|
||||
|
||||
$id = (int) ($_GET['id'] ?? 0);
|
||||
if ($id <= 0) { http_response_code(400); echo 'Ungültige Anfrage.'; exit; }
|
||||
|
||||
$stmt = db()->prepare('SELECT * FROM dokumente WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
$doc = $stmt->fetch();
|
||||
|
||||
if (!$doc) { http_response_code(404); echo 'Dokument nicht gefunden.'; exit; }
|
||||
|
||||
$user = current_user();
|
||||
if ((int) $doc['user_id'] !== (int) $user['id'] && !is_admin()) {
|
||||
http_response_code(403);
|
||||
echo 'Keine Berechtigung.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$path = dirname(__DIR__) . '/uploads/' . $doc['filename'];
|
||||
if (!is_file($path)) { http_response_code(404); echo 'Datei nicht gefunden.'; exit; }
|
||||
|
||||
$display = $doc['original_name'];
|
||||
$safe_display = addslashes($display);
|
||||
$safe_encoded = rawurlencode($display);
|
||||
|
||||
header('Content-Type: application/octet-stream');
|
||||
header('Content-Disposition: attachment; filename="' . $safe_display . '"; filename*=UTF-8\'\'' . $safe_encoded);
|
||||
header('Content-Length: ' . filesize($path));
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Cache-Control: private, no-store');
|
||||
readfile($path);
|
||||
exit;
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/layout.php';
|
||||
require_login();
|
||||
|
||||
$user_id = current_user()['id'];
|
||||
$admin = is_admin();
|
||||
|
||||
if ($admin) {
|
||||
$docs = db()->query(
|
||||
'SELECT d.*, u.name AS user_name
|
||||
FROM dokumente d
|
||||
JOIN users u ON u.id = d.user_id
|
||||
ORDER BY d.created_at DESC'
|
||||
)->fetchAll();
|
||||
} else {
|
||||
$stmt = db()->prepare('SELECT * FROM dokumente WHERE user_id = ? ORDER BY created_at DESC');
|
||||
$stmt->execute([$user_id]);
|
||||
$docs = $stmt->fetchAll();
|
||||
}
|
||||
|
||||
layout_start('OMSORG – Dokumentenarchiv', 'dokumentenarchiv');
|
||||
?>
|
||||
<h1 class="main-title">🗂 Dokumenten­archiv</h1>
|
||||
<p class="main-sub">Eigene Nachweise und Dokumente hochladen und verwalten.</p>
|
||||
|
||||
<?php if (isset($_GET['ok'])): ?>
|
||||
<div class="success" style="margin-top:16px">Dokument erfolgreich hochgeladen.</div>
|
||||
<?php elseif (isset($_GET['deleted'])): ?>
|
||||
<div class="success" style="margin-top:16px">Dokument wurde gelöscht.</div>
|
||||
<?php elseif (isset($_GET['error'])): ?>
|
||||
<div class="error" style="margin-top:16px"><?= e(urldecode($_GET['error'])) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="page-split" style="margin-top:24px">
|
||||
|
||||
<div class="page-split-list">
|
||||
<h2><?= $admin ? 'Alle Dokumente' : 'Meine Dokumente' ?></h2>
|
||||
<?php if (empty($docs)): ?>
|
||||
<p style="color:var(--muted)">Noch keine Dokumente vorhanden.</p>
|
||||
<?php else: ?>
|
||||
<table class="requests-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<?php if ($admin): ?><th>Mitarbeiter</th><?php endif; ?>
|
||||
<th>Kategorie</th>
|
||||
<th>Beschreibung</th>
|
||||
<th>Datei</th>
|
||||
<th>Hochgeladen</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($docs as $d): ?>
|
||||
<tr>
|
||||
<?php if ($admin): ?>
|
||||
<td style="font-weight:800"><?= e($d['user_name']) ?></td>
|
||||
<?php endif; ?>
|
||||
<td><?= e($d['kategorie']) ?></td>
|
||||
<td style="color:var(--muted);font-size:.9rem"><?= $d['beschreibung'] !== '' ? e($d['beschreibung']) : '–' ?></td>
|
||||
<td>
|
||||
<a href="dokument-serve.php?id=<?= (int)$d['id'] ?>" class="btn download-btn" style="padding:6px 14px;font-size:.85rem">↓ <?= e($d['original_name']) ?></a>
|
||||
</td>
|
||||
<td style="color:var(--muted);font-size:.9rem"><?= e(date('d.m.Y', strtotime($d['created_at']))) ?></td>
|
||||
<td>
|
||||
<form method="post" action="../actions/delete-dokument.php" onsubmit="return confirm('Dokument wirklich löschen?')">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$d['id'] ?>">
|
||||
<button type="submit" class="btn" style="background:rgba(220,50,50,.25);padding:6px 14px;font-size:.85rem">Löschen</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="page-split-form">
|
||||
<div class="glass detail-card">
|
||||
<h2>Dokument hochladen</h2>
|
||||
<form action="../actions/upload-dokument.php" method="post" enctype="multipart/form-data">
|
||||
<?= csrf_field() ?>
|
||||
<label>
|
||||
<span>Kategorie <span style="color:#ff8080">*</span></span>
|
||||
<select name="kategorie" required>
|
||||
<option value="">– Bitte wählen –</option>
|
||||
<option value="Fortbildungsnachweis">Fortbildungsnachweis</option>
|
||||
<option value="Zeugnis">Zeugnis</option>
|
||||
<option value="Bescheinigung">Bescheinigung</option>
|
||||
<option value="Sonstiges">Sonstiges</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Beschreibung
|
||||
<input type="text" name="beschreibung" placeholder="z. B. Erste-Hilfe-Kurs 2025">
|
||||
</label>
|
||||
<label>
|
||||
<span>Datei <span style="color:#ff8080">*</span></span>
|
||||
<input type="file" name="datei" accept=".pdf,.doc,.docx,.jpg,.jpeg,.png" required>
|
||||
<span class="hint">PDF, Word, JPG, PNG — max. 12 MB</span>
|
||||
</label>
|
||||
<button type="submit" class="btn" style="margin-top:6px">Hochladen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php layout_end(); ?>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_login();
|
||||
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
if ($id <= 0) { http_response_code(400); echo 'Ungültige Anfrage.'; exit; }
|
||||
|
||||
$stmt = db()->prepare('SELECT filename, original_name FROM downloads WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) { http_response_code(404); echo 'Datei nicht gefunden.'; exit; }
|
||||
|
||||
$path = dirname(__DIR__) . '/downloads/' . $row['filename'];
|
||||
if (!is_file($path)) { http_response_code(404); echo 'Datei nicht gefunden.'; exit; }
|
||||
|
||||
$safe_display = addslashes($row['original_name']);
|
||||
$safe_encoded = rawurlencode($row['original_name']);
|
||||
|
||||
header('Content-Type: application/octet-stream');
|
||||
header('Content-Disposition: attachment; filename="' . $safe_display . '"; filename*=UTF-8\'\'' . $safe_encoded);
|
||||
header('Content-Length: ' . filesize($path));
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Cache-Control: private, no-store');
|
||||
readfile($path);
|
||||
exit;
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/layout.php';
|
||||
require_login();
|
||||
|
||||
$downloads = db()->query('SELECT * FROM downloads ORDER BY sort_order ASC, id ASC')->fetchAll();
|
||||
|
||||
layout_start('OMSORG – Downloads', 'downloads');
|
||||
?>
|
||||
<h1 class="main-title">Down<span>loads</span></h1>
|
||||
<p class="main-sub">Dokumente und Formulare zum Herunterladen.</p>
|
||||
|
||||
<?php if (empty($downloads)): ?>
|
||||
<p style="color:var(--muted);margin-top:40px">Noch keine Downloads verfügbar.</p>
|
||||
<?php else: ?>
|
||||
<div class="downloads-grid">
|
||||
<?php foreach ($downloads as $dl): ?>
|
||||
<div class="glass download-card">
|
||||
<h3 class="download-card-title"><?= e($dl['title']) ?></h3>
|
||||
<?php if ($dl['description'] !== ''): ?>
|
||||
<p class="download-card-desc"><?= e($dl['description']) ?></p>
|
||||
<?php endif; ?>
|
||||
<a href="download-serve.php?id=<?= (int)$dl['id'] ?>" class="btn download-btn">↓ Herunterladen</a>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php layout_end(); ?>
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_login();
|
||||
|
||||
$current = current_user();
|
||||
|
||||
if (is_admin() && isset($_GET['user_id'])) {
|
||||
$target_id = (int)$_GET['user_id'];
|
||||
} else {
|
||||
$target_id = (int)$current['id'];
|
||||
}
|
||||
|
||||
$stmt = db()->prepare('SELECT * FROM einsatzanweisung WHERE user_id = ?');
|
||||
$stmt->execute([$target_id]);
|
||||
$ea = $stmt->fetch();
|
||||
|
||||
if (!$ea || $ea['filename'] === '') {
|
||||
http_response_code(404);
|
||||
echo 'Keine Datei gefunden.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$path = dirname(__DIR__) . '/uploads/' . basename($ea['filename']);
|
||||
if (!is_file($path)) {
|
||||
http_response_code(404);
|
||||
echo 'Datei nicht gefunden.';
|
||||
exit;
|
||||
}
|
||||
|
||||
$display = $ea['original_name'];
|
||||
$safe_display = addslashes($display);
|
||||
$safe_encoded = rawurlencode($display);
|
||||
|
||||
header('Content-Type: application/pdf');
|
||||
header('Content-Disposition: attachment; filename="' . $safe_display . '"; filename*=UTF-8\'\'' . $safe_encoded);
|
||||
header('Content-Length: ' . filesize($path));
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Cache-Control: private, no-store');
|
||||
readfile($path);
|
||||
exit;
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/layout.php';
|
||||
require_login();
|
||||
|
||||
$user_id = current_user()['id'];
|
||||
$stmt = db()->prepare('SELECT * FROM einsatzanweisung WHERE user_id = ?');
|
||||
$stmt->execute([$user_id]);
|
||||
$ea = $stmt->fetch();
|
||||
|
||||
layout_start('OMSORG – Einsatzanweisung', 'einsatzanweisung');
|
||||
?>
|
||||
<h1 class="main-title">Einsatz<span>anweisung</span></h1>
|
||||
<p class="main-sub">Deine persönliche Einsatzanweisung als PDF-Dokument.</p>
|
||||
|
||||
<?php if (!$ea || ($ea['filename'] === '' && $ea['ort'] === '')): ?>
|
||||
<p style="color:var(--muted);margin-top:40px">Noch keine Einsatzanweisung hinterlegt.</p>
|
||||
<?php else: ?>
|
||||
<div class="glass" style="max-width:520px;padding:28px;border-radius:20px;margin-top:24px">
|
||||
<?php if ($ea['ort'] !== ''): ?>
|
||||
<div style="margin-bottom:24px">
|
||||
<div style="color:var(--cyan2);font-size:.78rem;font-weight:900;text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px">Einsatzort</div>
|
||||
<div style="font-size:1.15rem;font-weight:800"><?= e($ea['ort']) ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($ea['filename'] !== ''): ?>
|
||||
<div>
|
||||
<div style="color:var(--cyan2);font-size:.78rem;font-weight:900;text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px">Dokument</div>
|
||||
<a href="einsatzanweisung-serve.php" class="btn" style="display:inline-flex;align-items:center;gap:8px">
|
||||
↓ <?= e($ea['original_name']) ?>
|
||||
</a>
|
||||
<?php if ($ea['uploaded_at'] !== ''): ?>
|
||||
<div style="color:var(--muted);font-size:.82rem;margin-top:8px">
|
||||
Hochgeladen: <?= e(date('d.m.Y', strtotime($ea['uploaded_at']))) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php layout_end(); ?>
|
||||
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/db.php';
|
||||
require_once __DIR__ . '/../lib/layout.php';
|
||||
require_login();
|
||||
|
||||
$user_id = current_user()['id'];
|
||||
|
||||
$stmt = db()->prepare('SELECT * FROM einsatzbewertungen WHERE user_id = ? ORDER BY created_at DESC');
|
||||
$stmt->execute([$user_id]);
|
||||
$bewertungen = $stmt->fetchAll();
|
||||
|
||||
$extra_head = <<<'CSS'
|
||||
<style>
|
||||
.star-rating{display:flex;flex-direction:row-reverse;gap:4px;justify-content:flex-end}
|
||||
.star-rating input{display:none}
|
||||
.star-rating label{
|
||||
font-size:2rem;cursor:pointer;color:rgba(170,240,250,.25);
|
||||
transition:color .1s;line-height:1;
|
||||
}
|
||||
.star-rating input:checked ~ label,
|
||||
.star-rating label:hover,
|
||||
.star-rating label:hover ~ label{color:#f5c518}
|
||||
.star-display{color:#f5c518;letter-spacing:2px;font-size:1.1rem}
|
||||
.star-display .empty{color:rgba(170,240,250,.2)}
|
||||
.modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.55);display:none;align-items:center;justify-content:center;z-index:1000}
|
||||
.modal-box{background:var(--glass-bg,#1e2235);border:1px solid var(--border,rgba(255,255,255,.1));border-radius:14px;padding:28px 32px;max-width:480px;width:90%;position:relative}
|
||||
.modal-box h3{margin:0 0 20px;font-family:'KindelSerif',Georgia,serif;font-size:1.3rem}
|
||||
.modal-close{position:absolute;top:12px;right:16px;background:none;border:none;font-size:1.5rem;cursor:pointer;color:var(--muted,#888);line-height:1}
|
||||
.modal-dl{display:grid;grid-template-columns:auto 1fr;gap:8px 16px;font-size:.95rem}
|
||||
.modal-dl dt{color:var(--muted,#888);white-space:nowrap}
|
||||
.modal-dl dd{margin:0;font-weight:500}
|
||||
.btn-sm{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:9px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.08);color:#fff;font-weight:600;font-size:.82rem;cursor:pointer;font:inherit;transition:background .15s}
|
||||
.btn-sm:hover{background:rgba(255,255,255,.15)}
|
||||
</style>
|
||||
CSS;
|
||||
|
||||
layout_start('OMSORG – Einsatzbewertung', 'einsatzbewertung', $extra_head);
|
||||
?>
|
||||
<h1 class="main-title">⭐ Einsatzbewertung</h1>
|
||||
<p class="main-sub">Bewerte deinen Einsatzort. Dein Feedback hilft uns, die Einsatzplanung zu verbessern.</p>
|
||||
|
||||
<?php if (isset($_GET['success'])): ?>
|
||||
<div class="success" style="margin-top:16px">Bewertung erfolgreich abgegeben.</div>
|
||||
<?php elseif (isset($_GET['error'])): ?>
|
||||
<div class="error" style="margin-top:16px"><?= e(urldecode($_GET['error'])) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div id="bew-modal" class="modal-backdrop" onclick="if(event.target===this)this.style.display='none'">
|
||||
<div class="modal-box glass">
|
||||
<button class="modal-close" onclick="document.getElementById('bew-modal').style.display='none'">×</button>
|
||||
<h3>Bewertung Details</h3>
|
||||
<dl class="modal-dl">
|
||||
<dt>Einsatzort</dt> <dd id="bew-ort">–</dd>
|
||||
<dt>Von</dt> <dd id="bew-von">–</dd>
|
||||
<dt>Bis</dt> <dd id="bew-bis">–</dd>
|
||||
<dt>Bewertung</dt> <dd id="bew-sterne">–</dd>
|
||||
<dt>Wieder?</dt> <dd id="bew-wieder">–</dd>
|
||||
<dt>Feedback</dt> <dd id="bew-feedback">–</dd>
|
||||
<dt>Datum</dt> <dd id="bew-datum">–</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-split">
|
||||
|
||||
<div class="page-split-list">
|
||||
<h2>Meine Bewertungen</h2>
|
||||
<?php if (empty($bewertungen)): ?>
|
||||
<p style="color:var(--muted)">Noch keine Bewertungen abgegeben.</p>
|
||||
<?php else: ?>
|
||||
<table class="requests-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Einsatzort</th>
|
||||
<th>Bewertung</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($bewertungen as $b): ?>
|
||||
<tr>
|
||||
<td style="font-weight:800"><?= e($b['einsatzort']) ?></td>
|
||||
<td>
|
||||
<span class="star-display">
|
||||
<?php for ($i = 1; $i <= 5; $i++): ?>
|
||||
<span<?= $i > $b['bewertung'] ? ' class="empty"' : '' ?>>★</span>
|
||||
<?php endfor; ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn-sm js-bew-details"
|
||||
data-ort="<?= e($b['einsatzort']) ?>"
|
||||
data-von="<?= e(date('d.m.Y', strtotime($b['von']))) ?>"
|
||||
data-bis="<?= e(date('d.m.Y', strtotime($b['bis']))) ?>"
|
||||
data-bewertung="<?= (int)$b['bewertung'] ?>"
|
||||
data-wieder="<?= (int)$b['wieder'] ?>"
|
||||
data-feedback="<?= e($b['feedback'] !== '' ? $b['feedback'] : '–') ?>"
|
||||
data-datum="<?= e(date('d.m.Y', strtotime($b['created_at']))) ?>"
|
||||
>Details</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="page-split-form">
|
||||
<div class="glass detail-card">
|
||||
<h2>Neue Bewertung</h2>
|
||||
<form action="../actions/submit-einsatzbewertung.php" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<label>
|
||||
<span>Einsatzort / Einrichtung <span style="color:#ff8080">*</span></span>
|
||||
<input type="text" name="einsatzort" required placeholder="z. B. Pflegeheim Sonnenhof">
|
||||
</label>
|
||||
<label>
|
||||
<span>Von <span style="color:#ff8080">*</span></span>
|
||||
<input type="date" name="von" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Bis <span style="color:#ff8080">*</span></span>
|
||||
<input type="date" name="bis" required>
|
||||
</label>
|
||||
<div style="margin-bottom:14px">
|
||||
<span style="display:block;margin-bottom:8px;font-weight:700">
|
||||
Bewertung <span style="color:#ff8080">*</span>
|
||||
</span>
|
||||
<div class="star-rating">
|
||||
<input type="radio" id="star5" name="bewertung" value="5" required>
|
||||
<label for="star5" title="5 Sterne">★</label>
|
||||
<input type="radio" id="star4" name="bewertung" value="4">
|
||||
<label for="star4" title="4 Sterne">★</label>
|
||||
<input type="radio" id="star3" name="bewertung" value="3">
|
||||
<label for="star3" title="3 Sterne">★</label>
|
||||
<input type="radio" id="star2" name="bewertung" value="2">
|
||||
<label for="star2" title="2 Sterne">★</label>
|
||||
<input type="radio" id="star1" name="bewertung" value="1">
|
||||
<label for="star1" title="1 Stern">★</label>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom:14px">
|
||||
<label style="display:flex;align-items:center;gap:10px;flex-direction:row;font-weight:700;cursor:pointer">
|
||||
<input type="checkbox" name="wieder" value="1" style="width:18px;height:18px;cursor:pointer">
|
||||
Würdest du wieder dort arbeiten?
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
Feedback
|
||||
<textarea name="feedback" rows="4" placeholder="Optionale Anmerkungen zu diesem Einsatz"></textarea>
|
||||
</label>
|
||||
<button type="submit" class="btn" style="margin-top:6px">Bewertung abgeben</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('click', function(e) {
|
||||
var btn = e.target.closest('.js-bew-details');
|
||||
if (!btn) return;
|
||||
var d = btn.dataset;
|
||||
document.getElementById('bew-ort').textContent = d.ort;
|
||||
document.getElementById('bew-von').textContent = d.von;
|
||||
document.getElementById('bew-bis').textContent = d.bis;
|
||||
var n = parseInt(d.bewertung, 10);
|
||||
var stars = '';
|
||||
for (var i = 1; i <= 5; i++) stars += '<span' + (i > n ? ' class="empty"' : '') + '>★</span>';
|
||||
document.getElementById('bew-sterne').innerHTML = '<span class="star-display">' + stars + '</span>';
|
||||
document.getElementById('bew-wieder').innerHTML =
|
||||
parseInt(d.wieder) ? '<span style="color:#6eff9a;font-weight:800">Ja</span>' : '<span style="color:var(--muted)">Nein</span>';
|
||||
document.getElementById('bew-feedback').textContent = d.feedback;
|
||||
document.getElementById('bew-datum').textContent = d.datum;
|
||||
document.getElementById('bew-modal').style.display = 'flex';
|
||||
});
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') document.getElementById('bew-modal').style.display = 'none';
|
||||
});
|
||||
</script>
|
||||
<?php layout_end(); ?>
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
|
||||
if (is_logged_in()) {
|
||||
header('Location: dashboard.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$step = $_POST['step'] ?? 'request';
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
verify_csrf();
|
||||
|
||||
if ($step === 'request') {
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
if ($username === '') {
|
||||
$error = 'Bitte Benutzername eingeben.';
|
||||
} else {
|
||||
omsorgcore_forgot_password_request(_omsorgcore_config(), $username);
|
||||
// Bewusst immer weiter zu Schritt 2, unabhängig vom Ergebnis - kein Rückschluss darauf,
|
||||
// ob der Username existiert (siehe PasswordResetService.RequestResetAsync in omsorgCore).
|
||||
$step = 'verify';
|
||||
}
|
||||
} elseif ($step === 'verify') {
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$pin = trim($_POST['pin'] ?? '');
|
||||
$result = omsorgcore_forgot_password_verify(_omsorgcore_config(), $username, $pin);
|
||||
|
||||
if ($result['ok']) {
|
||||
$resetToken = $result['data']['resetToken'];
|
||||
$step = 'reset';
|
||||
} else {
|
||||
$error = 'Code ungültig oder abgelaufen.';
|
||||
}
|
||||
} elseif ($step === 'reset') {
|
||||
$resetToken = $_POST['reset_token'] ?? '';
|
||||
$new = $_POST['new_password'] ?? '';
|
||||
$repeat = $_POST['new_password_repeat'] ?? '';
|
||||
|
||||
if (strlen($new) < 8) {
|
||||
$error = 'Das neue Passwort muss mindestens 8 Zeichen lang sein.';
|
||||
} elseif ($new !== $repeat) {
|
||||
$error = 'Die Passwörter stimmen nicht überein.';
|
||||
} else {
|
||||
$result = omsorgcore_forgot_password_reset(_omsorgcore_config(), $resetToken, $new);
|
||||
if ($result['ok']) {
|
||||
header('Location: ../index.php?pwchanged=1');
|
||||
exit;
|
||||
}
|
||||
$error = 'Zurücksetzen fehlgeschlagen. Bitte Vorgang erneut starten.';
|
||||
$step = 'request';
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>OMSORG Felix – Passwort vergessen</title>
|
||||
<link rel="stylesheet" href="../app.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="login-page">
|
||||
<div class="login-card glass">
|
||||
<h2 style="font-family:'KindelSerif',Georgia,serif;font-size:1.5rem;margin:0 0 6px">Passwort vergessen</h2>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="error" style="margin-bottom:16px"><?= e($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($step === 'request'): ?>
|
||||
<p class="hint" style="margin:0 0 22px">Gib deinen Benutzernamen ein, wir senden dir einen Code per E-Mail.</p>
|
||||
<form method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="step" value="request">
|
||||
<label>
|
||||
Benutzername
|
||||
<input type="text" name="username" autocomplete="username" required autofocus>
|
||||
</label>
|
||||
<button type="submit" class="btn" style="width:100%;margin-top:6px">Code anfordern</button>
|
||||
</form>
|
||||
|
||||
<?php elseif ($step === 'verify'): ?>
|
||||
<p class="hint" style="margin:0 0 22px">Gib den Code ein, den wir dir per E-Mail geschickt haben.</p>
|
||||
<form method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="step" value="verify">
|
||||
<input type="hidden" name="username" value="<?= e($username ?? '') ?>">
|
||||
<label>
|
||||
Code
|
||||
<input type="text" name="pin" inputmode="numeric" pattern="[0-9]*" maxlength="6" required autofocus>
|
||||
</label>
|
||||
<button type="submit" class="btn" style="width:100%;margin-top:6px">Code bestätigen</button>
|
||||
</form>
|
||||
|
||||
<?php elseif ($step === 'reset'): ?>
|
||||
<p class="hint" style="margin:0 0 22px">Lege dein neues Passwort fest.</p>
|
||||
<form method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="step" value="reset">
|
||||
<input type="hidden" name="reset_token" value="<?= e($resetToken ?? '') ?>">
|
||||
<label>
|
||||
Neues Passwort
|
||||
<input type="password" name="new_password" autocomplete="new-password" required minlength="8" autofocus>
|
||||
</label>
|
||||
<label>
|
||||
Neues Passwort wiederholen
|
||||
<input type="password" name="new_password_repeat" autocomplete="new-password" required minlength="8">
|
||||
</label>
|
||||
<button type="submit" class="btn" style="width:100%;margin-top:6px">Passwort setzen</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<p class="hint" style="text-align:center;margin-top:14px">
|
||||
<a href="../index.php">Zurück zum Login</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_login();
|
||||
|
||||
$id = (int)($_GET['id'] ?? 0);
|
||||
if ($id <= 0) { http_response_code(400); echo 'Ungültige Anfrage.'; exit; }
|
||||
|
||||
$stmt = db()->prepare('SELECT filename, original_name FROM fortbildung_materials WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) { http_response_code(404); echo 'Datei nicht gefunden.'; exit; }
|
||||
|
||||
$path = dirname(__DIR__) . '/fortbildung-materials/' . $row['filename'];
|
||||
if (!is_file($path)) { http_response_code(404); echo 'Datei nicht gefunden.'; exit; }
|
||||
|
||||
$safe_display = addslashes($row['original_name']);
|
||||
$safe_encoded = rawurlencode($row['original_name']);
|
||||
|
||||
header('Content-Type: application/octet-stream');
|
||||
header('Content-Disposition: attachment; filename="' . $safe_display . '"; filename*=UTF-8\'\'' . $safe_encoded);
|
||||
header('Content-Length: ' . filesize($path));
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Cache-Control: private, no-store');
|
||||
readfile($path);
|
||||
exit;
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/layout.php';
|
||||
require_login();
|
||||
|
||||
$user_id = current_user()['id'];
|
||||
|
||||
$materials = db()->query('SELECT * FROM fortbildung_materials ORDER BY sort_order ASC, id ASC')->fetchAll();
|
||||
|
||||
$stmt = db()->prepare('SELECT * FROM requests_fortbildungsantrag WHERE user_id = ? ORDER BY created_at DESC');
|
||||
$stmt->execute([$user_id]);
|
||||
$requests = $stmt->fetchAll();
|
||||
|
||||
$anliegen_labels = [
|
||||
'wbl_pdl' => 'Anfrage WBL/PDL Weiterbildung',
|
||||
'fortbildung' => 'Fortbildung anfragen',
|
||||
'pflichtschulung' => 'Pflichtschulung',
|
||||
];
|
||||
|
||||
$status_labels = ['pending' => 'Ausstehend', 'accepted' => 'Akzeptiert', 'rejected' => 'Abgelehnt'];
|
||||
|
||||
layout_start('OMSORG – Fortbildungsantrag', 'fortbildungsantrag');
|
||||
?>
|
||||
<h1 class="main-title">🎓 Fortbildungs­antrag</h1>
|
||||
<p class="main-sub">Unterlagen herunterladen und Fortbildungsanträge einreichen.</p>
|
||||
|
||||
<?php if (isset($_GET['success'])): ?>
|
||||
<div class="success" style="margin-top:16px">Antrag erfolgreich eingereicht.</div>
|
||||
<?php elseif (isset($_GET['error'])): ?>
|
||||
<div class="error" style="margin-top:16px"><?= e(urldecode($_GET['error'])) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="page-split" style="margin-top:24px">
|
||||
|
||||
<div class="page-split-list">
|
||||
<h2>Unterlagen</h2>
|
||||
<?php if (empty($materials)): ?>
|
||||
<p style="color:var(--muted)">Noch keine Unterlagen verfügbar.</p>
|
||||
<?php else: ?>
|
||||
<div class="downloads-grid">
|
||||
<?php foreach ($materials as $m): ?>
|
||||
<div class="glass download-card">
|
||||
<h3 class="download-card-title"><?= e($m['title']) ?></h3>
|
||||
<?php if ($m['description'] !== ''): ?>
|
||||
<p class="download-card-desc"><?= e($m['description']) ?></p>
|
||||
<?php endif; ?>
|
||||
<a href="fortbildung-serve.php?id=<?= (int)$m['id'] ?>" class="btn download-btn">↓ Herunterladen</a>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="page-split-form">
|
||||
<div class="glass detail-card">
|
||||
<h2>Neuer Antrag</h2>
|
||||
<form action="../actions/submit-fortbildungsantrag.php" method="post" enctype="multipart/form-data">
|
||||
<?= csrf_field() ?>
|
||||
<label>
|
||||
<span>Anliegen <span style="color:#ff8080">*</span></span>
|
||||
<select name="anliegen" required>
|
||||
<option value="">– Bitte wählen –</option>
|
||||
<option value="wbl_pdl">Anfrage WBL/PDL Weiterbildung</option>
|
||||
<option value="fortbildung">Fortbildung anfragen</option>
|
||||
<option value="pflichtschulung">Pflichtschulung</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Thema <span style="color:#ff8080">*</span></span>
|
||||
<input type="text" name="thema" required placeholder="z. B. Erste Hilfe, Demenzpflege …">
|
||||
</label>
|
||||
<label>
|
||||
Nachricht
|
||||
<textarea name="nachricht" rows="4" placeholder="Weitere Informationen oder Wünsche"></textarea>
|
||||
</label>
|
||||
<label>
|
||||
Anhang
|
||||
<input type="file" name="datei" accept=".pdf,.doc,.docx,.jpg,.jpeg,.png">
|
||||
<span class="hint">PDF, Word, JPG, PNG — max. 12 MB (optional)</span>
|
||||
</label>
|
||||
<button type="submit" class="btn" style="margin-top:6px">Antrag einreichen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php if (!empty($requests)): ?>
|
||||
<div style="margin-top:40px">
|
||||
<h2>Meine Anträge</h2>
|
||||
<table class="requests-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Anliegen</th>
|
||||
<th>Thema</th>
|
||||
<th>Eingereicht</th>
|
||||
<th>Status</th>
|
||||
<th>Notiz</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($requests as $r): ?>
|
||||
<tr>
|
||||
<td><?= e($anliegen_labels[$r['anliegen']] ?? $r['anliegen']) ?></td>
|
||||
<td style="font-weight:800"><?= e($r['thema']) ?></td>
|
||||
<td style="color:var(--muted);font-size:.9rem"><?= e(date('d.m.Y', strtotime($r['created_at']))) ?></td>
|
||||
<td><span class="badge badge-<?= e($r['status']) ?>"><?= e($status_labels[$r['status']] ?? $r['status']) ?></span></td>
|
||||
<td style="color:var(--muted);font-size:.9rem"><?= $r['admin_note'] !== '' ? e($r['admin_note']) : '–' ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php layout_end(); ?>
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/layout.php';
|
||||
require_login();
|
||||
|
||||
$username = current_username();
|
||||
$user = current_user();
|
||||
|
||||
$saved = isset($_GET['saved']);
|
||||
$pwchanged = isset($_GET['pwchanged']);
|
||||
$avatar_saved = isset($_GET['avatar_saved']);
|
||||
$error = isset($_GET['error']) ? urldecode($_GET['error']) : '';
|
||||
|
||||
layout_start('OMSORG – Einstellungen', 'settings');
|
||||
?>
|
||||
<h1 class="main-title">Einstellungen</h1>
|
||||
<p class="main-sub">Deine persönlichen Daten und Passwort.</p>
|
||||
|
||||
<?php if ($saved): ?>
|
||||
<div class="success" style="margin-top:20px">Profil erfolgreich gespeichert.</div>
|
||||
<?php elseif ($pwchanged): ?>
|
||||
<div class="success" style="margin-top:20px">Passwort erfolgreich geändert.</div>
|
||||
<?php elseif ($avatar_saved): ?>
|
||||
<div class="success" style="margin-top:20px">Profilbild erfolgreich gespeichert.</div>
|
||||
<?php elseif ($error): ?>
|
||||
<div class="error" style="margin-top:20px"><?= e($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Profilbild -->
|
||||
<div class="settings-section">
|
||||
<h2 style="font-family:'KindelSerif',Georgia,serif;font-size:1.5rem;margin:0 0 18px">Profilbild</h2>
|
||||
<div style="display:flex;align-items:center;gap:24px;flex-wrap:wrap;margin-bottom:20px">
|
||||
<?php if (!empty($user['avatar'])): ?>
|
||||
<img id="avatar-preview" src="../assets/avatars/<?= e($user['avatar']) ?>"
|
||||
alt="Profilbild" class="settings-avatar-preview">
|
||||
<?php else: ?>
|
||||
<div id="avatar-preview-placeholder" class="settings-avatar-placeholder">
|
||||
<?= e(mb_substr($user['name'], 0, 1)) ?>
|
||||
</div>
|
||||
<img id="avatar-preview" src="" alt="Vorschau" class="settings-avatar-preview"
|
||||
style="display:none">
|
||||
<?php endif; ?>
|
||||
<div>
|
||||
<form action="../actions/upload-avatar.php" method="post" enctype="multipart/form-data"
|
||||
style="display:flex;flex-direction:column;gap:10px;align-items:flex-start">
|
||||
<?= csrf_field() ?>
|
||||
<label class="btn" style="cursor:pointer;margin:0">
|
||||
Bild auswählen
|
||||
<input type="file" name="avatar" accept="image/*" id="avatar-input"
|
||||
style="display:none" onchange="previewAvatar(this)">
|
||||
</label>
|
||||
<button type="submit" class="btn" id="avatar-upload-btn" style="display:none">
|
||||
Hochladen
|
||||
</button>
|
||||
</form>
|
||||
<?php if (!empty($user['avatar'])): ?>
|
||||
<form action="../actions/upload-avatar.php" method="post" style="margin-top:8px">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="remove_avatar" value="1">
|
||||
<button type="submit" class="btn btn-ghost" style="font-size:.85rem;padding:6px 14px">
|
||||
Bild entfernen
|
||||
</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<p style="color:var(--muted);font-size:.82rem;margin:0">JPG, PNG, GIF oder WebP · max. 3 MB</p>
|
||||
</div>
|
||||
|
||||
<!-- Profildaten -->
|
||||
<div class="settings-section">
|
||||
<h2 style="font-family:'KindelSerif',Georgia,serif;font-size:1.5rem;margin:0 0 18px">Profildaten</h2>
|
||||
<form action="../actions/save-profile.php" method="post" style="max-width:480px">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="profile">
|
||||
<label>
|
||||
Name
|
||||
<input type="text" value="<?= e($user['name']) ?>" disabled>
|
||||
</label>
|
||||
<label>
|
||||
Benutzername
|
||||
<input type="text" value="<?= e($username) ?>" disabled>
|
||||
</label>
|
||||
<label>
|
||||
E-Mail
|
||||
<input type="email" name="email" autocomplete="email"
|
||||
value="<?= e($user['email'] ?? '') ?>">
|
||||
</label>
|
||||
<label>
|
||||
Telefon
|
||||
<input type="tel" name="telefon" autocomplete="tel"
|
||||
value="<?= e($user['telefon'] ?? '') ?>">
|
||||
</label>
|
||||
<button type="submit" class="btn">Speichern</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Passwort ändern -->
|
||||
<div class="settings-section">
|
||||
<h2 style="font-family:'KindelSerif',Georgia,serif;font-size:1.5rem;margin:0 0 18px">Passwort ändern</h2>
|
||||
<form action="../actions/save-profile.php" method="post" style="max-width:480px">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="password">
|
||||
<label>
|
||||
Aktuelles Passwort
|
||||
<input type="password" name="old_password" autocomplete="current-password" required>
|
||||
</label>
|
||||
<label>
|
||||
Neues Passwort
|
||||
<input type="password" name="new_password" autocomplete="new-password" required minlength="8">
|
||||
</label>
|
||||
<label>
|
||||
Neues Passwort wiederholen
|
||||
<input type="password" name="new_password_repeat" autocomplete="new-password" required minlength="8">
|
||||
</label>
|
||||
<button type="submit" class="btn">Passwort ändern</button>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
function previewAvatar(input) {
|
||||
if (!input.files || !input.files[0]) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
const img = document.getElementById('avatar-preview');
|
||||
const ph = document.getElementById('avatar-preview-placeholder');
|
||||
img.src = e.target.result;
|
||||
img.style.display = '';
|
||||
if (ph) ph.style.display = 'none';
|
||||
document.getElementById('avatar-upload-btn').style.display = '';
|
||||
};
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
</script>
|
||||
<?php layout_end(); ?>
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/layout.php';
|
||||
require_login();
|
||||
|
||||
$user_id = current_user()['id'];
|
||||
|
||||
$stmt = db()->prepare('SELECT * FROM requests_stundennachweis WHERE user_id = ? ORDER BY created_at DESC');
|
||||
$stmt->execute([$user_id]);
|
||||
$requests = $stmt->fetchAll();
|
||||
|
||||
$status_labels = ['pending' => 'Ausstehend', 'accepted' => 'Akzeptiert', 'rejected' => 'Abgelehnt'];
|
||||
|
||||
layout_start('OMSORG – Stundennachweis', 'stundennachweis');
|
||||
?>
|
||||
<h1 class="main-title">🕐 Stundennachweis</h1>
|
||||
<p class="main-sub">Monatlichen Stundennachweis einreichen.</p>
|
||||
|
||||
<?php if (isset($_GET['success'])): ?>
|
||||
<div class="success" style="margin-top:16px">Nachweis erfolgreich eingereicht.</div>
|
||||
<?php elseif (isset($_GET['error'])): ?>
|
||||
<div class="error" style="margin-top:16px"><?= e(urldecode($_GET['error'])) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="glass detail-card" style="margin-top:24px;max-width:480px">
|
||||
<h2>Nachweis einreichen</h2>
|
||||
<form action="../actions/submit-stundennachweis.php" method="post" enctype="multipart/form-data">
|
||||
<?= csrf_field() ?>
|
||||
<div class="monat-grid">
|
||||
<label>
|
||||
<span>Monat <span style="color:#ff8080">*</span></span>
|
||||
<select name="monat_m" required>
|
||||
<option value="">– Monat –</option>
|
||||
<?php foreach (['01'=>'Januar','02'=>'Februar','03'=>'März','04'=>'April','05'=>'Mai','06'=>'Juni','07'=>'Juli','08'=>'August','09'=>'September','10'=>'Oktober','11'=>'November','12'=>'Dezember'] as $v => $l): ?>
|
||||
<option value="<?= $v ?>"><?= $l ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Jahr <span style="color:#ff8080">*</span></span>
|
||||
<input type="number" name="monat_j" min="2020" max="2099"
|
||||
value="<?= date('Y') ?>" required style="width:100%">
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
<span>Nachweis <span style="color:#ff8080">*</span></span>
|
||||
<input type="file" name="datei" accept=".pdf,.jpg,.jpeg,.png" required>
|
||||
<span class="hint">PDF, JPG, PNG — max. 12 MB</span>
|
||||
</label>
|
||||
<button type="submit" class="btn" style="margin-top:6px">Einreichen</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($requests)): ?>
|
||||
<div style="margin-top:40px">
|
||||
<h2>Meine Nachweise</h2>
|
||||
<table class="requests-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Monat</th>
|
||||
<th>Datei</th>
|
||||
<th>Eingereicht</th>
|
||||
<th>Status</th>
|
||||
<th>Notiz</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($requests as $r): ?>
|
||||
<tr>
|
||||
<td style="font-weight:800"><?= e(date('F Y', strtotime($r['monat'] . '-01'))) ?></td>
|
||||
<td>
|
||||
<a href="upload-serve.php?file=<?= e(rawurlencode($r['filename'])) ?>&name=<?= e(rawurlencode($r['original_name'])) ?>"
|
||||
style="color:var(--cyan2)"><?= e($r['original_name']) ?></a>
|
||||
</td>
|
||||
<td style="color:var(--muted);font-size:.9rem"><?= e(date('d.m.Y', strtotime($r['created_at']))) ?></td>
|
||||
<td><span class="badge badge-<?= e($r['status']) ?>"><?= e($status_labels[$r['status']] ?? $r['status']) ?></span></td>
|
||||
<td style="color:var(--muted);font-size:.9rem"><?= $r['admin_note'] !== '' ? e($r['admin_note']) : '–' ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php layout_end(); ?>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_admin();
|
||||
|
||||
$file = basename($_GET['file'] ?? '');
|
||||
if ($file === '') { http_response_code(400); echo 'Ungültige Anfrage.'; exit; }
|
||||
|
||||
$path = dirname(__DIR__) . '/uploads/' . $file;
|
||||
if (!is_file($path)) { http_response_code(404); echo 'Datei nicht gefunden.'; exit; }
|
||||
|
||||
$display = $_GET['name'] ?? $file;
|
||||
$safe_display = addslashes($display);
|
||||
$safe_encoded = rawurlencode($display);
|
||||
|
||||
header('Content-Type: application/octet-stream');
|
||||
header('Content-Disposition: attachment; filename="' . $safe_display . '"; filename*=UTF-8\'\'' . $safe_encoded);
|
||||
header('Content-Length: ' . filesize($path));
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Cache-Control: private, no-store');
|
||||
readfile($path);
|
||||
exit;
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/db.php';
|
||||
require_once __DIR__ . '/../lib/layout.php';
|
||||
require_login();
|
||||
|
||||
$user_id = current_user()['id'];
|
||||
|
||||
$stmt = db()->prepare('SELECT * FROM requests_urlaubsantrag WHERE user_id = ? ORDER BY created_at DESC');
|
||||
$stmt->execute([$user_id]);
|
||||
$requests = $stmt->fetchAll();
|
||||
|
||||
$status_labels = ['pending' => 'Ausstehend', 'accepted' => 'Akzeptiert', 'rejected' => 'Abgelehnt'];
|
||||
|
||||
layout_start('OMSORG – Urlaubsantrag', 'urlaubsantrag');
|
||||
?>
|
||||
<style>
|
||||
.modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.55);display:none;align-items:center;justify-content:center;z-index:1000}
|
||||
.modal-box{background:var(--glass-bg,#1e2235);border:1px solid var(--border,rgba(255,255,255,.1));border-radius:14px;padding:28px 32px;max-width:480px;width:90%;position:relative}
|
||||
.modal-box h3{margin:0 0 20px;font-family:'KindelSerif',Georgia,serif;font-size:1.3rem}
|
||||
.modal-close{position:absolute;top:12px;right:16px;background:none;border:none;font-size:1.5rem;cursor:pointer;color:var(--muted,#888);line-height:1}
|
||||
.modal-dl{display:grid;grid-template-columns:auto 1fr;gap:8px 16px;font-size:.95rem}
|
||||
.modal-dl dt{color:var(--muted,#888);white-space:nowrap}
|
||||
.modal-dl dd{margin:0;font-weight:500}
|
||||
.btn-sm{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:9px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.08);color:#fff;font-weight:600;font-size:.82rem;cursor:pointer;font:inherit;transition:background .15s}
|
||||
.btn-sm:hover{background:rgba(255,255,255,.15)}
|
||||
</style>
|
||||
|
||||
<h1 class="main-title">🌴 Urlaubsantrag</h1>
|
||||
<p class="main-sub">Beantrage deinen Urlaub. Das Startdatum muss vor dem Enddatum liegen.</p>
|
||||
|
||||
<?php if (isset($_GET['success'])): ?>
|
||||
<div class="success" style="margin-top:16px">Antrag erfolgreich eingereicht.</div>
|
||||
<?php elseif (isset($_GET['error'])): ?>
|
||||
<div class="error" style="margin-top:16px"><?= e(urldecode($_GET['error'])) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Details-Modal -->
|
||||
<div id="urlaub-modal" class="modal-backdrop" onclick="if(event.target===this)this.style.display='none'">
|
||||
<div class="modal-box glass">
|
||||
<button class="modal-close" onclick="document.getElementById('urlaub-modal').style.display='none'">×</button>
|
||||
<h3>Antrag Details</h3>
|
||||
<dl class="modal-dl">
|
||||
<dt>Von</dt> <dd id="md-von">–</dd>
|
||||
<dt>Bis</dt> <dd id="md-bis">–</dd>
|
||||
<dt>Vertretung</dt><dd id="md-vertretung">–</dd>
|
||||
<dt>Nachricht</dt> <dd id="md-nachricht">–</dd>
|
||||
<dt>Eingereicht</dt><dd id="md-eingereicht">–</dd>
|
||||
<dt>Status</dt> <dd id="md-status">–</dd>
|
||||
<dt>Admin-Notiz</dt><dd id="md-note">–</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-split">
|
||||
|
||||
<div class="page-split-list">
|
||||
<h2>Meine Anträge</h2>
|
||||
<?php if (empty($requests)): ?>
|
||||
<p style="color:var(--muted)">Noch keine Anträge gestellt.</p>
|
||||
<?php else: ?>
|
||||
<table class="requests-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Von</th>
|
||||
<th>Bis</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($requests as $r): ?>
|
||||
<tr>
|
||||
<td><?= e(date('d.m.Y', strtotime($r['von']))) ?></td>
|
||||
<td><?= e(date('d.m.Y', strtotime($r['bis']))) ?></td>
|
||||
<td><span class="badge badge-<?= e($r['status']) ?>"><?= e($status_labels[$r['status']] ?? $r['status']) ?></span></td>
|
||||
<td>
|
||||
<button class="btn-sm js-urlaub-details"
|
||||
data-von="<?= e(date('d.m.Y', strtotime($r['von']))) ?>"
|
||||
data-bis="<?= e(date('d.m.Y', strtotime($r['bis']))) ?>"
|
||||
data-vertretung="<?= e($r['vertretung'] !== '' ? $r['vertretung'] : '–') ?>"
|
||||
data-nachricht="<?= e($r['nachricht'] !== '' ? $r['nachricht'] : '–') ?>"
|
||||
data-eingereicht="<?= e(date('d.m.Y', strtotime($r['created_at']))) ?>"
|
||||
data-status-label="<?= e($status_labels[$r['status']] ?? $r['status']) ?>"
|
||||
data-status-key="<?= e($r['status']) ?>"
|
||||
data-note="<?= e($r['admin_note'] !== '' ? $r['admin_note'] : '–') ?>"
|
||||
>Details</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="page-split-form">
|
||||
<div class="glass detail-card">
|
||||
<h2>Neuer Urlaubsantrag</h2>
|
||||
<form action="../actions/submit-urlaubsantrag.php" method="post" enctype="multipart/form-data">
|
||||
<?= csrf_field() ?>
|
||||
<label>
|
||||
<span>Von <span style="color:#ff8080">*</span></span>
|
||||
<input type="date" name="von" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Bis <span style="color:#ff8080">*</span></span>
|
||||
<input type="date" name="bis" required>
|
||||
</label>
|
||||
<label>
|
||||
Vertretung
|
||||
<input type="text" name="vertretung" placeholder="Name der Vertretung">
|
||||
</label>
|
||||
<label>
|
||||
Nachricht
|
||||
<textarea name="nachricht" rows="4" placeholder="Optionale Anmerkungen"></textarea>
|
||||
</label>
|
||||
<label>
|
||||
Anhang
|
||||
<input type="file" name="datei" accept=".pdf,.doc,.docx,.jpg,.jpeg,.png">
|
||||
<span class="hint">PDF, Word, JPG, PNG — max. 12 MB (optional)</span>
|
||||
</label>
|
||||
<button type="submit" class="btn" style="margin-top:6px">Antrag einreichen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('click', function(e) {
|
||||
var btn = e.target.closest('.js-urlaub-details');
|
||||
if (!btn) return;
|
||||
var d = btn.dataset;
|
||||
document.getElementById('md-von').textContent = d.von;
|
||||
document.getElementById('md-bis').textContent = d.bis;
|
||||
document.getElementById('md-vertretung').textContent = d.vertretung;
|
||||
document.getElementById('md-nachricht').textContent = d.nachricht;
|
||||
document.getElementById('md-eingereicht').textContent = d.eingereicht;
|
||||
document.getElementById('md-status').innerHTML =
|
||||
'<span class="badge badge-' + d.statusKey + '">' + d.statusLabel + '</span>';
|
||||
document.getElementById('md-note').textContent = d.note;
|
||||
document.getElementById('urlaub-modal').style.display = 'flex';
|
||||
});
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') document.getElementById('urlaub-modal').style.display = 'none';
|
||||
});
|
||||
</script>
|
||||
<?php layout_end(); ?>
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../lib/auth.php';
|
||||
require_once __DIR__ . '/../lib/db.php';
|
||||
require_once __DIR__ . '/../lib/layout.php';
|
||||
require_login();
|
||||
|
||||
$user_id = current_user()['id'];
|
||||
|
||||
$stmt = db()->prepare('SELECT * FROM requests_werben WHERE user_id = ? ORDER BY created_at DESC');
|
||||
$stmt->execute([$user_id]);
|
||||
$requests = $stmt->fetchAll();
|
||||
|
||||
$qual_labels = [
|
||||
'1jahr' => '1 Jährige Examinierte Pflegekraft',
|
||||
'3jahr' => '3 Jährige Examinierte Pflegekraft',
|
||||
];
|
||||
|
||||
$status_labels = ['pending' => 'Ausstehend', 'accepted' => 'Akzeptiert', 'rejected' => 'Abgelehnt'];
|
||||
|
||||
layout_start('OMSORG – Mitarbeiter werben', 'werben');
|
||||
?>
|
||||
<style>
|
||||
.modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.55);display:none;align-items:center;justify-content:center;z-index:1000}
|
||||
.modal-box{background:var(--glass-bg,#1e2235);border:1px solid var(--border,rgba(255,255,255,.1));border-radius:14px;padding:28px 32px;max-width:480px;width:90%;position:relative}
|
||||
.modal-box h3{margin:0 0 20px;font-family:'KindelSerif',Georgia,serif;font-size:1.3rem}
|
||||
.modal-close{position:absolute;top:12px;right:16px;background:none;border:none;font-size:1.5rem;cursor:pointer;color:var(--muted,#888);line-height:1}
|
||||
.modal-dl{display:grid;grid-template-columns:auto 1fr;gap:8px 16px;font-size:.95rem}
|
||||
.modal-dl dt{color:var(--muted,#888);white-space:nowrap}
|
||||
.modal-dl dd{margin:0;font-weight:500}
|
||||
.btn-sm{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:9px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.08);color:#fff;font-weight:600;font-size:.82rem;cursor:pointer;font:inherit;transition:background .15s}
|
||||
.btn-sm:hover{background:rgba(255,255,255,.15)}
|
||||
</style>
|
||||
|
||||
<h1 class="main-title">🤝 Mitarbeiter werben</h1>
|
||||
<p class="main-sub">Empfehle einen Kandidaten für eine offene Stelle bei OMSORG.</p>
|
||||
|
||||
<?php if (isset($_GET['success'])): ?>
|
||||
<div class="success" style="margin-top:16px">Empfehlung erfolgreich eingereicht.</div>
|
||||
<?php elseif (isset($_GET['error'])): ?>
|
||||
<div class="error" style="margin-top:16px"><?= e(urldecode($_GET['error'])) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div id="werb-modal" class="modal-backdrop" onclick="if(event.target===this)this.style.display='none'">
|
||||
<div class="modal-box glass">
|
||||
<button class="modal-close" onclick="document.getElementById('werb-modal').style.display='none'">×</button>
|
||||
<h3>Empfehlung Details</h3>
|
||||
<dl class="modal-dl">
|
||||
<dt>Name</dt> <dd id="werb-name">–</dd>
|
||||
<dt>E-Mail</dt> <dd id="werb-email">–</dd>
|
||||
<dt>Qualifikation</dt> <dd id="werb-qual">–</dd>
|
||||
<dt>Nachricht</dt> <dd id="werb-nachricht">–</dd>
|
||||
<dt>Eingereicht</dt> <dd id="werb-eingereicht">–</dd>
|
||||
<dt>Status</dt> <dd id="werb-status">–</dd>
|
||||
<dt>Admin-Notiz</dt> <dd id="werb-note">–</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-split">
|
||||
|
||||
<div class="page-split-list">
|
||||
<h2>Meine Empfehlungen</h2>
|
||||
<?php if (empty($requests)): ?>
|
||||
<p style="color:var(--muted)">Noch keine Empfehlungen eingereicht.</p>
|
||||
<?php else: ?>
|
||||
<table class="requests-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($requests as $r): ?>
|
||||
<tr>
|
||||
<td style="font-weight:800"><?= e($r['name']) ?></td>
|
||||
<td><span class="badge badge-<?= e($r['status']) ?>"><?= e($status_labels[$r['status']] ?? $r['status']) ?></span></td>
|
||||
<td>
|
||||
<button class="btn-sm js-werb-details"
|
||||
data-name="<?= e($r['name']) ?>"
|
||||
data-email="<?= e($r['email']) ?>"
|
||||
data-qual="<?= e($qual_labels[$r['qualifikation']] ?? $r['qualifikation']) ?>"
|
||||
data-nachricht="<?= e($r['nachricht'] !== '' ? $r['nachricht'] : '–') ?>"
|
||||
data-eingereicht="<?= e(date('d.m.Y', strtotime($r['created_at']))) ?>"
|
||||
data-status-label="<?= e($status_labels[$r['status']] ?? $r['status']) ?>"
|
||||
data-status-key="<?= e($r['status']) ?>"
|
||||
data-note="<?= e($r['admin_note'] !== '' ? $r['admin_note'] : '–') ?>"
|
||||
>Details</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="page-split-form">
|
||||
<div class="glass detail-card">
|
||||
<h2>Kandidaten empfehlen</h2>
|
||||
<form action="../actions/submit-werben.php" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<label>
|
||||
<span>Name des Kandidaten <span style="color:#ff8080">*</span></span>
|
||||
<input type="text" name="name" required placeholder="Vorname Nachname">
|
||||
</label>
|
||||
<label>
|
||||
<span>E-Mail des Kandidaten <span style="color:#ff8080">*</span></span>
|
||||
<input type="email" name="email" required placeholder="kandidat@example.com">
|
||||
</label>
|
||||
<label>
|
||||
<span>Qualifikation <span style="color:#ff8080">*</span></span>
|
||||
<select name="qualifikation" required>
|
||||
<option value="">– Bitte wählen –</option>
|
||||
<option value="1jahr">1 Jährige Examinierte Pflegekraft</option>
|
||||
<option value="3jahr">3 Jährige Examinierte Pflegekraft</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Nachricht
|
||||
<textarea name="nachricht" rows="4" placeholder="Warum empfiehlst du diese Person?"></textarea>
|
||||
</label>
|
||||
<button type="submit" class="btn" style="margin-top:6px">Empfehlung einreichen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('click', function(e) {
|
||||
var btn = e.target.closest('.js-werb-details');
|
||||
if (!btn) return;
|
||||
var d = btn.dataset;
|
||||
document.getElementById('werb-name').textContent = d.name;
|
||||
document.getElementById('werb-email').textContent = d.email;
|
||||
document.getElementById('werb-qual').textContent = d.qual;
|
||||
document.getElementById('werb-nachricht').textContent = d.nachricht;
|
||||
document.getElementById('werb-eingereicht').textContent= d.eingereicht;
|
||||
document.getElementById('werb-status').innerHTML =
|
||||
'<span class="badge badge-' + d.statusKey + '">' + d.statusLabel + '</span>';
|
||||
document.getElementById('werb-note').textContent = d.note;
|
||||
document.getElementById('werb-modal').style.display = 'flex';
|
||||
});
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') document.getElementById('werb-modal').style.display = 'none';
|
||||
});
|
||||
</script>
|
||||
<?php layout_end(); ?>
|
||||
@@ -0,0 +1,13 @@
|
||||
const CACHE = 'omsorg-v1';
|
||||
const STATIC = ['./app.css'];
|
||||
|
||||
self.addEventListener('install', e =>
|
||||
e.waitUntil(caches.open(CACHE).then(c => c.addAll(STATIC)))
|
||||
);
|
||||
|
||||
self.addEventListener('fetch', e => {
|
||||
if (e.request.url.endsWith('.php') || e.request.method !== 'GET') return;
|
||||
e.respondWith(
|
||||
fetch(e.request).catch(() => caches.match(e.request))
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
ini_set('display_errors', '1');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
echo "<pre>\n";
|
||||
|
||||
try {
|
||||
echo "1. Lade config...\n";
|
||||
$c = require __DIR__ . '/lib/config.php';
|
||||
echo " driver={$c['db_driver']} dsn={$c['db_dsn']}\n";
|
||||
|
||||
echo "2. Verbinde Datenbank...\n";
|
||||
require_once __DIR__ . '/lib/db.php';
|
||||
$pdo = db();
|
||||
echo " Verbindung OK\n";
|
||||
|
||||
echo "3. Zaehle User...\n";
|
||||
$count = (int) $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();
|
||||
echo " $count User gefunden\n";
|
||||
|
||||
if ($count === 0) {
|
||||
echo "4. Erstelle Admin-User...\n";
|
||||
$hash = password_hash('adminadmin', PASSWORD_BCRYPT, ['cost' => 12]);
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO users (username, name, role, password_hash, active, created_at)
|
||||
VALUES (?, ?, ?, ?, 1, ?)'
|
||||
);
|
||||
$stmt->execute(['admin', 'Administrator', 'admin', $hash, date('c')]);
|
||||
echo " Admin erstellt: username=admin password=adminadmin\n";
|
||||
} else {
|
||||
echo "4. DB hat bereits User — kein Admin eingefuegt.\n";
|
||||
}
|
||||
|
||||
echo "\nSetup erfolgreich abgeschlossen.\n";
|
||||
} catch (Throwable $e) {
|
||||
echo "\nFEHLER: " . $e->getMessage() . "\n";
|
||||
echo "Datei: " . $e->getFile() . " Zeile " . $e->getLine() . "\n";
|
||||
echo "\nStack trace:\n" . $e->getTraceAsString() . "\n";
|
||||
}
|
||||
|
||||
echo "</pre>\n";
|
||||
Reference in New Issue
Block a user