web - refactor core modules, harden authorization, simplify endpoints
This commit is contained in:
@@ -1,782 +1,217 @@
|
||||
<?php /* Name: kalli.php
|
||||
Beschreibung: Kalli: Hauptseite */
|
||||
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/initialise.php';
|
||||
<?php
|
||||
/* Name: kalli.php
|
||||
Beschreibung: Kalli: Eintragungsassistent */
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/general_files/initialise.php';
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
require_once __DIR__ . '/util/kalli_helpers.php';
|
||||
|
||||
$csrfToken = kalliCsrfToken();
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !kalliCsrfIsValid($_POST['csrf_token'] ?? null)) {
|
||||
http_response_code(400);
|
||||
exit('Ungültige Anfrage. Bitte laden Sie die Seite neu.');
|
||||
}
|
||||
|
||||
$permissionGroups = is_array($groups ?? null) ? $groups : [];
|
||||
$catalog = kalliBuildCatalog(kalliFetchCatalogRows($pdo), $permissionGroups);
|
||||
$formValues = kalliFormValues($_SERVER['REQUEST_METHOD'] === 'POST' ? $_POST : []);
|
||||
$formError = null;
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
try {
|
||||
$entry = kalliValidateEntry($_POST, $catalog);
|
||||
$xlsxDirectory = intranet_env('CKAL_XLSX_DIR', '/data/ckal-xlsx');
|
||||
kalliWriteCalendarFile($entry, is_array($user) ? $user : [], $xlsxDirectory);
|
||||
|
||||
header('Location: /intranet_repo/projects/intranet/index.php?kalli_success=1', true, 303);
|
||||
exit();
|
||||
} catch (InvalidArgumentException $exception) {
|
||||
$formError = $exception->getMessage();
|
||||
} catch (Throwable $exception) {
|
||||
error_log('Kalli: XLSX-Erzeugung fehlgeschlagen: ' . $exception->getMessage());
|
||||
$formError = 'Die Eintragung konnte nicht gespeichert werden. Bitte versuchen Sie es erneut oder kontaktieren Sie uns.';
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de" dir="ltr">
|
||||
<head>
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/html_head.php'; ?>
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/projects/_central/php/html_head.php'; ?>
|
||||
<link rel="stylesheet" href="/intranet_repo/projects/_central/layouts/kalli.css">
|
||||
<script src="/intranet_repo/projects/_central/scripts/kalli.js" defer></script>
|
||||
</head>
|
||||
<body class="theme-blue">
|
||||
|
||||
<?php // Stelle sicher, dass die Autoload-Datei von PHPSpreadsheet geladen wird
|
||||
require 'vendor/autoload.php'; ?>
|
||||
|
||||
<?php // Durchführung der Aktionen
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
|
||||
|
||||
if(isset($_POST['submit'])) {
|
||||
// if no errors, carry on
|
||||
if(!isset($error)) {
|
||||
$name = htmlspecialchars($user["first_name"]) . " " . htmlspecialchars($user["last_name"]);
|
||||
$info = "Von " . $name . " eingetragen (Eintragungsassistent)";
|
||||
// Create a new PHPExcel object
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
// Set the header row
|
||||
$sheet->setCellValue('A1', 'ID');
|
||||
$sheet->setCellValue('B1', 'Lang-ID');
|
||||
$sheet->setCellValue('C1', 'Termin-Typ');
|
||||
$sheet->setCellValue('D1', 'Von-Datum');
|
||||
$sheet->setCellValue('E1', 'Von-Zeit');
|
||||
$sheet->setCellValue('F1', 'Bis-Datum');
|
||||
$sheet->setCellValue('G1', 'Bis-Zeit');
|
||||
$sheet->setCellValue('H1', 'Zeitzusatz');
|
||||
$sheet->setCellValue('I1', 'Titel');
|
||||
$sheet->setCellValue('J1', 'Text');
|
||||
$sheet->setCellValue('K1', 'Ort');
|
||||
$sheet->setCellValue('L1', 'Planungsstatus');
|
||||
$sheet->setCellValue('M1', 'Anzeigeziel');
|
||||
$sheet->setCellValue('N1', 'Gruppenzuordnung');
|
||||
$sheet->setCellValue('O1', 'Zyklusart');
|
||||
$sheet->setCellValue('P1', 'Zyklusintervall');
|
||||
$sheet->setCellValue('Q1', 'Zyklusdetail');
|
||||
$sheet->setCellValue('R1', 'Notiz');
|
||||
$sheet->setCellValue('S1', 'Verantwortlich');
|
||||
$sheet->setCellValue('T1', 'Hintergrundfarbe');
|
||||
$sheet->setCellValue('U1', 'BucherID');
|
||||
$sheet->setCellValue('V1', 'Benutzerdefinierte Felder');
|
||||
$sheet->setCellValue('W1', 'Dokument-Titel');
|
||||
$sheet->setCellValue('X1', 'Anzeigeziel Ort');
|
||||
$sheet->setCellValue('Y1', 'Anzeigeziel Notiz');
|
||||
$sheet->setCellValue('Z1', 'Anzeigeziel Verantwortlicher');
|
||||
$sheet->setCellValue('AA1', 'Erzeugt');
|
||||
$sheet->setCellValue('AB1', 'Aktualisiert');
|
||||
|
||||
// Set the data row
|
||||
// Titel
|
||||
$selectedGroup = isset($_POST['group']) ? $_POST['group'] : '';
|
||||
$sheet->setCellValue('I2', $selectedGroup);
|
||||
// Datum
|
||||
$date_s = isset($_POST['date_s']) ? $_POST['date_s'] : '';
|
||||
|
||||
$sheet->setCellValue('D2', date('d.m.Y', strtotime($date_s)));
|
||||
|
||||
$date_e = isset($_POST['date_e']) ? $_POST['date_e'] : '';
|
||||
if (date('d.m.Y', strtotime($date_e)) != "01.01.1970"){
|
||||
$sheet->setCellValue('F2', date('d.m.Y', strtotime($date_e)));
|
||||
}
|
||||
// Zeit
|
||||
$time_s = isset($_POST['time_s']) ? $_POST['time_s'] : '';
|
||||
if ($time_s !== '' && preg_match('/^\d{2}:\d{2}$/', $time_s)) {
|
||||
$sheet->setCellValue('E2', $time_s);
|
||||
} elseif ($time_s !== '') {
|
||||
$error[] = "Ungültiges Zeitformat. Verwenden Sie das Format hh:mm.";
|
||||
}
|
||||
$time_e = isset($_POST['time_e']) ? $_POST['time_e'] : '';
|
||||
if ($time_e !== '' && preg_match('/^\d{2}:\d{2}$/', $time_e)) {
|
||||
$sheet->setCellValue('G2', $time_e);
|
||||
} elseif ($time_e !== '') {
|
||||
$error[] = "Ungültiges Zeitformat. Verwenden Sie das Format hh:mm.";
|
||||
}
|
||||
// FIX
|
||||
$sheet->setCellValue('C2', 'Einzel');
|
||||
$sheet->setCellValue('O2', 'einmalig');
|
||||
|
||||
// Setze den Ort in Zelle K2
|
||||
$location = isset($_POST['location']) ? htmlspecialchars($_POST['location']) : '';
|
||||
$sheet->setCellValue('K2', $location);
|
||||
|
||||
// Setze den Text in Zelle J2
|
||||
$further_text = isset($_POST['further_text']) ? htmlspecialchars($_POST['further_text']) : '';
|
||||
$sheet->setCellValue('J2', $further_text);
|
||||
$sheet->setCellValue('R2', $info);
|
||||
$responsibility = isset($_POST['responsibility']) ? htmlspecialchars($_POST['responsibility']) : '';
|
||||
$sheet->setCellValue('S2', $responsibility);
|
||||
|
||||
$userInput = isset($_POST['userinput']) ? $_POST['userinput'] : '';
|
||||
$userInput2 = isset($_POST['userinput2']) ? $_POST['userinput2'] : '';
|
||||
|
||||
// DB-Lookup: Preset-Werte für die gewählte Terminart laden
|
||||
$presetStmt = $pdo->prepare("SELECT planungsstatus, anzeigeziel, gruppenzuordnung, anzeigeziel_ort, anzeigeziel_notiz, anzeigeziel_verantwortlicher, default_text FROM kalli_appointment_types WHERE name = ? LIMIT 1");
|
||||
$presetStmt->execute([$selectedGroup]);
|
||||
$preset = $presetStmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($preset) {
|
||||
$sheet->setCellValue('L2', $preset['planungsstatus']);
|
||||
$sheet->setCellValue('M2', $preset['anzeigeziel']);
|
||||
$sheet->setCellValue('N2', $preset['gruppenzuordnung']);
|
||||
$sheet->setCellValue('X2', $preset['anzeigeziel_ort']);
|
||||
$sheet->setCellValue('Y2', $preset['anzeigeziel_notiz']);
|
||||
$sheet->setCellValue('Z2', $preset['anzeigeziel_verantwortlicher']);
|
||||
if (!empty($preset['default_text']) && empty($further_text)) {
|
||||
$sheet->setCellValue('J2', $preset['default_text']);
|
||||
}
|
||||
} elseif ($userInput != "" && $selectedGroup == "oeffentlich") {
|
||||
$sheet->setCellValue('I2', $userInput);
|
||||
$sheet->setCellValue('L2', 'fix');
|
||||
$sheet->setCellValue('M2', 'öffentlich');
|
||||
$sheet->setCellValue('N2', 'AN ALLE');
|
||||
$sheet->setCellValue('X2', 'öffentlich');
|
||||
$sheet->setCellValue('Y2', 'intern');
|
||||
$sheet->setCellValue('Z2', 'intern');
|
||||
} elseif ($userInput != "" && $selectedGroup == "intern") {
|
||||
$sheet->setCellValue('I2', $userInput);
|
||||
$sheet->setCellValue('L2', 'fix');
|
||||
$sheet->setCellValue('M2', 'intern');
|
||||
$sheet->setCellValue('N2', 'AN ALLE');
|
||||
$sheet->setCellValue('X2', 'intern');
|
||||
$sheet->setCellValue('Y2', 'intern');
|
||||
$sheet->setCellValue('Z2', 'intern');
|
||||
}
|
||||
|
||||
// Save the file on the server
|
||||
$xlsxDir = rtrim(intranet_env('CKAL_XLSX_DIR', '/data/ckal-xlsx'), '/');
|
||||
if (!is_dir($xlsxDir) && !mkdir($xlsxDir, 0770, true) && !is_dir($xlsxDir)) {
|
||||
throw new RuntimeException("CKalender-XLSX-Verzeichnis kann nicht angelegt werden: {$xlsxDir}");
|
||||
}
|
||||
$FileName = $xlsxDir . "/" . $name . "-" . date("Ymd-His") . ".xlsx";
|
||||
$writer = new Xlsx($spreadsheet);
|
||||
$writer->save($FileName);
|
||||
|
||||
// Redirect to prevent form resubmission on page reload
|
||||
print "<script language='javascript'>window.location.href='/intranet_repo/projects/intranet/index.php?kalli_success=1';</script>";
|
||||
//header("Location: kalli.php");
|
||||
//exit();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ACHTUNG: Die PHP-Header Weiterleitungen müssen hier abgeschlossen sein.
|
||||
?>
|
||||
|
||||
|
||||
<?php
|
||||
// Es folgen ab hier die HTML-Ausgaben beginnend mit dem HTML-Header:
|
||||
?>
|
||||
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/header.php'; ?>
|
||||
|
||||
<?php include $g_phpSnippetsPath.'overlay_message.php'; ?>
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/projects/_central/php/header.php'; ?>
|
||||
<?php include $g_phpSnippetsPath . 'overlay_message.php'; ?>
|
||||
|
||||
<div class="header_title ht_kalli text_white" id="header_title">
|
||||
<span class="header_title_text">Kalli: Eintragungsassistent</span>
|
||||
<span class="header_title_text">Kalli: Eintragungsassistent</span>
|
||||
</div>
|
||||
|
||||
<div class="content" id="base">
|
||||
<div class="kalli-wizard">
|
||||
<div class="kalli-progress" aria-label="Fortschritt">
|
||||
<?php foreach (['Terminart', 'Zeitraum', 'Informationen', 'Übersicht'] as $index => $label): ?>
|
||||
<?php if ($index > 0): ?><div class="kalli-progress-line"></div><?php endif; ?>
|
||||
<div class="kalli-progress-step<?= $index === 0 ? ' active' : '' ?>" data-step="<?= $index + 1 ?>">
|
||||
<div class="kalli-progress-circle"><?= $index + 1 ?></div>
|
||||
<span class="kalli-progress-label"><?= kalliHtml($label) ?></span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php if (isset($_SESSION["user_id"])): ?>
|
||||
<?php if ($formError !== null): ?>
|
||||
<div class="kalli-validation-error" role="alert"><?= kalliHtml($formError) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
if (isset($_GET["error"])) {
|
||||
$error = $_GET["error"];
|
||||
$isSuccess = in_array($error, [3]);
|
||||
$toastType = $isSuccess ? 'success' : 'error';
|
||||
$toastIcon = $isSuccess
|
||||
? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><path d="M20 6L9 17l-5-5"/></svg>'
|
||||
: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>';
|
||||
$toastText = ($error == 3) ? 'Eintragung erfolgreich! Kalli wird Ihren Termin direkt vermerken.' : 'Es gab einen Fehler. Bitte kontaktieren Sie uns!';
|
||||
echo "<div class='nak-toast nak-toast--{$toastType}' id='nakToast'>";
|
||||
echo "<div class='nak-toast__icon'>{$toastIcon}</div>";
|
||||
echo "<span class='nak-toast__text'>" . htmlspecialchars($toastText) . "</span>";
|
||||
echo "<button class='nak-toast__close' onclick='dismissNakToast()' aria-label='Schließen'><svg viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2.5' stroke-linecap='square' stroke-linejoin='miter'><line x1='18' y1='6' x2='6' y2='18'/><line x1='6' y1='6' x2='18' y2='18'/></svg></button>";
|
||||
echo "</div>";
|
||||
echo "<script>
|
||||
requestAnimationFrame(function(){document.getElementById('nakToast').classList.add('nak-toast--visible');});
|
||||
setTimeout(function(){dismissNakToast();},4000);
|
||||
function dismissNakToast(){var t=document.getElementById('nakToast');if(!t||t.classList.contains('nak-toast--hiding'))return;t.classList.add('nak-toast--hiding');t.classList.remove('nak-toast--visible');setTimeout(function(){t.remove();},350);}
|
||||
</script>";
|
||||
}
|
||||
?>
|
||||
<form id="kalliForm" action="/intranet_repo/projects/kalli/kalli.php" method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?= kalliHtml($csrfToken) ?>">
|
||||
|
||||
<style>
|
||||
/* ── Kalli Page Wizard ── */
|
||||
.kalli-wizard {
|
||||
max-width: 1400px;
|
||||
width: 90%;
|
||||
margin: var(--space-xl) auto;
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
<section class="kalli-step" id="kalliStep1">
|
||||
<div class="kalli-modal-body">
|
||||
<div class="kalli-section-title">Terminart auswählen</div>
|
||||
<div class="kalli-category-grid">
|
||||
<?php foreach ($catalog['categories'] as $category): ?>
|
||||
<div class="kalli-category">
|
||||
<div class="kalli-category-label"><?= kalliHtml($category['name']) ?></div>
|
||||
<?php foreach ($category['types'] as $type): ?>
|
||||
<?php $displayLabel = $type['display_label'] !== '' ? $type['display_label'] : $type['name']; ?>
|
||||
<label class="kalli-radio-item">
|
||||
<input type="radio" name="group" value="<?= kalliHtml($type['name']) ?>"<?= $formValues['group'] === $type['name'] ? ' checked' : '' ?>>
|
||||
<span><?= kalliHtml($displayLabel) ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
/* ── Progress Bar ── */
|
||||
.kalli-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
padding: 20px 28px 16px;
|
||||
background: var(--gray-50);
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
}
|
||||
.kalli-progress-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.kalli-progress-circle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
color: var(--gray-500);
|
||||
background: var(--gray-200);
|
||||
border: 2px solid var(--gray-300);
|
||||
transition: all 250ms ease;
|
||||
}
|
||||
.kalli-progress-step.active .kalli-progress-circle {
|
||||
background: var(--nak-blue);
|
||||
border-color: var(--nak-blue);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 2px 8px rgba(39,134,207,0.35);
|
||||
}
|
||||
.kalli-progress-step.done .kalli-progress-circle {
|
||||
background: #ffffff;
|
||||
border-color: var(--nak-blue);
|
||||
color: var(--nak-blue);
|
||||
}
|
||||
.kalli-progress-label {
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
color: var(--gray-500);
|
||||
white-space: nowrap;
|
||||
transition: color 250ms ease;
|
||||
}
|
||||
.kalli-progress-step.active .kalli-progress-label,
|
||||
.kalli-progress-step.done .kalli-progress-label {
|
||||
color: var(--nak-blue);
|
||||
}
|
||||
.kalli-progress-line {
|
||||
flex: 1;
|
||||
height: 2px;
|
||||
min-width: 20px;
|
||||
max-width: 60px;
|
||||
background: var(--gray-300);
|
||||
margin: 0 6px;
|
||||
margin-bottom: 20px;
|
||||
transition: background 250ms ease;
|
||||
}
|
||||
.kalli-progress-line.done {
|
||||
background: var(--nak-blue);
|
||||
}
|
||||
<?php if ($catalog['allow_custom']): ?>
|
||||
<div class="kalli-custom-section">
|
||||
<div class="kalli-category-label kalli-category-label--custom">Terminart nicht aufgelistet?</div>
|
||||
<p class="kalli-custom-hint">Nur ausfüllen, wenn die Terminart oben nicht vorhanden ist.</p>
|
||||
<div class="kalli-custom-options">
|
||||
<label class="kalli-radio-item">
|
||||
<input type="radio" name="group" value="<?= KALLI_CUSTOM_PUBLIC ?>" data-kalli-custom="1"<?= $formValues['group'] === KALLI_CUSTOM_PUBLIC ? ' checked' : '' ?>>
|
||||
<span>Öffentlich</span>
|
||||
</label>
|
||||
<label class="kalli-radio-item">
|
||||
<input type="radio" name="group" value="<?= KALLI_CUSTOM_INTERNAL ?>" data-kalli-custom="1"<?= $formValues['group'] === KALLI_CUSTOM_INTERNAL ? ' checked' : '' ?>>
|
||||
<span>Intern</span>
|
||||
</label>
|
||||
</div>
|
||||
<input class="kalli-field" type="text" name="userinput" id="kalliUserInput" value="<?= kalliHtml($formValues['userinput']) ?>" placeholder="Titel (nur für benutzerdefinierte Termine)">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
/* ── Body ── */
|
||||
.kalli-modal-body {
|
||||
padding: 22px 28px 16px;
|
||||
}
|
||||
.kalli-section-title {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
<div class="kalli-modal-actions">
|
||||
<div></div>
|
||||
<div class="kalli-modal-actions-right">
|
||||
<button type="button" class="kalli-btn kalli-btn-primary" data-kalli-next="2">Weiter</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
/* ── Category Grid (Step 1) ── */
|
||||
.kalli-category-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
.kalli-category {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.kalli-category-label {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
margin-bottom: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
label.kalli-radio-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap:8px;
|
||||
padding: 0px 5px;
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 300;
|
||||
color: var(--gray-600);
|
||||
border: 1px solid transparent;
|
||||
transition: background 150ms ease, border-color 150ms ease;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
label.kalli-radio-item input[type="radio"],
|
||||
label.kalli-radio-item input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--nak-blue);
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
align-self: center;
|
||||
}
|
||||
label.kalli-radio-item span {
|
||||
hyphens: auto;
|
||||
-webkit-hyphens: auto;
|
||||
-ms-hyphens: auto;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
line-height: 1.4;
|
||||
}
|
||||
label.kalli-radio-item:hover {
|
||||
background: var(--gray-50);
|
||||
border-color: var(--gray-200);
|
||||
}
|
||||
.kalli-custom-section {
|
||||
margin-top: var(--space-lg);
|
||||
padding: var(--space-md);
|
||||
background: hsl(0,60%,98%);
|
||||
border: 1px solid hsl(0,60%,90%);
|
||||
}
|
||||
<section class="kalli-step" id="kalliStep2" hidden>
|
||||
<div class="kalli-modal-body">
|
||||
<div class="kalli-section-title">Zeitraum festlegen</div>
|
||||
<div class="kalli-field-grid">
|
||||
<div class="kalli-field-group">
|
||||
<label class="kalli-label" for="kalliDateS">Datum <span class="kalli-required">*</span></label>
|
||||
<input class="kalli-field" type="date" name="date_s" id="kalliDateS" value="<?= kalliHtml($formValues['date_s']) ?>" aria-required="true">
|
||||
</div>
|
||||
<div class="kalli-field-group">
|
||||
<label class="kalli-label" for="kalliDateE">Enddatum <span class="kalli-label-note">(falls mehrtägig)</span></label>
|
||||
<input class="kalli-field" type="date" name="date_e" id="kalliDateE" value="<?= kalliHtml($formValues['date_e']) ?>">
|
||||
</div>
|
||||
<div class="kalli-field-group">
|
||||
<label class="kalli-label" for="kalliTimeS">Zeit von <span class="kalli-label-note">(optional)</span></label>
|
||||
<input class="kalli-field" type="time" name="time_s" id="kalliTimeS" value="<?= kalliHtml($formValues['time_s']) ?>">
|
||||
</div>
|
||||
<div class="kalli-field-group">
|
||||
<label class="kalli-label" for="kalliTimeE">Zeit bis <span class="kalli-label-note">(optional)</span></label>
|
||||
<input class="kalli-field" type="time" name="time_e" id="kalliTimeE" value="<?= kalliHtml($formValues['time_e']) ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kalli-modal-actions">
|
||||
<div class="kalli-modal-actions-left">
|
||||
<button type="button" class="kalli-btn" data-kalli-prev="1">Zurück</button>
|
||||
</div>
|
||||
<div class="kalli-modal-actions-right">
|
||||
<button type="button" class="kalli-btn kalli-btn-primary" data-kalli-next="3">Weiter</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
/* ── Field Grid (Steps 2 & 3) ── */
|
||||
.kalli-field-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
.kalli-field-grid-single {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.kalli-field-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.kalli-label {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
.kalli-field {
|
||||
padding: 12px 14px;
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-primary);
|
||||
font-weight: 300;
|
||||
border: 1px solid var(--gray-300);
|
||||
background: var(--gray-50);
|
||||
outline: none;
|
||||
transition: border-color 150ms ease, box-shadow 150ms ease;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.kalli-field:focus {
|
||||
border-color: var(--nak-blue);
|
||||
box-shadow: 0 0 0 3px rgba(39,134,207,0.2);
|
||||
background: #ffffff;
|
||||
}
|
||||
<section class="kalli-step" id="kalliStep3" hidden>
|
||||
<div class="kalli-modal-body">
|
||||
<div class="kalli-section-title">Zusätzliche Informationen</div>
|
||||
<div class="kalli-field-grid kalli-field-grid-single">
|
||||
<div class="kalli-field-group">
|
||||
<label class="kalli-label" for="kalliFurtherText">Textfeld <span class="kalli-label-note">(optional)</span></label>
|
||||
<input class="kalli-field" type="text" name="further_text" id="kalliFurtherText" value="<?= kalliHtml($formValues['further_text']) ?>" placeholder="z.B. betreffende Person, etc.">
|
||||
</div>
|
||||
<div class="kalli-field-group">
|
||||
<label class="kalli-label" for="kalliLocation">Ort <span class="kalli-label-note">(optional)</span></label>
|
||||
<input class="kalli-field" type="text" name="location" id="kalliLocation" value="<?= kalliHtml($formValues['location']) ?>">
|
||||
</div>
|
||||
<div class="kalli-field-group">
|
||||
<label class="kalli-label" for="kalliResponsibility">Verantwortliche/r <span class="kalli-label-note">(optional)</span></label>
|
||||
<input class="kalli-field" type="text" name="responsibility" id="kalliResponsibility" value="<?= kalliHtml($formValues['responsibility']) ?>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kalli-modal-actions">
|
||||
<div class="kalli-modal-actions-left">
|
||||
<button type="button" class="kalli-btn" data-kalli-prev="2">Zurück</button>
|
||||
</div>
|
||||
<div class="kalli-modal-actions-right">
|
||||
<button type="button" class="kalli-btn kalli-btn-primary" data-kalli-next="4">Weiter</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
/* ── Summary (Step 4) ── */
|
||||
.kalli-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
border: 1px solid var(--gray-200);
|
||||
}
|
||||
.kalli-summary-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--gray-100);
|
||||
}
|
||||
.kalli-summary-row:last-child { border-bottom: none; }
|
||||
.kalli-summary-label {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
.kalli-summary-value {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
color: var(--gray-900);
|
||||
text-align: right;
|
||||
}
|
||||
.kalli-validation-error {
|
||||
margin-top: var(--space-md);
|
||||
padding: 10px 14px;
|
||||
background: hsl(0,78%,95%);
|
||||
color: hsl(0,78%,40%);
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
/* ── Actions ── */
|
||||
.kalli-modal-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 16px 28px 20px;
|
||||
border-top: 1px solid var(--gray-200);
|
||||
}
|
||||
.kalli-modal-actions-left,
|
||||
.kalli-modal-actions-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.kalli-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 38px;
|
||||
padding: 8px 18px;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
font-family: var(--font-primary);
|
||||
border: 1px solid var(--gray-300);
|
||||
background: #ffffff;
|
||||
color: var(--gray-800);
|
||||
cursor: pointer;
|
||||
transition: background 150ms ease, transform 150ms ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.kalli-btn:hover {
|
||||
background: var(--gray-100);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.kalli-btn-primary {
|
||||
background: var(--nak-blue);
|
||||
border-color: var(--nak-blue);
|
||||
color: #ffffff;
|
||||
}
|
||||
.kalli-btn-primary:hover {
|
||||
background: var(--nak-blue-hover);
|
||||
border-color: var(--nak-blue-hover);
|
||||
}
|
||||
.kalli-btn-submit {
|
||||
background: linear-gradient(135deg, var(--nak-blue), var(--nak-blue-hover));
|
||||
border-color: var(--nak-blue-hover);
|
||||
color: #ffffff;
|
||||
padding: 10px 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.kalli-btn-submit:hover {
|
||||
background: linear-gradient(135deg, var(--nak-blue-hover), #185d8f);
|
||||
}
|
||||
|
||||
/* ── Responsive ── */
|
||||
@media (max-width: 600px) {
|
||||
.kalli-wizard { margin: 0; width: 100%; }
|
||||
.kalli-progress { padding: 14px 12px 10px; }
|
||||
.kalli-progress-label { font-size: 0.65rem; }
|
||||
.kalli-progress-circle { width: 26px; height: 26px; font-size: 0.7rem; }
|
||||
.kalli-progress-line { min-width: 12px; margin-bottom: 16px; }
|
||||
.kalli-modal-body { padding: 16px; }
|
||||
.kalli-category-grid { grid-template-columns: 1fr; }
|
||||
.kalli-field-grid { grid-template-columns: 1fr; }
|
||||
.kalli-modal-actions { padding: 12px 16px 16px; flex-wrap: wrap; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="kalli-wizard">
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="kalli-progress">
|
||||
<div class="kalli-progress-step active" data-step="1">
|
||||
<div class="kalli-progress-circle">1</div>
|
||||
<span class="kalli-progress-label">Terminart</span>
|
||||
</div>
|
||||
<div class="kalli-progress-line"></div>
|
||||
<div class="kalli-progress-step" data-step="2">
|
||||
<div class="kalli-progress-circle">2</div>
|
||||
<span class="kalli-progress-label">Zeitraum</span>
|
||||
</div>
|
||||
<div class="kalli-progress-line"></div>
|
||||
<div class="kalli-progress-step" data-step="3">
|
||||
<div class="kalli-progress-circle">3</div>
|
||||
<span class="kalli-progress-label">Informationen</span>
|
||||
</div>
|
||||
<div class="kalli-progress-line"></div>
|
||||
<div class="kalli-progress-step" data-step="4">
|
||||
<div class="kalli-progress-circle">4</div>
|
||||
<span class="kalli-progress-label">Übersicht</span>
|
||||
</div>
|
||||
<section class="kalli-step" id="kalliStep4" hidden>
|
||||
<div class="kalli-modal-body">
|
||||
<div class="kalli-section-title">Übersicht – Bitte prüfen Sie Ihre Eingaben</div>
|
||||
<div class="kalli-summary">
|
||||
<div class="kalli-summary-row">
|
||||
<span class="kalli-summary-label">Terminart</span>
|
||||
<span class="kalli-summary-value" id="sumGroup">–</span>
|
||||
</div>
|
||||
<div class="kalli-summary-row">
|
||||
<span class="kalli-summary-label">Datum</span>
|
||||
<span class="kalli-summary-value" id="sumDateS">–</span>
|
||||
</div>
|
||||
<?php foreach ([
|
||||
['sumDateERow', 'Enddatum', 'sumDateE'],
|
||||
['sumTimeSRow', 'Zeit von', 'sumTimeS'],
|
||||
['sumTimeERow', 'Zeit bis', 'sumTimeE'],
|
||||
['sumTextRow', 'Textfeld', 'sumText'],
|
||||
['sumLocRow', 'Ort', 'sumLoc'],
|
||||
['sumRespRow', 'Verantwortlich', 'sumResp'],
|
||||
] as [$rowId, $label, $valueId]): ?>
|
||||
<div class="kalli-summary-row" id="<?= $rowId ?>" hidden>
|
||||
<span class="kalli-summary-label"><?= kalliHtml($label) ?></span>
|
||||
<span class="kalli-summary-value" id="<?= $valueId ?>">–</span>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kalli-modal-actions">
|
||||
<div class="kalli-modal-actions-left">
|
||||
<button type="button" class="kalli-btn" data-kalli-prev="3">Zurück</button>
|
||||
</div>
|
||||
<div class="kalli-modal-actions-right">
|
||||
<button type="submit" name="submit" value="1" class="kalli-btn kalli-btn-submit">Eintragung absenden</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form wraps all steps -->
|
||||
<form id="kalliForm" action="" method="post">
|
||||
|
||||
<!-- STEP 1: Terminart -->
|
||||
<div class="kalli-step" id="kalliStep1">
|
||||
<div class="kalli-modal-body">
|
||||
<div class="kalli-section-title">Terminart auswählen</div>
|
||||
<div class="kalli-category-grid">
|
||||
<?php
|
||||
// Kategorien und Terminarten aus der Datenbank laden
|
||||
$catQuery = $pdo->query("SELECT * FROM kalli_categories ORDER BY sort_order, name");
|
||||
$kalliCategories = [];
|
||||
while ($cat = $catQuery->fetch(PDO::FETCH_ASSOC)) {
|
||||
$kalliCategories[] = $cat;
|
||||
}
|
||||
$typeQuery = $pdo->query("SELECT * FROM kalli_appointment_types ORDER BY sort_order, name");
|
||||
$kalliTypesByCategory = [];
|
||||
while ($type = $typeQuery->fetch(PDO::FETCH_ASSOC)) {
|
||||
$kalliTypesByCategory[$type['category_id']][] = $type;
|
||||
}
|
||||
|
||||
// Alle verwendeten Rollen sammeln für die "Terminart nicht aufgelistet"-Sektion
|
||||
$kalliAllRoles = [];
|
||||
$isAdminUser = (isset($groups["Admin"]) && $groups["Admin"] == 1);
|
||||
|
||||
foreach ($kalliCategories as $cat):
|
||||
$role = $cat['permission_role'];
|
||||
$kalliAllRoles[$role] = 1;
|
||||
if (!$isAdminUser && !(isset($groups[$role]) && $groups[$role] == 1)) continue;
|
||||
$types = $kalliTypesByCategory[$cat['id']] ?? [];
|
||||
if (empty($types)) continue;
|
||||
?>
|
||||
<div class="kalli-category">
|
||||
<div class="kalli-category-label"><?= htmlspecialchars($cat['name']) ?></div>
|
||||
<?php foreach ($types as $type):
|
||||
$displayLabel = !empty($type['display_label']) ? $type['display_label'] : $type['name'];
|
||||
?>
|
||||
<label class="kalli-radio-item"><input type="radio" name="group" value="<?= htmlspecialchars($type['name']) ?>"><span><?= htmlspecialchars($displayLabel) ?></span></label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$hasAnyKalliRole = $isAdminUser || count(array_intersect_key($groups, $kalliAllRoles)) > 0;
|
||||
if ($hasAnyKalliRole): ?>
|
||||
<div class="kalli-custom-section">
|
||||
<div class="kalli-category-label" style="color:var(--nak-red);">Terminart nicht aufgelistet?</div>
|
||||
<p style="margin:0 0 var(--space-sm) 0;font-size:var(--font-size-sm);color:var(--gray-600);">Nur ausfüllen, wenn die Terminart oben nicht vorhanden ist.</p>
|
||||
<div style="display:flex;gap:var(--space-lg);flex-wrap:wrap;margin-bottom:var(--space-sm);">
|
||||
<label class="kalli-radio-item"><input type="radio" name="group" value="oeffentlich"><span>Öffentlich</span></label>
|
||||
<label class="kalli-radio-item"><input type="radio" name="group" value="intern"><span>Intern</span></label>
|
||||
</div>
|
||||
<input class="kalli-field" type="text" name="userinput" id="kalliUserInput" placeholder="Titel (nur für benutzerdefinierte Termine)">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="kalli-modal-actions">
|
||||
<div></div>
|
||||
<div class="kalli-modal-actions-right">
|
||||
<button type="button" class="kalli-btn kalli-btn-primary" onclick="kalliNextStep(2)">Weiter</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 2: Zeitraum -->
|
||||
<div class="kalli-step" id="kalliStep2" style="display:none;">
|
||||
<div class="kalli-modal-body">
|
||||
<div class="kalli-section-title">Zeitraum festlegen</div>
|
||||
<div class="kalli-field-grid">
|
||||
<div class="kalli-field-group">
|
||||
<label class="kalli-label">Datum <span style="color:var(--nak-red);">*</span></label>
|
||||
<input class="kalli-field" type="date" name="date_s" id="kalliDateS" required>
|
||||
</div>
|
||||
<div class="kalli-field-group">
|
||||
<label class="kalli-label">Enddatum <span style="font-weight:300;color:var(--gray-500);">(falls mehrtägig)</span></label>
|
||||
<input class="kalli-field" type="date" name="date_e" id="kalliDateE">
|
||||
</div>
|
||||
<div class="kalli-field-group">
|
||||
<label class="kalli-label">Zeit von <span style="font-weight:300;color:var(--gray-500);">(optional)</span></label>
|
||||
<input class="kalli-field" type="time" name="time_s" id="kalliTimeS">
|
||||
</div>
|
||||
<div class="kalli-field-group">
|
||||
<label class="kalli-label">Zeit bis <span style="font-weight:300;color:var(--gray-500);">(optional)</span></label>
|
||||
<input class="kalli-field" type="time" name="time_e" id="kalliTimeE">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kalli-modal-actions">
|
||||
<div class="kalli-modal-actions-left">
|
||||
<button type="button" class="kalli-btn" onclick="kalliPrevStep(1)">Zurück</button>
|
||||
</div>
|
||||
<div class="kalli-modal-actions-right">
|
||||
<button type="button" class="kalli-btn kalli-btn-primary" onclick="kalliNextStep(3)">Weiter</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 3: Informationen -->
|
||||
<div class="kalli-step" id="kalliStep3" style="display:none;">
|
||||
<div class="kalli-modal-body">
|
||||
<div class="kalli-section-title">Zusätzliche Informationen</div>
|
||||
<div class="kalli-field-grid kalli-field-grid-single">
|
||||
<div class="kalli-field-group">
|
||||
<label class="kalli-label">Textfeld <span style="font-weight:300;color:var(--gray-500);">(optional)</span></label>
|
||||
<input class="kalli-field" type="text" name="further_text" id="kalliFurtherText" placeholder="z.B. betreffende Person, etc.">
|
||||
</div>
|
||||
<div class="kalli-field-group">
|
||||
<label class="kalli-label">Ort <span style="font-weight:300;color:var(--gray-500);">(optional)</span></label>
|
||||
<input class="kalli-field" type="text" name="location" id="kalliLocation">
|
||||
</div>
|
||||
<div class="kalli-field-group">
|
||||
<label class="kalli-label">Verantwortliche/r <span style="font-weight:300;color:var(--gray-500);">(optional)</span></label>
|
||||
<input class="kalli-field" type="text" name="responsibility" id="kalliResponsibility">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kalli-modal-actions">
|
||||
<div class="kalli-modal-actions-left">
|
||||
<button type="button" class="kalli-btn" onclick="kalliPrevStep(2)">Zurück</button>
|
||||
</div>
|
||||
<div class="kalli-modal-actions-right">
|
||||
<button type="button" class="kalli-btn kalli-btn-primary" onclick="kalliNextStep(4)">Weiter</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 4: Übersicht -->
|
||||
<div class="kalli-step" id="kalliStep4" style="display:none;">
|
||||
<div class="kalli-modal-body">
|
||||
<div class="kalli-section-title">Übersicht – Bitte prüfen Sie Ihre Eingaben</div>
|
||||
<div class="kalli-summary">
|
||||
<div class="kalli-summary-row">
|
||||
<span class="kalli-summary-label">Terminart</span>
|
||||
<span class="kalli-summary-value" id="sumGroup">–</span>
|
||||
</div>
|
||||
<div class="kalli-summary-row">
|
||||
<span class="kalli-summary-label">Datum</span>
|
||||
<span class="kalli-summary-value" id="sumDateS">–</span>
|
||||
</div>
|
||||
<div class="kalli-summary-row" id="sumDateERow" style="display:none;">
|
||||
<span class="kalli-summary-label">Enddatum</span>
|
||||
<span class="kalli-summary-value" id="sumDateE">–</span>
|
||||
</div>
|
||||
<div class="kalli-summary-row" id="sumTimeSRow" style="display:none;">
|
||||
<span class="kalli-summary-label">Zeit von</span>
|
||||
<span class="kalli-summary-value" id="sumTimeS">–</span>
|
||||
</div>
|
||||
<div class="kalli-summary-row" id="sumTimeERow" style="display:none;">
|
||||
<span class="kalli-summary-label">Zeit bis</span>
|
||||
<span class="kalli-summary-value" id="sumTimeE">–</span>
|
||||
</div>
|
||||
<div class="kalli-summary-row" id="sumTextRow" style="display:none;">
|
||||
<span class="kalli-summary-label">Textfeld</span>
|
||||
<span class="kalli-summary-value" id="sumText">–</span>
|
||||
</div>
|
||||
<div class="kalli-summary-row" id="sumLocRow" style="display:none;">
|
||||
<span class="kalli-summary-label">Ort</span>
|
||||
<span class="kalli-summary-value" id="sumLoc">–</span>
|
||||
</div>
|
||||
<div class="kalli-summary-row" id="sumRespRow" style="display:none;">
|
||||
<span class="kalli-summary-label">Verantwortlich</span>
|
||||
<span class="kalli-summary-value" id="sumResp">–</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="kalliValidationError" class="kalli-validation-error" style="display:none;"></div>
|
||||
</div>
|
||||
<div class="kalli-modal-actions">
|
||||
<div class="kalli-modal-actions-left">
|
||||
<button type="button" class="kalli-btn" onclick="kalliPrevStep(3)">Zurück</button>
|
||||
</div>
|
||||
<div class="kalli-modal-actions-right">
|
||||
<button type="submit" name="submit" class="kalli-btn kalli-btn-submit">Eintragung absenden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var kalliCurrentStep = 1;
|
||||
|
||||
function kalliGoToStep(step) {
|
||||
for (var i = 1; i <= 4; i++) {
|
||||
document.getElementById('kalliStep' + i).style.display = (i === step) ? '' : 'none';
|
||||
}
|
||||
kalliCurrentStep = step;
|
||||
kalliUpdateProgress(step);
|
||||
window.scrollTo({top: 0, behavior: 'smooth'});
|
||||
|
||||
// Header wieder einblenden, falls er durch Scrollen ausgeblendet war
|
||||
var headerHeight = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--header-height')) || 64;
|
||||
var elemHeaderMenue = document.getElementById('header_menue');
|
||||
var elemHeaderTitle = document.getElementById('header_title');
|
||||
if (elemHeaderMenue) { elemHeaderMenue.style.top = '0'; }
|
||||
if (elemHeaderTitle) { elemHeaderTitle.style.top = headerHeight + 'px'; }
|
||||
}
|
||||
|
||||
function kalliUpdateProgress(step) {
|
||||
var steps = document.querySelectorAll('.kalli-progress-step');
|
||||
var lines = document.querySelectorAll('.kalli-progress-line');
|
||||
steps.forEach(function(el, idx) {
|
||||
var s = idx + 1;
|
||||
el.classList.remove('active', 'done');
|
||||
if (s === step) el.classList.add('active');
|
||||
else if (s < step) el.classList.add('done');
|
||||
});
|
||||
lines.forEach(function(el, idx) {
|
||||
el.classList.toggle('done', idx < step - 1);
|
||||
});
|
||||
}
|
||||
|
||||
function kalliNextStep(step) {
|
||||
var errEl = document.getElementById('kalliValidationError');
|
||||
if (errEl) { errEl.style.display = 'none'; errEl.textContent = ''; }
|
||||
|
||||
if (step === 2) {
|
||||
var sel = document.querySelector('#kalliForm input[name="group"]:checked');
|
||||
if (!sel) { alert('Bitte wählen Sie eine Terminart aus.'); return; }
|
||||
if ((sel.value === 'oeffentlich' || sel.value === 'intern') && !document.getElementById('kalliUserInput').value.trim()) {
|
||||
alert('Bitte geben Sie einen Titel für den benutzerdefinierten Termin ein.'); return;
|
||||
}
|
||||
}
|
||||
if (step === 3) {
|
||||
if (!document.getElementById('kalliDateS').value) { alert('Bitte geben Sie ein Datum ein.'); return; }
|
||||
}
|
||||
if (step === 4) { kalliBuildSummary(); }
|
||||
kalliGoToStep(step);
|
||||
}
|
||||
|
||||
function kalliPrevStep(step) { kalliGoToStep(step); }
|
||||
|
||||
function kalliBuildSummary() {
|
||||
var grp = document.querySelector('#kalliForm input[name="group"]:checked');
|
||||
var grpVal = grp ? grp.value : '–';
|
||||
if ((grpVal === 'oeffentlich' || grpVal === 'intern') && document.getElementById('kalliUserInput')) {
|
||||
var custom = document.getElementById('kalliUserInput').value.trim();
|
||||
grpVal = custom ? custom + ' (' + (grpVal === 'oeffentlich' ? 'Öffentlich' : 'Intern') + ')' : grpVal;
|
||||
}
|
||||
document.getElementById('sumGroup').textContent = grpVal;
|
||||
document.getElementById('sumDateS').textContent = kalliFormatDate(document.getElementById('kalliDateS').value);
|
||||
kalliShowSummaryRow('sumDateERow', document.getElementById('kalliDateE').value, 'sumDateE', kalliFormatDate(document.getElementById('kalliDateE').value));
|
||||
kalliShowSummaryRow('sumTimeSRow', document.getElementById('kalliTimeS').value, 'sumTimeS', document.getElementById('kalliTimeS').value);
|
||||
kalliShowSummaryRow('sumTimeERow', document.getElementById('kalliTimeE').value, 'sumTimeE', document.getElementById('kalliTimeE').value);
|
||||
kalliShowSummaryRow('sumTextRow', document.getElementById('kalliFurtherText').value.trim(), 'sumText', document.getElementById('kalliFurtherText').value.trim());
|
||||
kalliShowSummaryRow('sumLocRow', document.getElementById('kalliLocation').value.trim(), 'sumLoc', document.getElementById('kalliLocation').value.trim());
|
||||
kalliShowSummaryRow('sumRespRow', document.getElementById('kalliResponsibility').value.trim(), 'sumResp', document.getElementById('kalliResponsibility').value.trim());
|
||||
}
|
||||
|
||||
function kalliShowSummaryRow(rowId, value, valId, displayVal) {
|
||||
var row = document.getElementById(rowId);
|
||||
if (value) { row.style.display = ''; document.getElementById(valId).textContent = displayVal; }
|
||||
else { row.style.display = 'none'; }
|
||||
}
|
||||
|
||||
function kalliFormatDate(iso) {
|
||||
if (!iso) return '–';
|
||||
var parts = iso.split('-');
|
||||
return parts.length === 3 ? parts[2] + '.' + parts[1] + '.' + parts[0] : iso;
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php endif; ?>
|
||||
|
||||
</div><!-- content -->
|
||||
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
|
||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/projects/_central/php/footer.php'; ?>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,746 +0,0 @@
|
||||
<!-- Kalli Eintragungsassistent – Mehrstufiges Modal -->
|
||||
<div class="standard_overlay" id="kalliModal" onclick="if(event.target.id==='kalliModal')closeKalliModal();" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;justify-content:center;align-items:center;background-color:hsla(210,57%,25%,0.8);z-index:9999;">
|
||||
<div class="kalli-modal-card">
|
||||
|
||||
<!-- Close Button -->
|
||||
<button type="button" class="kalli-modal-exit" onclick="closeKalliModal()" aria-label="Schließen">×</button>
|
||||
|
||||
<!-- Hero Header -->
|
||||
<div class="kalli-modal-hero">
|
||||
<div>
|
||||
<div class="kalli-modal-title">Kalli – Eintragungsassistent</div>
|
||||
<div class="kalli-modal-subtitle">Neuen Termin im CKalender eintragen</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="kalli-progress">
|
||||
<div class="kalli-progress-step active" data-step="1">
|
||||
<div class="kalli-progress-circle">1</div>
|
||||
<span class="kalli-progress-label">Terminart</span>
|
||||
</div>
|
||||
<div class="kalli-progress-line"></div>
|
||||
<div class="kalli-progress-step" data-step="2">
|
||||
<div class="kalli-progress-circle">2</div>
|
||||
<span class="kalli-progress-label">Zeitraum</span>
|
||||
</div>
|
||||
<div class="kalli-progress-line"></div>
|
||||
<div class="kalli-progress-step" data-step="3">
|
||||
<div class="kalli-progress-circle">3</div>
|
||||
<span class="kalli-progress-label">Informationen</span>
|
||||
</div>
|
||||
<div class="kalli-progress-line"></div>
|
||||
<div class="kalli-progress-step" data-step="4">
|
||||
<div class="kalli-progress-circle">4</div>
|
||||
<span class="kalli-progress-label">Übersicht</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form wraps all steps -->
|
||||
<form id="kalliForm" action="/intranet_repo/projects/kalli/kalli.php" method="post">
|
||||
|
||||
<!-- STEP 1: Terminart -->
|
||||
<div class="kalli-step" id="kalliStep1">
|
||||
<div class="kalli-modal-body">
|
||||
<div class="kalli-section-title">Terminart auswählen</div>
|
||||
|
||||
<div class="kalli-category-grid">
|
||||
<?php
|
||||
$isAdminUser = (isset($groups["Admin"]) && $groups["Admin"] == 1);
|
||||
|
||||
// Kategorien und Terminarten aus der Datenbank laden
|
||||
$catQueryModal = $pdo->query("SELECT * FROM kalli_categories ORDER BY sort_order, name");
|
||||
$kalliCategoriesModal = [];
|
||||
while ($catM = $catQueryModal->fetch(PDO::FETCH_ASSOC)) {
|
||||
$kalliCategoriesModal[] = $catM;
|
||||
}
|
||||
$typeQueryModal = $pdo->query("SELECT * FROM kalli_appointment_types ORDER BY sort_order, name");
|
||||
$kalliTypesByCategoryModal = [];
|
||||
while ($typeM = $typeQueryModal->fetch(PDO::FETCH_ASSOC)) {
|
||||
$kalliTypesByCategoryModal[$typeM['category_id']][] = $typeM;
|
||||
}
|
||||
|
||||
$kalliAllRolesModal = [];
|
||||
foreach ($kalliCategoriesModal as $catM):
|
||||
$roleM = $catM['permission_role'];
|
||||
$kalliAllRolesModal[$roleM] = 1;
|
||||
if (!$isAdminUser && !(isset($groups[$roleM]) && $groups[$roleM] == 1)) continue;
|
||||
$typesM = $kalliTypesByCategoryModal[$catM['id']] ?? [];
|
||||
if (empty($typesM)) continue;
|
||||
?>
|
||||
<div class="kalli-category">
|
||||
<div class="kalli-category-label"><?= htmlspecialchars($catM['name']) ?></div>
|
||||
<?php foreach ($typesM as $typeM):
|
||||
$displayLabelM = !empty($typeM['display_label']) ? $typeM['display_label'] : $typeM['name'];
|
||||
?>
|
||||
<label class="kalli-radio-item"><input type="radio" name="group" value="<?= htmlspecialchars($typeM['name']) ?>"><span><?= htmlspecialchars($displayLabelM) ?></span></label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$hasAnyKalliRoleModal = $isAdminUser || count(array_intersect_key($groups, $kalliAllRolesModal)) > 0;
|
||||
if ($hasAnyKalliRoleModal): ?>
|
||||
<div class="kalli-custom-section">
|
||||
<div class="kalli-category-label" style="color:var(--nak-red);">Terminart nicht aufgelistet?</div>
|
||||
<p style="margin:0 0 var(--space-sm) 0;font-size:var(--font-size-sm);color:var(--gray-600);">Nur ausfüllen, wenn die Terminart oben nicht vorhanden ist.</p>
|
||||
<div style="display:flex;gap:var(--space-lg);flex-wrap:wrap;margin-bottom:var(--space-sm);">
|
||||
<label class="kalli-radio-item"><input type="radio" name="group" value="oeffentlich"><span>Öffentlich</span></label>
|
||||
<label class="kalli-radio-item"><input type="radio" name="group" value="intern"><span>Intern</span></label>
|
||||
</div>
|
||||
<input class="kalli-field" type="text" name="userinput" id="kalliUserInput" placeholder="Titel (nur für benutzerdefinierte Termine)">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="kalli-modal-actions">
|
||||
<div></div>
|
||||
<div class="kalli-modal-actions-right">
|
||||
<button type="button" class="kalli-btn" onclick="closeKalliModal()">Abbrechen</button>
|
||||
<button type="button" class="kalli-btn kalli-btn-primary" onclick="kalliNextStep(2)">Weiter</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 2: Zeitraum -->
|
||||
<div class="kalli-step" id="kalliStep2" style="display:none;">
|
||||
<div class="kalli-modal-body">
|
||||
<div class="kalli-section-title">Zeitraum festlegen</div>
|
||||
|
||||
<div class="kalli-field-grid">
|
||||
<div class="kalli-field-group">
|
||||
<label class="kalli-label">Datum <span style="color:var(--nak-red);">*</span></label>
|
||||
<input class="kalli-field" type="date" name="date_s" id="kalliDateS" required>
|
||||
</div>
|
||||
<div class="kalli-field-group">
|
||||
<label class="kalli-label">Enddatum <span style="font-weight:300;color:var(--gray-500);">(falls mehrtägig)</span></label>
|
||||
<input class="kalli-field" type="date" name="date_e" id="kalliDateE">
|
||||
</div>
|
||||
<div class="kalli-field-group">
|
||||
<label class="kalli-label">Zeit von <span style="font-weight:300;color:var(--gray-500);">(optional)</span></label>
|
||||
<input class="kalli-field" type="time" name="time_s" id="kalliTimeS">
|
||||
</div>
|
||||
<div class="kalli-field-group">
|
||||
<label class="kalli-label">Zeit bis <span style="font-weight:300;color:var(--gray-500);">(optional)</span></label>
|
||||
<input class="kalli-field" type="time" name="time_e" id="kalliTimeE">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kalli-modal-actions">
|
||||
<div class="kalli-modal-actions-left">
|
||||
<button type="button" class="kalli-btn" onclick="kalliPrevStep(1)">Zurück</button>
|
||||
</div>
|
||||
<div class="kalli-modal-actions-right">
|
||||
<button type="button" class="kalli-btn kalli-btn-primary" onclick="kalliNextStep(3)">Weiter</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 3: Informationen -->
|
||||
<div class="kalli-step" id="kalliStep3" style="display:none;">
|
||||
<div class="kalli-modal-body">
|
||||
<div class="kalli-section-title">Zusätzliche Informationen</div>
|
||||
|
||||
<div class="kalli-field-grid kalli-field-grid-single">
|
||||
<div class="kalli-field-group">
|
||||
<label class="kalli-label">Textfeld <span style="font-weight:300;color:var(--gray-500);">(optional)</span></label>
|
||||
<input class="kalli-field" type="text" name="further_text" id="kalliFurtherText" placeholder="z.B. betreffende Person, etc.">
|
||||
</div>
|
||||
<div class="kalli-field-group">
|
||||
<label class="kalli-label">Ort <span style="font-weight:300;color:var(--gray-500);">(optional)</span></label>
|
||||
<input class="kalli-field" type="text" name="location" id="kalliLocation">
|
||||
</div>
|
||||
<div class="kalli-field-group">
|
||||
<label class="kalli-label">Verantwortliche/r <span style="font-weight:300;color:var(--gray-500);">(optional)</span></label>
|
||||
<input class="kalli-field" type="text" name="responsibility" id="kalliResponsibility">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kalli-modal-actions">
|
||||
<div class="kalli-modal-actions-left">
|
||||
<button type="button" class="kalli-btn" onclick="kalliPrevStep(2)">Zurück</button>
|
||||
</div>
|
||||
<div class="kalli-modal-actions-right">
|
||||
<button type="button" class="kalli-btn kalli-btn-primary" onclick="kalliNextStep(4)">Weiter</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STEP 4: Übersicht -->
|
||||
<div class="kalli-step" id="kalliStep4" style="display:none;">
|
||||
<div class="kalli-modal-body">
|
||||
<div class="kalli-section-title">Übersicht – Bitte prüfen Sie Ihre Eingaben</div>
|
||||
|
||||
<div class="kalli-summary">
|
||||
<div class="kalli-summary-row">
|
||||
<span class="kalli-summary-label">Terminart</span>
|
||||
<span class="kalli-summary-value" id="sumGroup">–</span>
|
||||
</div>
|
||||
<div class="kalli-summary-row">
|
||||
<span class="kalli-summary-label">Datum</span>
|
||||
<span class="kalli-summary-value" id="sumDateS">–</span>
|
||||
</div>
|
||||
<div class="kalli-summary-row" id="sumDateERow" style="display:none;">
|
||||
<span class="kalli-summary-label">Enddatum</span>
|
||||
<span class="kalli-summary-value" id="sumDateE">–</span>
|
||||
</div>
|
||||
<div class="kalli-summary-row" id="sumTimeSRow" style="display:none;">
|
||||
<span class="kalli-summary-label">Zeit von</span>
|
||||
<span class="kalli-summary-value" id="sumTimeS">–</span>
|
||||
</div>
|
||||
<div class="kalli-summary-row" id="sumTimeERow" style="display:none;">
|
||||
<span class="kalli-summary-label">Zeit bis</span>
|
||||
<span class="kalli-summary-value" id="sumTimeE">–</span>
|
||||
</div>
|
||||
<div class="kalli-summary-row" id="sumTextRow" style="display:none;">
|
||||
<span class="kalli-summary-label">Textfeld</span>
|
||||
<span class="kalli-summary-value" id="sumText">–</span>
|
||||
</div>
|
||||
<div class="kalli-summary-row" id="sumLocRow" style="display:none;">
|
||||
<span class="kalli-summary-label">Ort</span>
|
||||
<span class="kalli-summary-value" id="sumLoc">–</span>
|
||||
</div>
|
||||
<div class="kalli-summary-row" id="sumRespRow" style="display:none;">
|
||||
<span class="kalli-summary-label">Verantwortlich</span>
|
||||
<span class="kalli-summary-value" id="sumResp">–</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="kalliValidationError" class="kalli-validation-error" style="display:none;"></div>
|
||||
</div>
|
||||
|
||||
<div class="kalli-modal-actions">
|
||||
<div class="kalli-modal-actions-left">
|
||||
<button type="button" class="kalli-btn" onclick="kalliPrevStep(3)">Zurück</button>
|
||||
</div>
|
||||
<div class="kalli-modal-actions-right">
|
||||
<button type="submit" name="submit" class="kalli-btn kalli-btn-submit">Eintragung absenden</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* ── Kalli Modal ── */
|
||||
.kalli-modal-card {
|
||||
position: relative;
|
||||
width: min(720px, calc(100vw - 24px));
|
||||
max-height: min(90vh, calc(100vh - 24px));
|
||||
overflow-y: auto;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 24px 70px rgba(0,0,0,0.36);
|
||||
}
|
||||
|
||||
.kalli-modal-exit {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 14px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border: 1px solid rgba(255,255,255,0.25);
|
||||
background: rgba(255,255,255,0.16);
|
||||
color: #ffffff;
|
||||
font-size: 1.35em;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.kalli-modal-hero {
|
||||
padding: 24px 28px;
|
||||
color: #ffffff;
|
||||
background:
|
||||
radial-gradient(circle at 10% 0%, rgba(255,255,255,0.24), transparent 32%),
|
||||
linear-gradient(135deg, var(--nak-blue), var(--nak-blue-hover));
|
||||
}
|
||||
|
||||
.kalli-modal-title {
|
||||
font-size: 1.35em;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.kalli-modal-subtitle {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 300;
|
||||
color: rgba(255,255,255,0.8);
|
||||
}
|
||||
|
||||
/* ── Progress Bar ── */
|
||||
.kalli-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
padding: 20px 28px 16px;
|
||||
background: var(--gray-50);
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.kalli-progress-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.kalli-progress-circle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
color: var(--gray-500);
|
||||
background: var(--gray-200);
|
||||
border: 2px solid var(--gray-300);
|
||||
transition: all 250ms ease;
|
||||
}
|
||||
|
||||
.kalli-progress-step.active .kalli-progress-circle {
|
||||
background: var(--nak-blue);
|
||||
border-color: var(--nak-blue);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 2px 8px rgba(39,134,207,0.35);
|
||||
}
|
||||
|
||||
.kalli-progress-step.done .kalli-progress-circle {
|
||||
background: #ffffff;
|
||||
border-color: var(--nak-blue);
|
||||
color: var(--nak-blue);
|
||||
}
|
||||
|
||||
.kalli-progress-label {
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 500;
|
||||
color: var(--gray-500);
|
||||
white-space: nowrap;
|
||||
transition: color 250ms ease;
|
||||
}
|
||||
|
||||
.kalli-progress-step.active .kalli-progress-label,
|
||||
.kalli-progress-step.done .kalli-progress-label {
|
||||
color: var(--nak-blue);
|
||||
}
|
||||
|
||||
.kalli-progress-line {
|
||||
flex: 1;
|
||||
height: 2px;
|
||||
min-width: 20px;
|
||||
max-width: 60px;
|
||||
background: var(--gray-300);
|
||||
margin: 0 6px;
|
||||
margin-bottom: 20px;
|
||||
transition: background 250ms ease;
|
||||
}
|
||||
|
||||
.kalli-progress-line.done {
|
||||
background: var(--nak-blue);
|
||||
}
|
||||
|
||||
/* ── Modal Body ── */
|
||||
.kalli-modal-body {
|
||||
padding: 22px 28px 16px;
|
||||
}
|
||||
|
||||
.kalli-section-title {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
/* ── Category Grid (Step 1) ── */
|
||||
.kalli-category-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: var(--space-lg);
|
||||
}
|
||||
|
||||
.kalli-category {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.kalli-category-label {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
color: var(--gray-700);
|
||||
margin-bottom: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
label.kalli-radio-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 400;
|
||||
color: var(--gray-800);
|
||||
border: 1px solid transparent;
|
||||
transition: background 150ms ease, border-color 150ms ease;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
label.kalli-radio-item input[type="radio"],
|
||||
label.kalli-radio-item input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: var(--nak-blue);
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
align-self: center;
|
||||
}
|
||||
label.kalli-radio-item span {
|
||||
hyphens: auto;
|
||||
-webkit-hyphens: auto;
|
||||
-ms-hyphens: auto;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
line-height: 1.4;
|
||||
}
|
||||
label.kalli-radio-item:hover {
|
||||
background: var(--gray-50);
|
||||
border-color: var(--gray-200);
|
||||
}
|
||||
|
||||
.kalli-custom-section {
|
||||
margin-top: var(--space-lg);
|
||||
padding: var(--space-md);
|
||||
background: hsl(0,60%,98%);
|
||||
border: 1px solid hsl(0,60%,90%);
|
||||
}
|
||||
|
||||
/* ── Field Grid (Steps 2 & 3) ── */
|
||||
.kalli-field-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.kalli-field-grid-single {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.kalli-field-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.kalli-label {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
|
||||
.kalli-field {
|
||||
padding: 12px 14px;
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-primary);
|
||||
font-weight: 300;
|
||||
border: 1px solid var(--gray-300);
|
||||
background: var(--gray-50);
|
||||
outline: none;
|
||||
transition: border-color 150ms ease, box-shadow 150ms ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.kalli-field:focus {
|
||||
border-color: var(--nak-blue);
|
||||
box-shadow: 0 0 0 3px var(--nak-blue-20);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* ── Summary (Step 4) ── */
|
||||
.kalli-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
border: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.kalli-summary-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--gray-100);
|
||||
}
|
||||
|
||||
.kalli-summary-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.kalli-summary-label {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.kalli-summary-value {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 500;
|
||||
color: var(--gray-900);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.kalli-validation-error {
|
||||
margin-top: var(--space-md);
|
||||
padding: 10px 14px;
|
||||
background: hsl(0,78%,95%);
|
||||
color: hsl(0,78%,40%);
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
/* ── Actions ── */
|
||||
.kalli-modal-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 16px 28px 20px;
|
||||
border-top: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.kalli-modal-actions-left,
|
||||
.kalli-modal-actions-right {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.kalli-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 38px;
|
||||
padding: 8px 18px;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
font-family: var(--font-primary);
|
||||
border: 1px solid var(--gray-300);
|
||||
background: #ffffff;
|
||||
color: var(--gray-800);
|
||||
cursor: pointer;
|
||||
transition: background 150ms ease, transform 150ms ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.kalli-btn:hover {
|
||||
background: var(--gray-100);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.kalli-btn-primary {
|
||||
background: var(--nak-blue);
|
||||
border-color: var(--nak-blue);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.kalli-btn-primary:hover {
|
||||
background: var(--nak-blue-hover);
|
||||
border-color: var(--nak-blue-hover);
|
||||
}
|
||||
|
||||
.kalli-btn-submit {
|
||||
background: linear-gradient(135deg, var(--nak-blue), var(--nak-blue-hover));
|
||||
border-color: var(--nak-blue-hover);
|
||||
color: #ffffff;
|
||||
padding: 10px 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.kalli-btn-submit:hover {
|
||||
background: linear-gradient(135deg, var(--nak-blue-hover), #185d8f);
|
||||
}
|
||||
|
||||
/* ── Responsive ── */
|
||||
@media (max-width: 600px) {
|
||||
.kalli-modal-card {
|
||||
width: 100vw;
|
||||
max-height: 100vh;
|
||||
max-height: 100dvh;
|
||||
}
|
||||
|
||||
.kalli-modal-hero {
|
||||
padding: 18px 16px;
|
||||
}
|
||||
|
||||
.kalli-modal-title {
|
||||
font-size: 1.15em;
|
||||
}
|
||||
|
||||
.kalli-progress {
|
||||
padding: 14px 12px 10px;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.kalli-progress-label {
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.kalli-progress-circle {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.kalli-progress-line {
|
||||
min-width: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.kalli-modal-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.kalli-category-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.kalli-field-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.kalli-modal-actions {
|
||||
padding: 12px 16px 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
var kalliCurrentStep = 1;
|
||||
|
||||
function openKalliModal() {
|
||||
document.getElementById('kalliModal').style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
kalliGoToStep(1);
|
||||
}
|
||||
|
||||
function closeKalliModal() {
|
||||
document.getElementById('kalliModal').style.display = 'none';
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
function kalliGoToStep(step) {
|
||||
for (var i = 1; i <= 4; i++) {
|
||||
document.getElementById('kalliStep' + i).style.display = (i === step) ? '' : 'none';
|
||||
}
|
||||
kalliCurrentStep = step;
|
||||
kalliUpdateProgress(step);
|
||||
// Scroll modal body to top
|
||||
document.querySelector('.kalli-modal-card').scrollTop = 0;
|
||||
}
|
||||
|
||||
function kalliUpdateProgress(step) {
|
||||
var steps = document.querySelectorAll('.kalli-progress-step');
|
||||
var lines = document.querySelectorAll('.kalli-progress-line');
|
||||
steps.forEach(function(el, idx) {
|
||||
var s = idx + 1;
|
||||
el.classList.remove('active', 'done');
|
||||
if (s === step) el.classList.add('active');
|
||||
else if (s < step) el.classList.add('done');
|
||||
});
|
||||
lines.forEach(function(el, idx) {
|
||||
el.classList.toggle('done', idx < step - 1);
|
||||
});
|
||||
}
|
||||
|
||||
function kalliNextStep(step) {
|
||||
var errEl = document.getElementById('kalliValidationError');
|
||||
if (errEl) { errEl.style.display = 'none'; errEl.textContent = ''; }
|
||||
|
||||
if (step === 2) {
|
||||
// Validate step 1: a group must be selected
|
||||
var sel = document.querySelector('#kalliForm input[name="group"]:checked');
|
||||
if (!sel) {
|
||||
alert('Bitte wählen Sie eine Terminart aus.');
|
||||
return;
|
||||
}
|
||||
// If custom group, require title
|
||||
if ((sel.value === 'oeffentlich' || sel.value === 'intern') && !document.getElementById('kalliUserInput').value.trim()) {
|
||||
alert('Bitte geben Sie einen Titel für den benutzerdefinierten Termin ein.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (step === 3) {
|
||||
// Validate step 2: date is required
|
||||
if (!document.getElementById('kalliDateS').value) {
|
||||
alert('Bitte geben Sie ein Datum ein.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (step === 4) {
|
||||
kalliBuildSummary();
|
||||
}
|
||||
kalliGoToStep(step);
|
||||
}
|
||||
|
||||
function kalliPrevStep(step) {
|
||||
kalliGoToStep(step);
|
||||
}
|
||||
|
||||
function kalliBuildSummary() {
|
||||
var grp = document.querySelector('#kalliForm input[name="group"]:checked');
|
||||
var grpVal = grp ? grp.value : '–';
|
||||
if ((grpVal === 'oeffentlich' || grpVal === 'intern') && document.getElementById('kalliUserInput')) {
|
||||
var custom = document.getElementById('kalliUserInput').value.trim();
|
||||
grpVal = custom ? custom + ' (' + (grpVal === 'oeffentlich' ? 'Öffentlich' : 'Intern') + ')' : grpVal;
|
||||
}
|
||||
document.getElementById('sumGroup').textContent = grpVal;
|
||||
|
||||
var dateS = document.getElementById('kalliDateS').value;
|
||||
document.getElementById('sumDateS').textContent = dateS ? kalliFormatDate(dateS) : '–';
|
||||
|
||||
var dateE = document.getElementById('kalliDateE').value;
|
||||
kalliShowSummaryRow('sumDateERow', dateE, 'sumDateE', kalliFormatDate(dateE));
|
||||
|
||||
var timeS = document.getElementById('kalliTimeS').value;
|
||||
kalliShowSummaryRow('sumTimeSRow', timeS, 'sumTimeS', timeS);
|
||||
|
||||
var timeE = document.getElementById('kalliTimeE').value;
|
||||
kalliShowSummaryRow('sumTimeERow', timeE, 'sumTimeE', timeE);
|
||||
|
||||
var text = document.getElementById('kalliFurtherText').value.trim();
|
||||
kalliShowSummaryRow('sumTextRow', text, 'sumText', text);
|
||||
|
||||
var loc = document.getElementById('kalliLocation').value.trim();
|
||||
kalliShowSummaryRow('sumLocRow', loc, 'sumLoc', loc);
|
||||
|
||||
var resp = document.getElementById('kalliResponsibility').value.trim();
|
||||
kalliShowSummaryRow('sumRespRow', resp, 'sumResp', resp);
|
||||
}
|
||||
|
||||
function kalliShowSummaryRow(rowId, value, valId, displayVal) {
|
||||
var row = document.getElementById(rowId);
|
||||
if (value) {
|
||||
row.style.display = '';
|
||||
document.getElementById(valId).textContent = displayVal;
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function kalliFormatDate(iso) {
|
||||
if (!iso) return '–';
|
||||
var parts = iso.split('-');
|
||||
if (parts.length === 3) return parts[2] + '.' + parts[1] + '.' + parts[0];
|
||||
return iso;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,405 @@
|
||||
<?php
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
|
||||
const KALLI_CKAL_HEADERS = [
|
||||
'ID',
|
||||
'Lang-ID',
|
||||
'Termin-Typ',
|
||||
'Von-Datum',
|
||||
'Von-Zeit',
|
||||
'Bis-Datum',
|
||||
'Bis-Zeit',
|
||||
'Zeitzusatz',
|
||||
'Titel',
|
||||
'Text',
|
||||
'Ort',
|
||||
'Planungsstatus',
|
||||
'Anzeigeziel',
|
||||
'Gruppenzuordnung',
|
||||
'Zyklusart',
|
||||
'Zyklusintervall',
|
||||
'Zyklusdetail',
|
||||
'Notiz',
|
||||
'Verantwortlich',
|
||||
'Hintergrundfarbe',
|
||||
'BucherID',
|
||||
'Benutzerdefinierte Felder',
|
||||
'Dokument-Titel',
|
||||
'Anzeigeziel Ort',
|
||||
'Anzeigeziel Notiz',
|
||||
'Anzeigeziel Verantwortlicher',
|
||||
'Erzeugt',
|
||||
'Aktualisiert',
|
||||
];
|
||||
|
||||
const KALLI_CUSTOM_PUBLIC = 'oeffentlich';
|
||||
const KALLI_CUSTOM_INTERNAL = 'intern';
|
||||
|
||||
function kalliHtml($value): string
|
||||
{
|
||||
return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function kalliPermissionEnabled($value): bool
|
||||
{
|
||||
return $value === true || $value === 1 || $value === '1' || $value === 't' || $value === 'true';
|
||||
}
|
||||
|
||||
function kalliCsrfToken(): string
|
||||
{
|
||||
if (empty($_SESSION['kalli_csrf'])) {
|
||||
$_SESSION['kalli_csrf'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
return $_SESSION['kalli_csrf'];
|
||||
}
|
||||
|
||||
function kalliCsrfIsValid($postedToken): bool
|
||||
{
|
||||
$sessionToken = $_SESSION['kalli_csrf'] ?? '';
|
||||
|
||||
return is_string($postedToken)
|
||||
&& is_string($sessionToken)
|
||||
&& $sessionToken !== ''
|
||||
&& hash_equals($sessionToken, $postedToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt Kategorien und Terminarten in einer Abfrage. Die Berechtigungsfilterung
|
||||
* erfolgt anschließend zentral in kalliBuildCatalog().
|
||||
*/
|
||||
function kalliFetchCatalogRows(PDO $pdo): array
|
||||
{
|
||||
$sql = <<<'SQL'
|
||||
SELECT
|
||||
c.id AS category_id,
|
||||
c.name AS category_name,
|
||||
c.permission_role,
|
||||
t.id AS type_id,
|
||||
t.name AS type_name,
|
||||
t.display_label,
|
||||
t.planungsstatus,
|
||||
t.anzeigeziel,
|
||||
t.gruppenzuordnung,
|
||||
t.anzeigeziel_ort,
|
||||
t.anzeigeziel_notiz,
|
||||
t.anzeigeziel_verantwortlicher,
|
||||
t.default_text
|
||||
FROM kalli_categories c
|
||||
LEFT JOIN kalli_appointment_types t ON t.category_id = c.id
|
||||
ORDER BY c.sort_order, c.name, t.sort_order, t.name
|
||||
SQL;
|
||||
|
||||
return $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt die einzige Berechtigungs- und Preset-Sicht für UI und POST-Verarbeitung.
|
||||
*/
|
||||
function kalliBuildCatalog(array $rows, array $groups): array
|
||||
{
|
||||
$isAdmin = kalliPermissionEnabled($groups['Admin'] ?? null);
|
||||
$categories = [];
|
||||
$typesByName = [];
|
||||
$allowCustom = $isAdmin;
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$role = (string)($row['permission_role'] ?? '');
|
||||
$allowed = $isAdmin || kalliPermissionEnabled($groups[$role] ?? null);
|
||||
if (!$allowed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Entspricht dem bisherigen Verhalten: Wer mindestens eine Kalli-Kategorie
|
||||
// sehen darf, darf auch einen benutzerdefinierten Termin anlegen.
|
||||
$allowCustom = true;
|
||||
|
||||
if ($row['type_id'] === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$categoryId = (string)$row['category_id'];
|
||||
if (!isset($categories[$categoryId])) {
|
||||
$categories[$categoryId] = [
|
||||
'id' => $row['category_id'],
|
||||
'name' => (string)$row['category_name'],
|
||||
'types' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$type = [
|
||||
'id' => $row['type_id'],
|
||||
'name' => (string)$row['type_name'],
|
||||
'display_label' => (string)($row['display_label'] ?? ''),
|
||||
'planungsstatus' => (string)($row['planungsstatus'] ?? ''),
|
||||
'anzeigeziel' => (string)($row['anzeigeziel'] ?? ''),
|
||||
'gruppenzuordnung' => (string)($row['gruppenzuordnung'] ?? ''),
|
||||
'anzeigeziel_ort' => (string)($row['anzeigeziel_ort'] ?? ''),
|
||||
'anzeigeziel_notiz' => (string)($row['anzeigeziel_notiz'] ?? ''),
|
||||
'anzeigeziel_verantwortlicher' => (string)($row['anzeigeziel_verantwortlicher'] ?? ''),
|
||||
'default_text' => (string)($row['default_text'] ?? ''),
|
||||
];
|
||||
|
||||
$categories[$categoryId]['types'][] = $type;
|
||||
|
||||
// Das Formular übermittelt aus Kompatibilitätsgründen weiterhin den Namen.
|
||||
// Bei versehentlichen Dubletten entspricht der erste sortierte Treffer dem
|
||||
// deterministischen Ersatz für das frühere "LIMIT 1" ohne ORDER BY.
|
||||
if (!isset($typesByName[$type['name']])) {
|
||||
$typesByName[$type['name']] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'categories' => array_values($categories),
|
||||
'types_by_name' => $typesByName,
|
||||
'allow_custom' => $allowCustom,
|
||||
];
|
||||
}
|
||||
|
||||
function kalliFormValues(array $source = []): array
|
||||
{
|
||||
$keys = [
|
||||
'group',
|
||||
'userinput',
|
||||
'date_s',
|
||||
'date_e',
|
||||
'time_s',
|
||||
'time_e',
|
||||
'further_text',
|
||||
'location',
|
||||
'responsibility',
|
||||
];
|
||||
|
||||
$values = [];
|
||||
foreach ($keys as $key) {
|
||||
$value = $source[$key] ?? '';
|
||||
$values[$key] = is_scalar($value) ? trim((string)$value) : '';
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
function kalliParseDate(string $value, string $label, bool $required = false): ?DateTimeImmutable
|
||||
{
|
||||
if ($value === '') {
|
||||
if ($required) {
|
||||
throw new InvalidArgumentException($label . ' fehlt.');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$date = DateTimeImmutable::createFromFormat('!Y-m-d', $value);
|
||||
$errors = DateTimeImmutable::getLastErrors();
|
||||
$hasErrors = $errors !== false && ($errors['warning_count'] > 0 || $errors['error_count'] > 0);
|
||||
|
||||
if (!$date || $hasErrors || $date->format('Y-m-d') !== $value) {
|
||||
throw new InvalidArgumentException($label . ' ist ungültig.');
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
function kalliParseTime(string $value, string $label): ?string
|
||||
{
|
||||
if ($value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!preg_match('/^(?:[01]\d|2[0-3]):[0-5]\d$/D', $value)) {
|
||||
throw new InvalidArgumentException($label . ' ist ungültig. Bitte hh:mm verwenden.');
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert einen vollständigen Formular-POST und löst die CKalender-Presets auf.
|
||||
*/
|
||||
function kalliValidateEntry(array $source, array $catalog): array
|
||||
{
|
||||
$values = kalliFormValues($source);
|
||||
$group = $values['group'];
|
||||
|
||||
if ($group === '') {
|
||||
throw new InvalidArgumentException('Bitte wählen Sie eine Terminart aus.');
|
||||
}
|
||||
|
||||
$preset = $catalog['types_by_name'][$group] ?? null;
|
||||
if ($preset !== null) {
|
||||
$title = $preset['name'];
|
||||
} elseif (in_array($group, [KALLI_CUSTOM_PUBLIC, KALLI_CUSTOM_INTERNAL], true)) {
|
||||
if (!$catalog['allow_custom']) {
|
||||
throw new InvalidArgumentException('Für benutzerdefinierte Termine fehlt die Berechtigung.');
|
||||
}
|
||||
|
||||
$title = $values['userinput'];
|
||||
if ($title === '') {
|
||||
throw new InvalidArgumentException('Bitte geben Sie einen Titel für den benutzerdefinierten Termin ein.');
|
||||
}
|
||||
|
||||
$isPublic = $group === KALLI_CUSTOM_PUBLIC;
|
||||
$preset = [
|
||||
'planungsstatus' => 'fix',
|
||||
'anzeigeziel' => $isPublic ? 'öffentlich' : 'intern',
|
||||
'gruppenzuordnung' => 'AN ALLE',
|
||||
'anzeigeziel_ort' => $isPublic ? 'öffentlich' : 'intern',
|
||||
'anzeigeziel_notiz' => 'intern',
|
||||
'anzeigeziel_verantwortlicher' => 'intern',
|
||||
'default_text' => '',
|
||||
];
|
||||
} else {
|
||||
throw new InvalidArgumentException('Die gewählte Terminart ist ungültig oder nicht freigegeben.');
|
||||
}
|
||||
|
||||
$dateStart = kalliParseDate($values['date_s'], 'Das Startdatum', true);
|
||||
$dateEnd = kalliParseDate($values['date_e'], 'Das Enddatum');
|
||||
if ($dateEnd !== null && $dateEnd < $dateStart) {
|
||||
throw new InvalidArgumentException('Das Enddatum darf nicht vor dem Startdatum liegen.');
|
||||
}
|
||||
|
||||
$timeStart = kalliParseTime($values['time_s'], 'Die Startzeit');
|
||||
$timeEnd = kalliParseTime($values['time_e'], 'Die Endzeit');
|
||||
if ($timeStart !== null && $timeEnd !== null && ($dateEnd === null || $dateEnd == $dateStart) && $timeEnd < $timeStart) {
|
||||
throw new InvalidArgumentException('Die Endzeit darf am selben Tag nicht vor der Startzeit liegen.');
|
||||
}
|
||||
|
||||
$text = $values['further_text'];
|
||||
if ($text === '' && $preset['default_text'] !== '') {
|
||||
$text = $preset['default_text'];
|
||||
}
|
||||
|
||||
return [
|
||||
'title' => $title,
|
||||
'date_start' => $dateStart->format('d.m.Y'),
|
||||
'date_end' => $dateEnd?->format('d.m.Y') ?? '',
|
||||
'time_start' => $timeStart ?? '',
|
||||
'time_end' => $timeEnd ?? '',
|
||||
'text' => $text,
|
||||
'location' => $values['location'],
|
||||
'responsibility' => $values['responsibility'],
|
||||
'planungsstatus' => $preset['planungsstatus'],
|
||||
'anzeigeziel' => $preset['anzeigeziel'],
|
||||
'gruppenzuordnung' => $preset['gruppenzuordnung'],
|
||||
'anzeigeziel_ort' => $preset['anzeigeziel_ort'],
|
||||
'anzeigeziel_notiz' => $preset['anzeigeziel_notiz'],
|
||||
'anzeigeziel_verantwortlicher' => $preset['anzeigeziel_verantwortlicher'],
|
||||
];
|
||||
}
|
||||
|
||||
function kalliCalendarRow(array $entry, array $user): array
|
||||
{
|
||||
$userName = trim((string)($user['first_name'] ?? '') . ' ' . (string)($user['last_name'] ?? ''));
|
||||
$info = 'Von ' . $userName . ' eingetragen (Eintragungsassistent)';
|
||||
|
||||
return [
|
||||
'', // ID
|
||||
'', // Lang-ID
|
||||
'Einzel', // Termin-Typ
|
||||
$entry['date_start'], // Von-Datum
|
||||
$entry['time_start'], // Von-Zeit
|
||||
$entry['date_end'], // Bis-Datum
|
||||
$entry['time_end'], // Bis-Zeit
|
||||
'', // Zeitzusatz
|
||||
$entry['title'], // Titel
|
||||
$entry['text'], // Text
|
||||
$entry['location'], // Ort
|
||||
$entry['planungsstatus'], // Planungsstatus
|
||||
$entry['anzeigeziel'], // Anzeigeziel
|
||||
$entry['gruppenzuordnung'], // Gruppenzuordnung
|
||||
'einmalig', // Zyklusart
|
||||
'', // Zyklusintervall
|
||||
'', // Zyklusdetail
|
||||
$info, // Notiz
|
||||
$entry['responsibility'], // Verantwortlich
|
||||
'', // Hintergrundfarbe
|
||||
'', // BucherID
|
||||
'', // Benutzerdefinierte Felder
|
||||
'', // Dokument-Titel
|
||||
$entry['anzeigeziel_ort'],
|
||||
$entry['anzeigeziel_notiz'],
|
||||
$entry['anzeigeziel_verantwortlicher'],
|
||||
'', // Erzeugt
|
||||
'', // Aktualisiert
|
||||
];
|
||||
}
|
||||
|
||||
function kalliSafeFilenamePart(string $value, string $fallback): string
|
||||
{
|
||||
$safe = preg_replace('/[^\p{L}\p{N}._ -]+/u', '_', $value);
|
||||
$safe = trim((string)$safe, " ._-\t\n\r\0\x0B");
|
||||
|
||||
return $safe !== '' ? $safe : $fallback;
|
||||
}
|
||||
|
||||
function kalliReserveExportPath(string $directory, array $user): array
|
||||
{
|
||||
$displayName = trim((string)($user['first_name'] ?? '') . ' ' . (string)($user['last_name'] ?? ''));
|
||||
$fallback = 'user-' . (string)($user['user_id'] ?? $_SESSION['user_id'] ?? 'unknown');
|
||||
$safeName = kalliSafeFilenamePart($displayName, $fallback);
|
||||
$base = $directory . '/' . $safeName . '-' . date('Ymd-His');
|
||||
|
||||
for ($suffix = 1; $suffix <= 1000; $suffix++) {
|
||||
$target = $base . ($suffix === 1 ? '' : '-' . $suffix) . '.xlsx';
|
||||
$lockPath = $target . '.lock';
|
||||
$lockHandle = @fopen($lockPath, 'x');
|
||||
if ($lockHandle === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file_exists($target)) {
|
||||
fclose($lockHandle);
|
||||
@unlink($lockPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
return [$target, $lockPath, $lockHandle];
|
||||
}
|
||||
|
||||
throw new RuntimeException('Kein freier Dateiname für den CKalender-Export verfügbar.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Schreibt die XLSX zunächst unter einer Nicht-XLSX-Endung und veröffentlicht sie
|
||||
* anschließend per rename(). So kann ein Ordner-Watcher keine halbfertige Datei lesen.
|
||||
*/
|
||||
function kalliWriteCalendarFile(array $entry, array $user, string $directory): string
|
||||
{
|
||||
$directory = rtrim($directory, '/');
|
||||
if ($directory === '') {
|
||||
throw new RuntimeException('CKalender-XLSX-Verzeichnis ist nicht konfiguriert.');
|
||||
}
|
||||
|
||||
if (!is_dir($directory) && !mkdir($directory, 0770, true) && !is_dir($directory)) {
|
||||
throw new RuntimeException('CKalender-XLSX-Verzeichnis kann nicht angelegt werden: ' . $directory);
|
||||
}
|
||||
if (!is_writable($directory)) {
|
||||
throw new RuntimeException('CKalender-XLSX-Verzeichnis ist nicht beschreibbar: ' . $directory);
|
||||
}
|
||||
|
||||
[$target, $lockPath, $lockHandle] = kalliReserveExportPath($directory, $user);
|
||||
$temporary = $target . '.tmp';
|
||||
$spreadsheet = new Spreadsheet();
|
||||
|
||||
try {
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->fromArray(KALLI_CKAL_HEADERS, null, 'A1');
|
||||
$sheet->fromArray(kalliCalendarRow($entry, $user), null, 'A2');
|
||||
|
||||
(new Xlsx($spreadsheet))->save($temporary);
|
||||
if (!rename($temporary, $target)) {
|
||||
throw new RuntimeException('CKalender-XLSX-Datei konnte nicht veröffentlicht werden.');
|
||||
}
|
||||
} finally {
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
if (is_file($temporary)) {
|
||||
@unlink($temporary);
|
||||
}
|
||||
fclose($lockHandle);
|
||||
@unlink($lockPath);
|
||||
}
|
||||
|
||||
return $target;
|
||||
}
|
||||
Reference in New Issue
Block a user