-Feature: Ab sofort werden alle Configdateien über eine Example.config.php definiert. über das Dashboard kann nun über den neuen Punkt "Config" die eigentliche config.php bearbeitet werden. Änderungen durch Programmupdates werden jetzt automatisch in der example.config.php definiert und beim nächsten Speichern der config-datei über die Website angepasst.

-Feature: Scrollen der Listen können einzeln abeschalten werden
This commit is contained in:
2025-10-10 10:46:33 +02:00
parent 91c0a2d9d9
commit 36944c71bf
6 changed files with 1004 additions and 336 deletions

View File

@@ -1,113 +1,250 @@
<?php <?php
require('config.php'); require('config.php');
require('vendor/autoload.php'); require('vendor/autoload.php');
?> ?>
<!doctype html> <!doctype html>
<html lang="en"> <html lang="de">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags --> <title>Aufgabenmonitor</title>
<title>Aufgabenmonitor</title>
<!-- Bootstrap --> <!-- Bootstrap -->
<link rel="stylesheet" href="vendor/twbs/bootstrap/dist/css/bootstrap.min.css" > <link rel="stylesheet" href="vendor/twbs/bootstrap/dist/css/bootstrap.min.css">
<script src="scripts/jquery-3.5.1.min.js"></script> <link href="css/sticky-footer.css" rel="stylesheet">
<link href="css/sticky-footer.css" rel="stylesheet">
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<script type="text/javascript">
$(document).ready(function(){
refreshAufgabenTable();
});
function refreshAufgabenTable(){ <style>
$('#AufgabenTableHolder').load('sources/getAufgabenTable.php', function(){ body { background-color: #000; }
setTimeout(refreshAufgabenTable, 30000); .tableFixHead { overflow:auto; }
}); .tableFixHead thead th {
} position: sticky;
// function refreshCheckInTable(){ top: 0;
// $('#getCheckInTableHolder').load('sources/getCheckInTable.php', function(){ z-index: 1;
// setTimeout(refreshCheckInTable, 5000); background-color: #212529; /* .table-dark head */
// }); }
// } .table-dark td, .table-dark th { vertical-align: middle; }
</style>
</script>
</head>
<body style="background-color: black;">
<div class="container-fluid"> <script src="scripts/jquery-3.5.1.min.js"></script>
<div class="row"> </head>
<div class="col-lg"> <body>
<h2 class="text-light">Aufgaben</h2>
<table class="table table-dark">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Bearbeiter</th>
<th scope="col">Aufgabe</th>
<th scope="col">Zieldatum</th>
<th scope="col">Priorität</th>
</tr>
</thead>
<tbody id="AufgabenTableHolder">
</tbody>
</table>
</div>
</header>
<div class="container-fluid py-3">
<div class="row">
<div class="col-12">
<h2 class="text-light mb-3">Aufgaben</h2>
<!-- SCROLL CONTAINER -->
<div id="aufgaben-scroll" class="tableFixHead">
<table id="aufgaben-table" class="table table-dark table-striped table-hover mb-0">
<thead>
<tr>
<th scope="col" style="width:6%">#</th>
<th scope="col" style="width:24%">Bearbeiter</th>
<th scope="col" style="width:40%">Aufgabe</th>
<th scope="col" style="width:15%">Zieldatum</th>
<th scope="col" style="width:15%">Priorität</th>
</tr>
</thead>
<tbody id="AufgabenTableHolder">
<!-- wird via AJAX gefüllt -->
</tbody>
</table>
</div>
<!-- /SCROLL CONTAINER -->
</div>
</div>
</div> </div>
<!-- Bootstrap JS -->
<script src="vendor/twbs/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<!-- === Scroll/Reload Script (wie bei CheckIn/CheckOut) === -->
<script type="text/javascript">
/* ===========================
Feature-Toggle aus PHP
=========================== */
const SCROLL_FLAGS = {
aufgaben: <?php echo (defined('EnableScrollingAufgaben') && EnableScrollingAufgaben) ? 'true' : 'false'; ?>
};
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) --> function isEnabled() { return !!SCROLL_FLAGS.aufgaben; }
<script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ" crossorigin="anonymous"></script>
<!-- Include all compiled plugins (below), or include individual files as needed --> /* ===========================
<script src="vendor/twbs/bootstrap/dist/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script> Höhe der Scroll-Container
</body> - Voller Viewport ab Oberkante
=========================== */
function sizeScrollContainers() {
const node = document.querySelector('#aufgaben-scroll');
if (!node) return;
const rect = node.getBoundingClientRect();
const bottomMargin = 16;
const minH = 120;
const available = window.innerHeight - rect.top - bottomMargin;
node.style.maxHeight = Math.max(minH, available) + 'px';
}
/* ===========================
Scroll-/Interaktionsstatus
=========================== */
const scrollState = new WeakMap(); // { userActive:boolean, autoTimer:number|null, loopHeight:number }
function ensureState(el) {
if (!scrollState.get(el)) scrollState.set(el, { userActive: false, autoTimer: null, loopHeight: 0 });
return scrollState.get(el);
}
function attachScrollGuards($scroller) {
const el = $scroller.get(0);
if (!el || el.__guardsBound) return;
if (!isEnabled()) return;
const state = ensureState(el);
const markActive = () => {
state.userActive = true;
clearTimeout(state._quietT);
state._quietT = setTimeout(() => { state.userActive = false; }, 800);
stopAutoScroll($scroller);
};
$scroller.on('wheel touchstart touchmove keydown mousedown mouseenter', markActive);
$scroller.on('mouseleave', () => setTimeout(() => maybeStartAutoScroll($scroller), 600));
el.__guardsBound = true;
}
/* ===========================
Nahtloser Loop (Clone)
=========================== */
function buildSeamlessLoop($table, $scroller) {
$table.find('tbody.__loopClone').remove();
const scEl = $scroller.get(0);
const st = ensureState(scEl);
st.loopHeight = 0;
if (!isEnabled()) return;
const $main = $table.find('tbody').first();
if ($main.length === 0 || $main.children().length === 0) return;
const needScroll = $main.get(0).offsetHeight > scEl.clientHeight + 1;
if (!needScroll) return;
const $clone = $main.clone(false, false).addClass('__loopClone').attr('aria-hidden', 'true');
$table.append($clone);
st.loopHeight = $main.get(0).offsetHeight;
}
/* ===========================
Auto-Scroll
=========================== */
function startAutoScroll($scroller, speedPx = 1, stepMs = 40) {
const el = $scroller.get(0);
if (!isEnabled()) return;
const st = ensureState(el);
if (st.autoTimer) return;
if (st.loopHeight <= 0) return;
const tick = () => {
if (st.userActive) { stopAutoScroll($scroller); return; }
el.scrollTop += speedPx;
if (el.scrollTop >= st.loopHeight) el.scrollTop -= st.loopHeight;
};
st.autoTimer = setInterval(tick, stepMs);
}
function stopAutoScroll($scroller) {
const el = $scroller.get(0);
const st = ensureState(el);
if (st.autoTimer) {
clearInterval(st.autoTimer);
st.autoTimer = null;
}
}
function maybeStartAutoScroll($scroller) {
const el = $scroller.get(0);
if (!isEnabled()) { stopAutoScroll($scroller); return; }
const st = ensureState(el);
if (st.userActive) return;
if (st.loopHeight > 0) startAutoScroll($scroller, 1, 80);
else stopAutoScroll($scroller);
}
/* ===========================
Smart AJAX Reload
=========================== */
function smartLoad($scroller, $table, $target, url, intervalMs) {
const scEl = $scroller.get(0);
const st = ensureState(scEl);
const oldLoop = st.loopHeight > 0 ? st.loopHeight : Math.max(1, scEl.scrollHeight - scEl.clientHeight);
const posInLoop = st.loopHeight > 0 ? (scEl.scrollTop % oldLoop) : scEl.scrollTop;
const posRatio = Math.min(1, posInLoop / oldLoop);
$target.load(url, function () {
buildSeamlessLoop($table, $scroller);
if (st.loopHeight > 0 && isEnabled()) {
const newPos = Math.floor(posRatio * st.loopHeight);
if (Math.abs(scEl.scrollTop - newPos) > 1) {
requestAnimationFrame(() => { scEl.scrollTop = newPos; });
}
stopAutoScroll($scroller);
startAutoScroll($scroller, 1, 80);
} else {
if (scEl.scrollTop !== 0) requestAnimationFrame(() => { scEl.scrollTop = 0; });
stopAutoScroll($scroller);
}
setTimeout(() => smartLoad($scroller, $table, $target, url, intervalMs), intervalMs);
});
}
/* ===========================
Initialisierung
=========================== */
$(document).ready(function () {
sizeScrollContainers();
$(window).on('resize', function () {
sizeScrollContainers();
const $scroller = $('#aufgaben-scroll');
const $table = $('#aufgaben-table');
buildSeamlessLoop($table, $scroller);
maybeStartAutoScroll($scroller);
});
// Guards nur aktivieren, wenn Scrolling erlaubt
if (isEnabled()) attachScrollGuards($('#aufgaben-scroll'));
// Erstladen
(function init() {
const $scroller = $('#aufgaben-scroll');
const $table = $('#aufgaben-table');
const $target = $('#AufgabenTableHolder');
const url = 'sources/getAufgabenTable.php';
const interval = 30000;
$target.load(url, function () {
buildSeamlessLoop($table, $scroller);
maybeStartAutoScroll($scroller);
setTimeout(() => smartLoad($scroller, $table, $target, url, interval), interval);
});
})();
});
</script>
</body>
</html> </html>
<?php <?php
// (Optional hier nicht benötigt, aber gelassen falls du später etwas anzeigst)
function getTimeFromSeconds(string $timestring) { function getTimeFromSeconds(string $timestring) {
$hours = floor($timestring / 3600);
$hours = floor($timestring / 3600); $mins = floor($timestring / 60 % 60);
$mins = floor($timestring / 60 % 60); $timeFormat = sprintf('%02d:%02d', $hours, $mins);
$secs = floor($timestring % 60); return $timeFormat;
$timeFormat = sprintf('%02d:%02d', $hours, $mins);
return $timeFormat;
} }
?> ?>

View File

@@ -23,15 +23,71 @@ $Epi = new Epirent();
<script src="https://kit.fontawesome.com/93d71de8bc.js" crossorigin="anonymous"></script> <script src="https://kit.fontawesome.com/93d71de8bc.js" crossorigin="anonymous"></script>
<script type="text/javascript"> <script type="text/javascript">
// === Höhe der Scroll-Container bis Viewport-Ende anpassen === // === PHP-Flags in JS bringen ===
const SCROLL_FLAGS = {
checkout: <?php echo (defined('EnableScrollingCheckOut') && EnableScrollingCheckOut) ? 'true' : 'false'; ?>,
checkin: <?php echo (defined('EnableScrollingCheckIn') && EnableScrollingCheckIn) ? 'true' : 'false'; ?>,
aufgaben: <?php echo (defined('EnableScrollingAufgaben') && EnableScrollingAufgaben) ? 'true' : 'false'; ?>
};
// Mapping: DOM-ID -> Flag-Key
const SCROLLER_KEYS = new Map([
['checkout-scroll', 'checkout'],
['checkin-scroll', 'checkin'],
['aufgaben-scroll', 'aufgaben']
]);
const TABLE_KEYS = new Map([
['checkout-table', 'checkout'],
['checkin-table', 'checkin'],
['aufgaben-table', 'aufgaben']
]);
function keyForEl(el, map) {
if (!el || !el.id) return null;
return map.get(el.id) || null;
}
function isEnabledByEl(el) {
const k = keyForEl(el, SCROLLER_KEYS);
return !!(k && SCROLL_FLAGS[k]);
}
function isEnabledByKey(key) {
return !!SCROLL_FLAGS[key];
}
// === Höhe der Scroll-Container: nebeneinander = volle Höhe; gestapelt = Drittel ===
function sizeScrollContainers() { function sizeScrollContainers() {
$('.tableFixHead').each(function () { const nodes = [
const rect = this.getBoundingClientRect(); document.querySelector('#checkout-scroll'),
const bottomMargin = 16; document.querySelector('#checkin-scroll'),
const available = window.innerHeight - rect.top - bottomMargin; document.querySelector('#aufgaben-scroll')
this.style.maxHeight = (available > 120 ? available : 120) + 'px'; ].filter(Boolean);
if (nodes.length === 0) return;
// Ermitteln, ob alles in einer Zeile (nebeneinander) oder gestapelt
const tops = nodes.map(n => n.getBoundingClientRect().top);
const ROW_TOL = 8; // px
const uniqueRows = [];
tops.forEach(t => {
const exists = uniqueRows.some(rt => Math.abs(rt - t) <= ROW_TOL);
if (!exists) uniqueRows.push(t);
});
const isOneRow = uniqueRows.length === 1;
const bottomMargin = 16;
const minH = 120;
nodes.forEach(node => {
// Wenn Scrolling für diesen Bereich deaktiviert ist, Höhe trotzdem setzen (wie gefordert)
const rect = node.getBoundingClientRect();
if (isOneRow) {
const available = window.innerHeight - rect.top - bottomMargin;
node.style.maxHeight = Math.max(minH, available) + 'px';
} else {
const third = Math.floor(window.innerHeight / 3) - bottomMargin;
node.style.maxHeight = Math.max(minH, third) + 'px';
}
}); });
} }
@@ -46,6 +102,7 @@ $Epi = new Epirent();
function attachScrollGuards($scroller) { function attachScrollGuards($scroller) {
const el = $scroller.get(0); const el = $scroller.get(0);
if (!el || el.__guardsBound) return; if (!el || el.__guardsBound) return;
if (!isEnabledByEl(el)) return; // nur wenn Feature aktiv
const state = ensureState(el); const state = ensureState(el);
const markActive = () => { const markActive = () => {
@@ -60,45 +117,40 @@ $Epi = new Epirent();
el.__guardsBound = true; el.__guardsBound = true;
} }
// === Loop aufbauen: nahtloses Doppel nur bei Bedarf === // === Loop aufbauen: nahtloses Doppel nur bei Bedarf & nur wenn aktiv ===
// Klont den ersten <tbody> als __loopClone ans Tabellenende, wenn Inhalt > Sichtbereich.
function buildSeamlessLoop($table, $scroller) { function buildSeamlessLoop($table, $scroller) {
$table.find('tbody.__loopClone').remove(); $table.find('tbody.__loopClone').remove();
const $main = $table.find('tbody').first();
const scEl = $scroller.get(0); const scEl = $scroller.get(0);
const enabled = isEnabledByEl(scEl);
const st = ensureState(scEl); const st = ensureState(scEl);
st.loopHeight = 0;
if ($main.length === 0 || $main.children().length === 0) { if (!enabled) return; // deaktiviert -> keine Loops
st.loopHeight = 0;
return;
}
const mainH = $main.get(0).offsetHeight; const $main = $table.find('tbody').first();
const needScroll = mainH > scEl.clientHeight + 1; if ($main.length === 0 || $main.children().length === 0) return;
if (!needScroll) { const needScroll = $main.get(0).offsetHeight > scEl.clientHeight + 1;
st.loopHeight = 0; if (!needScroll) return;
return;
}
const $clone = $main.clone(false, false).addClass('__loopClone').attr('aria-hidden', 'true'); const $clone = $main.clone(false, false).addClass('__loopClone').attr('aria-hidden', 'true');
$table.append($clone); $table.append($clone);
st.loopHeight = mainH; // Höhe des Original-Inhalts als Loop-Länge st.loopHeight = $main.get(0).offsetHeight; // Höhe des Original-Inhalts als Loop-Länge
} }
// === Auto-Scroll (nahtlos) === // === Auto-Scroll (nahtlos) ===
function startAutoScroll($scroller, speedPx = 1, stepMs = 40) { function startAutoScroll($scroller, speedPx = 1, stepMs = 40) {
const el = $scroller.get(0); const el = $scroller.get(0);
const st = ensureState(el); if (!isEnabledByEl(el)) return;
const st = ensureState(el);
if (st.autoTimer) return; // schon aktiv if (st.autoTimer) return; // schon aktiv
if (st.loopHeight <= 0) return; // kein Overflow => nicht scrollen if (st.loopHeight <= 0) return; // kein Overflow => nicht scrollen
const tick = () => { const tick = () => {
if (st.userActive) { stopAutoScroll($scroller); return; } if (st.userActive) { stopAutoScroll($scroller); return; }
el.scrollTop += speedPx; el.scrollTop += speedPx;
// Nahtlos: sobald wir das Ende des Original-Blocks überschreiten, ziehen wir loopHeight ab
if (el.scrollTop >= st.loopHeight) { if (el.scrollTop >= st.loopHeight) {
el.scrollTop -= st.loopHeight; el.scrollTop -= st.loopHeight;
} }
@@ -118,6 +170,7 @@ $Epi = new Epirent();
function maybeStartAutoScroll($scroller) { function maybeStartAutoScroll($scroller) {
const el = $scroller.get(0); const el = $scroller.get(0);
if (!isEnabledByEl(el)) { stopAutoScroll($scroller); return; }
const st = ensureState(el); const st = ensureState(el);
if (st.userActive) return; if (st.userActive) return;
if (st.loopHeight > 0) startAutoScroll($scroller, 1, 80); if (st.loopHeight > 0) startAutoScroll($scroller, 1, 80);
@@ -125,33 +178,33 @@ $Epi = new Epirent();
} }
// === AJAX-Reload: relative Position innerhalb der Loop erhalten === // === AJAX-Reload: relative Position innerhalb der Loop erhalten ===
// Wir merken die Position modulo loopHeight; nach dem Reload setzen wir proportional um.
function smartLoad($scroller, $table, $target, url, intervalMs) { function smartLoad($scroller, $table, $target, url, intervalMs) {
const scEl = $scroller.get(0); const scEl = $scroller.get(0);
const st = ensureState(scEl); const st = ensureState(scEl);
const enabled = isEnabledByEl(scEl);
const oldLoop = st.loopHeight > 0 ? st.loopHeight : Math.max(1, scEl.scrollHeight - scEl.clientHeight); const oldLoop = st.loopHeight > 0 ? st.loopHeight : Math.max(1, scEl.scrollHeight - scEl.clientHeight);
const posInLoop = st.loopHeight > 0 ? (scEl.scrollTop % oldLoop) : scEl.scrollTop; const posInLoop = st.loopHeight > 0 ? (scEl.scrollTop % oldLoop) : scEl.scrollTop;
const posRatio = Math.min(1, posInLoop / oldLoop); const posRatio = Math.min(1, posInLoop / oldLoop);
$target.load(url, function () { $target.load(url, function () {
// Loop neu bewerten/aufbauen if (enabled) {
buildSeamlessLoop($table, $scroller); buildSeamlessLoop($table, $scroller);
if (st.loopHeight > 0) {
if (st.loopHeight > 0) { const newPos = Math.floor(posRatio * st.loopHeight);
// Neue relative Position setzen (proportional) if (Math.abs(scEl.scrollTop - newPos) > 1) {
const newPos = Math.floor(posRatio * st.loopHeight); requestAnimationFrame(() => { scEl.scrollTop = newPos; });
if (Math.abs(scEl.scrollTop - newPos) > 1) { }
// rAF für flüssiges Setzen außerhalb des load()-Layouts stopAutoScroll($scroller);
requestAnimationFrame(() => { scEl.scrollTop = newPos; }); startAutoScroll($scroller, 1, 80);
} else {
if (scEl.scrollTop !== 0) requestAnimationFrame(() => { scEl.scrollTop = 0; });
stopAutoScroll($scroller);
} }
// Auto-Scroll (wieder) starten
stopAutoScroll($scroller);
startAutoScroll($scroller, 1, 80);
} else { } else {
// kein Overflow -> ganz oben und Auto-Scroll aus $table.find('tbody.__loopClone').remove();
if (scEl.scrollTop !== 0) requestAnimationFrame(() => { scEl.scrollTop = 0; });
stopAutoScroll($scroller); stopAutoScroll($scroller);
if (scEl.scrollTop !== 0) requestAnimationFrame(() => { scEl.scrollTop = 0; });
} }
setTimeout(() => smartLoad($scroller, $table, $target, url, intervalMs), intervalMs); setTimeout(() => smartLoad($scroller, $table, $target, url, intervalMs), intervalMs);
@@ -161,9 +214,9 @@ $Epi = new Epirent();
// === Initialisierung === // === Initialisierung ===
$(document).ready(function () { $(document).ready(function () {
sizeScrollContainers(); sizeScrollContainers();
$(window).on('resize', function () { $(window).on('resize', function () {
sizeScrollContainers(); sizeScrollContainers();
// Nach Layoutwechsel neu entscheiden
['#checkout', '#checkin', '#aufgaben'].forEach(prefix => { ['#checkout', '#checkin', '#aufgaben'].forEach(prefix => {
const $scroller = $(`${prefix}-scroll`); const $scroller = $(`${prefix}-scroll`);
const $table = $(`${prefix}-table`); const $table = $(`${prefix}-table`);
@@ -172,32 +225,39 @@ $Epi = new Epirent();
}); });
}); });
// Guards pro Scroller // Guards nur für aktivierte Scroller
attachScrollGuards($('#checkout-scroll')); [['#checkout-scroll','checkout'], ['#checkin-scroll','checkin'], ['#aufgaben-scroll','aufgaben']].forEach(([sel,key]) => {
attachScrollGuards($('#checkin-scroll')); if (isEnabledByKey(key)) attachScrollGuards($(sel));
attachScrollGuards($('#aufgaben-scroll')); });
// Erstladen je Tabelle: laden -> Loop bauen -> ggf. Auto-Scroll -> zyklisch refreshen // Erstladen je Tabelle
function initOne(scrollerSel, tableSel, tbodySel, url, ms) { function initOne(scrollerSel, tableSel, tbodySel, url, ms, key) {
const $scroller = $(scrollerSel); const $scroller = $(scrollerSel);
const $table = $(tableSel); const $table = $(tableSel);
const $target = $(tbodySel); const $target = $(tbodySel);
const enabled = isEnabledByKey(key);
$target.load(url, function () { $target.load(url, function () {
buildSeamlessLoop($table, $scroller); if (enabled) {
maybeStartAutoScroll($scroller); buildSeamlessLoop($table, $scroller);
maybeStartAutoScroll($scroller);
} else {
$table.find('tbody.__loopClone').remove();
stopAutoScroll($scroller);
}
setTimeout(() => smartLoad($scroller, $table, $target, url, ms), ms); setTimeout(() => smartLoad($scroller, $table, $target, url, ms), ms);
}); });
} }
initOne('#checkout-scroll', '#checkout-table', '#getCheckOutTableHolder', 'sources/getCheckOutTable.php', 5000); initOne('#checkout-scroll', '#checkout-table', '#getCheckOutTableHolder', 'sources/getCheckOutTable.php', 5000, 'checkout');
initOne('#checkin-scroll', '#checkin-table', '#getCheckInTableHolder', 'sources/getCheckInTable.php', 5000); initOne('#checkin-scroll', '#checkin-table', '#getCheckInTableHolder', 'sources/getCheckInTable.php', 5000, 'checkin');
initOne('#aufgaben-scroll', '#aufgaben-table', '#AufgabenTableHolder', 'sources/getAufgabenTable.php', 30000); initOne('#aufgaben-scroll', '#aufgaben-table', '#AufgabenTableHolder', 'sources/getAufgabenTable.php', 30000, 'aufgaben');
}); });
</script> </script>
<style> <style>
/* Scroll-Wrapper je Tabelle */ /* Scroll-Wrapper je Tabelle */
.tableFixHead { .tableFixHead {

View File

@@ -21,175 +21,228 @@ $Epi = new Epirent();
<link href="css/sticky-footer.css" rel="stylesheet"> <link href="css/sticky-footer.css" rel="stylesheet">
<script src="https://kit.fontawesome.com/93d71de8bc.js" crossorigin="anonymous"></script> <script src="https://kit.fontawesome.com/93d71de8bc.js" crossorigin="anonymous"></script>
<script type="text/javascript"> <script type="text/javascript">
// === Höhe der Scroll-Container bis Viewport-Ende anpassen === /* ===========================
function sizeScrollContainers() { Feature-Toggles aus PHP
$('.tableFixHead').each(function () { =========================== */
const rect = this.getBoundingClientRect(); const SCROLL_FLAGS = {
const bottomMargin = 16; checkout: <?php echo (defined('EnableScrollingCheckOut') && EnableScrollingCheckOut) ? 'true' : 'false'; ?>,
const available = window.innerHeight - rect.top - bottomMargin; checkin: <?php echo (defined('EnableScrollingCheckIn') && EnableScrollingCheckIn) ? 'true' : 'false'; ?>
this.style.maxHeight = (available > 120 ? available : 120) + 'px'; };
});
const SCROLLER_KEYS = new Map([
['checkout-scroll', 'checkout'],
['checkin-scroll', 'checkin']
]);
function keyForEl(el) { return el && el.id ? SCROLLER_KEYS.get(el.id) : null; }
function isEnabledByEl(el) { const k = keyForEl(el); return !!(k && SCROLL_FLAGS[k]); }
function isEnabledByKey(key) { return !!SCROLL_FLAGS[key]; }
/* ===========================
Höhe der Scroll-Container
- 2 nebeneinander: volle Höhe bis Viewport-Ende
- gestapelt: je 1/2 Fensterhöhe
=========================== */
function sizeScrollContainers() {
const nodes = [
document.querySelector('#checkout-scroll'),
document.querySelector('#checkin-scroll')
].filter(Boolean);
if (!nodes.length) return;
const tops = nodes.map(n => n.getBoundingClientRect().top);
const ROW_TOL = 8;
const firstTop = tops[0];
const isOneRow = tops.every(t => Math.abs(t - firstTop) <= ROW_TOL);
const bottomMargin = 16;
const minH = 120;
nodes.forEach(node => {
const rect = node.getBoundingClientRect();
if (isOneRow) {
const available = window.innerHeight - rect.top - bottomMargin;
node.style.maxHeight = Math.max(minH, available) + 'px';
} else {
const half = Math.floor(window.innerHeight / 2) - bottomMargin;
node.style.maxHeight = Math.max(minH, half) + 'px';
} }
});
}
// === Scroll-/Interaktionsstatus je Scroller === /* ===========================
const scrollState = new WeakMap(); // { userActive:boolean, autoTimer:number|null, loopHeight:number } Scroll-/Interaktionsstatus
=========================== */
const scrollState = new WeakMap(); // { userActive:boolean, autoTimer:number|null, loopHeight:number }
function ensureState(el) { function ensureState(el) {
if (!scrollState.get(el)) scrollState.set(el, { userActive: false, autoTimer: null, loopHeight: 0 }); if (!scrollState.get(el)) scrollState.set(el, { userActive: false, autoTimer: null, loopHeight: 0 });
return scrollState.get(el); return scrollState.get(el);
} }
function attachScrollGuards($scroller) { function attachScrollGuards($scroller) {
const el = $scroller.get(0); const el = $scroller.get(0);
if (!el || el.__guardsBound) return; if (!el || el.__guardsBound) return;
const state = ensureState(el); if (!isEnabledByEl(el)) return;
const state = ensureState(el);
const markActive = () => { const markActive = () => {
state.userActive = true; state.userActive = true;
clearTimeout(state._quietT); clearTimeout(state._quietT);
state._quietT = setTimeout(() => { state.userActive = false; }, 800); state._quietT = setTimeout(() => { state.userActive = false; }, 800);
stopAutoScroll($scroller);
};
$scroller.on('wheel touchstart touchmove keydown mousedown mouseenter', markActive);
$scroller.on('mouseleave', () => setTimeout(() => maybeStartAutoScroll($scroller), 600));
el.__guardsBound = true;
}
/* ===========================
Nahtloser Loop (Clone)
=========================== */
function buildSeamlessLoop($table, $scroller) {
$table.find('tbody.__loopClone').remove();
const scEl = $scroller.get(0);
const enabled = isEnabledByEl(scEl);
const st = ensureState(scEl);
st.loopHeight = 0;
if (!enabled) return;
const $main = $table.find('tbody').first();
if ($main.length === 0 || $main.children().length === 0) return;
const needScroll = $main.get(0).offsetHeight > scEl.clientHeight + 1;
if (!needScroll) return;
const $clone = $main.clone(false, false).addClass('__loopClone').attr('aria-hidden', 'true');
$table.append($clone);
st.loopHeight = $main.get(0).offsetHeight;
}
/* ===========================
Auto-Scroll
=========================== */
function startAutoScroll($scroller, speedPx = 1, stepMs = 40) {
const el = $scroller.get(0);
if (!isEnabledByEl(el)) return;
const st = ensureState(el);
if (st.autoTimer) return;
if (st.loopHeight <= 0) return;
const tick = () => {
if (st.userActive) { stopAutoScroll($scroller); return; }
el.scrollTop += speedPx;
if (el.scrollTop >= st.loopHeight) el.scrollTop -= st.loopHeight;
};
st.autoTimer = setInterval(tick, stepMs);
}
function stopAutoScroll($scroller) {
const el = $scroller.get(0);
const st = ensureState(el);
if (st.autoTimer) {
clearInterval(st.autoTimer);
st.autoTimer = null;
}
}
function maybeStartAutoScroll($scroller) {
const el = $scroller.get(0);
if (!isEnabledByEl(el)) { stopAutoScroll($scroller); return; }
const st = ensureState(el);
if (st.userActive) return;
if (st.loopHeight > 0) startAutoScroll($scroller, 1, 40);
else stopAutoScroll($scroller);
}
/* ===========================
Smart AJAX Reload
=========================== */
function smartLoad($scroller, $table, $target, url, intervalMs) {
const scEl = $scroller.get(0);
const st = ensureState(scEl);
const enabled = isEnabledByEl(scEl);
const oldLoop = st.loopHeight > 0 ? st.loopHeight : Math.max(1, scEl.scrollHeight - scEl.clientHeight);
const posInLoop = st.loopHeight > 0 ? (scEl.scrollTop % oldLoop) : scEl.scrollTop;
const posRatio = Math.min(1, posInLoop / oldLoop);
$target.load(url, function () {
if (enabled) {
buildSeamlessLoop($table, $scroller);
if (st.loopHeight > 0) {
const newPos = Math.floor(posRatio * st.loopHeight);
if (Math.abs(scEl.scrollTop - newPos) > 1) {
requestAnimationFrame(() => { scEl.scrollTop = newPos; });
}
stopAutoScroll($scroller); stopAutoScroll($scroller);
}; startAutoScroll($scroller, 1, 40);
} else {
$scroller.on('wheel touchstart touchmove keydown mousedown mouseenter', markActive); if (scEl.scrollTop !== 0) requestAnimationFrame(() => { scEl.scrollTop = 0; });
$scroller.on('mouseleave', () => setTimeout(() => maybeStartAutoScroll($scroller), 600)); stopAutoScroll($scroller);
el.__guardsBound = true; }
} } else {
// === Loop aufbauen: nahtloses Doppel nur bei Bedarf ===
// Klont den ersten <tbody> als __loopClone ans Tabellenende, wenn Inhalt > Sichtbereich.
function buildSeamlessLoop($table, $scroller) {
$table.find('tbody.__loopClone').remove(); $table.find('tbody.__loopClone').remove();
stopAutoScroll($scroller);
const $main = $table.find('tbody').first(); if (scEl.scrollTop !== 0) requestAnimationFrame(() => { scEl.scrollTop = 0; });
const scEl = $scroller.get(0);
const st = ensureState(scEl);
if ($main.length === 0 || $main.children().length === 0) {
st.loopHeight = 0;
return;
}
const mainH = $main.get(0).offsetHeight;
const needScroll = mainH > scEl.clientHeight + 1;
if (!needScroll) {
st.loopHeight = 0;
return;
}
const $clone = $main.clone(false, false).addClass('__loopClone').attr('aria-hidden', 'true');
$table.append($clone);
st.loopHeight = mainH; // Höhe des Original-Inhalts als Loop-Länge
} }
// === Auto-Scroll (nahtlos) === setTimeout(() => smartLoad($scroller, $table, $target, url, intervalMs), intervalMs);
function startAutoScroll($scroller, speedPx = 1, stepMs = 40) { });
const el = $scroller.get(0); }
const st = ensureState(el);
if (st.autoTimer) return; // schon aktiv /* ===========================
if (st.loopHeight <= 0) return; // kein Overflow => nicht scrollen Initialisierung
=========================== */
$(document).ready(function () {
sizeScrollContainers();
const tick = () => { $(window).on('resize', function () {
if (st.userActive) { stopAutoScroll($scroller); return; } sizeScrollContainers();
el.scrollTop += speedPx; ['#checkout', '#checkin'].forEach(prefix => {
// Nahtlos: sobald wir das Ende des Original-Blocks überschreiten, ziehen wir loopHeight ab const $scroller = $(`${prefix}-scroll`);
if (el.scrollTop >= st.loopHeight) { const $table = $(`${prefix}-table`);
el.scrollTop -= st.loopHeight; buildSeamlessLoop($table, $scroller);
} maybeStartAutoScroll($scroller);
};
st.autoTimer = setInterval(tick, stepMs);
}
function stopAutoScroll($scroller) {
const el = $scroller.get(0);
const st = ensureState(el);
if (st.autoTimer) {
clearInterval(st.autoTimer);
st.autoTimer = null;
}
}
function maybeStartAutoScroll($scroller) {
const el = $scroller.get(0);
const st = ensureState(el);
if (st.userActive) return;
if (st.loopHeight > 0) startAutoScroll($scroller, 1, 40);
else stopAutoScroll($scroller);
}
// === AJAX-Reload: relative Position innerhalb der Loop erhalten ===
// Wir merken die Position modulo loopHeight; nach dem Reload setzen wir proportional um.
function smartLoad($scroller, $table, $target, url, intervalMs) {
const scEl = $scroller.get(0);
const st = ensureState(scEl);
const oldLoop = st.loopHeight > 0 ? st.loopHeight : Math.max(1, scEl.scrollHeight - scEl.clientHeight);
const posInLoop = st.loopHeight > 0 ? (scEl.scrollTop % oldLoop) : scEl.scrollTop;
const posRatio = Math.min(1, posInLoop / oldLoop);
$target.load(url, function () {
// Loop neu bewerten/aufbauen
buildSeamlessLoop($table, $scroller);
if (st.loopHeight > 0) {
// Neue relative Position setzen (proportional)
const newPos = Math.floor(posRatio * st.loopHeight);
if (Math.abs(scEl.scrollTop - newPos) > 1) {
// rAF für flüssiges Setzen außerhalb des load()-Layouts
requestAnimationFrame(() => { scEl.scrollTop = newPos; });
}
// Auto-Scroll (wieder) starten
stopAutoScroll($scroller);
startAutoScroll($scroller, 1, 40);
} else {
// kein Overflow -> ganz oben und Auto-Scroll aus
if (scEl.scrollTop !== 0) requestAnimationFrame(() => { scEl.scrollTop = 0; });
stopAutoScroll($scroller);
}
setTimeout(() => smartLoad($scroller, $table, $target, url, intervalMs), intervalMs);
});
}
// === Initialisierung ===
$(document).ready(function () {
sizeScrollContainers();
$(window).on('resize', function () {
sizeScrollContainers();
// Nach Layoutwechsel neu entscheiden (nur 2 Tabellen)
['#checkout', '#checkin'].forEach(prefix => {
const $scroller = $(`${prefix}-scroll`);
const $table = $(`${prefix}-table`);
buildSeamlessLoop($table, $scroller);
maybeStartAutoScroll($scroller);
});
});
// Guards pro Scroller (nur 2)
attachScrollGuards($('#checkout-scroll'));
attachScrollGuards($('#checkin-scroll'));
// Erstladen je Tabelle: laden -> Loop bauen -> ggf. Auto-Scroll -> zyklisch refreshen
function initOne(scrollerSel, tableSel, tbodySel, url, ms) {
const $scroller = $(scrollerSel);
const $table = $(tableSel);
const $target = $(tbodySel);
$target.load(url, function () {
buildSeamlessLoop($table, $scroller);
maybeStartAutoScroll($scroller);
setTimeout(() => smartLoad($scroller, $table, $target, url, ms), ms);
});
}
initOne('#checkout-scroll', '#checkout-table', '#getCheckOutTableHolder', 'sources/getCheckOutTable.php', 5000);
initOne('#checkin-scroll', '#checkin-table', '#getCheckInTableHolder', 'sources/getCheckInTable.php', 5000);
}); });
</script> });
// Guards nur für aktivierte Scroller
[['#checkout-scroll','checkout'], ['#checkin-scroll','checkin']].forEach(([sel,key]) => {
if (isEnabledByKey(key)) attachScrollGuards($(sel));
});
// Erstladen je Tabelle
function initOne(scrollerSel, tableSel, tbodySel, url, ms, key) {
const $scroller = $(scrollerSel);
const $table = $(tableSel);
const $target = $(tbodySel);
const enabled = isEnabledByKey(key);
$target.load(url, function () {
if (enabled) {
buildSeamlessLoop($table, $scroller);
maybeStartAutoScroll($scroller);
} else {
$table.find('tbody.__loopClone').remove();
stopAutoScroll($scroller);
}
setTimeout(() => smartLoad($scroller, $table, $target, url, ms), ms);
});
}
initOne('#checkout-scroll', '#checkout-table', '#getCheckOutTableHolder', 'sources/getCheckOutTable.php', 5000, 'checkout');
initOne('#checkin-scroll', '#checkin-table', '#getCheckInTableHolder', 'sources/getCheckInTable.php', 5000, 'checkin');
});
</script>
<style> <style>
/* Scroll-Wrapper je Tabelle */ /* Scroll-Wrapper je Tabelle */

413
dist/editconfig.php vendored Normal file
View File

@@ -0,0 +1,413 @@
<?php
declare(strict_types=1);
/**
* Config-Editor für EPI Webview
* - Liest ../config.php (falls vorhanden) und ../example.config.php
* - Zeigt editierbare Felder für alle Keys aus example.config.php
* - Zeigt veraltete (nur in config.php existierende) Keys rot & disabled
* - Speichert Werte zurück nach ../config.php
*/
error_reporting(E_ALL);
// ---- Dateipfade anpassen falls nötig ----
$CONFIG_FILE = realpath(__DIR__ . '/../config.php') ?: (__DIR__ . '/../config.php');
$EXAMPLE_FILE = realpath(__DIR__ . '/../example.config.php') ?: (__DIR__ . '/../example.config.php');
// ---- Hilfsfunktionen: Parsing ----
function parse_define_file(string $file): array {
if (!is_file($file)) return ['defines' => [], 'order' => [], 'ui' => []];
$lines = file($file, FILE_IGNORE_NEW_LINES);
$defines = [];
$order = [];
$ui = [];
// BOM entfernen, falls vorhanden
if (isset($lines[0])) {
$lines[0] = preg_replace('/^\xEF\xBB\xBF/', '', $lines[0]);
}
// Regex
$reDefine = '/^\s*define\(\s*([\'"])([^\'"]+)\1\s*,\s*(.+?)\s*\)\s*;\s*(?:(?:\/\/|#)\s*(.*))?$/u';
$reSection = '/^\s*\/\/\s*@section\s*:\s*(.+)\s*$/iu';
$reHR = '/^\s*\/\/\s*@hr\s*$/iu';
$reNote = '/^\s*\/\/\s*@note\s*:\s*(.+)\s*$/iu';
foreach ($lines as $ln) {
// Meta-Kommentare zuerst prüfen (UI-Struktur)
if (preg_match($reSection, $ln, $m)) {
$ui[] = ['kind' => 'section', 'title' => trim($m[1])];
continue;
}
if (preg_match($reHR, $ln)) {
$ui[] = ['kind' => 'hr'];
continue;
}
if (preg_match($reNote, $ln, $m)) {
$ui[] = ['kind' => 'note', 'text' => trim($m[1])];
continue;
}
// define(…) Zeilen
if (!preg_match($reDefine, $ln, $m)) {
continue;
}
$name = $m[2];
$rawValueExpr = trim($m[3]);
$inlineC = isset($m[4]) ? trim($m[4]) : '';
// Typ bestimmen
$type = 'string';
$valForForm = '';
$raw = rtrim($rawValueExpr, ',');
if (preg_match('/^(true|false)$/i', $raw)) {
$type = 'bool';
$valForForm = (strtolower($raw) === 'true');
} elseif (preg_match('/^[+-]?\d+$/', $raw)) {
$type = 'int';
$valForForm = (int)$raw;
} elseif (preg_match('/^([\'"])(.*)\1$/s', $raw, $mm)) {
$type = 'string';
$valForForm = stripcslashes($mm[2]);
} else {
$type = 'string';
$valForForm = $raw;
}
$defines[$name] = [
'name' => $name,
'type' => $type,
'value' => $valForForm,
'raw' => $rawValueExpr,
'comment' => $inlineC,
'line' => $ln,
];
$order[] = $name;
// Wichtig: define auch in die UI-Reihenfolge aufnehmen
$ui[] = ['kind' => 'define', 'name' => $name];
}
return ['defines' => $defines, 'order' => $order, 'ui' => $ui];
}
function php_export_define_value(string $type, $value): string {
// Baut den rechten Teil von define('X', <HIER>);
if ($type === 'bool') {
return $value ? 'true' : 'false';
}
if ($type === 'int') {
return (string)intval($value);
}
// String:
// einfache Quotes nehmen und escapen
$escaped = str_replace("'", "\\'", (string)$value);
return "'" . $escaped . "'";
}
function h(?string $s): string { return htmlspecialchars((string)$s ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
// ---- Dateien parsen ----
$example = parse_define_file($EXAMPLE_FILE);
$current = parse_define_file($CONFIG_FILE);
// Keys klassifizieren
$exampleKeys = array_keys($example['defines']);
$currentKeys = array_keys($current['defines']);
$deprecatedKeys = array_values(array_diff($currentKeys, $exampleKeys)); // nur in config.php
$newKeys = array_values(array_diff($exampleKeys, $currentKeys)); // nur in example (neu)
$sharedKeys = array_values(array_intersect($exampleKeys, $currentKeys));
// Beim Rendern: Reihenfolge aus example beibehalten
$renderKeys = $example['order'];
// ---- POST: Speichern ----
$message = '';
$err = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['__save_config']) && isset($_POST['cfg']) && is_array($_POST['cfg'])) {
$incoming = $_POST['cfg']; // ['NAME' => valueString/checkbox etc.]
// Build neuer config.php Inhalt:
$out = [];
$out[] = "<?php";
$out[] = "// Diese Datei wurde durch den Epi Webview Config-Editor erzeugt/aktualisiert.";
$out[] = "// Bearbeite Werte bevorzugt über die Weboberfläche.";
$out[] = "";
// --- Inhalt in der Reihenfolge & Struktur der example.config.php ---
foreach ($example['ui'] as $item) {
if ($item['kind'] === 'section') {
$out[] = "// @section: " . $item['title'];
continue;
}
if ($item['kind'] === 'hr') {
$out[] = "// @hr";
continue;
}
if ($item['kind'] === 'note') {
$out[] = "// @note: " . $item['text'];
continue;
}
if ($item['kind'] !== 'define') {
continue;
}
$label = $item['name'];
$ex = $example['defines'][$label] ?? null;
if (!$ex) continue;
$type = $ex['type']; // Typ an example orientieren
$tooltip = $ex['comment'] ?? '';
// Wert aus POST übernehmen (Checkbox-Fix: existiert das Feld?)
if ($type === 'bool') {
$bool = array_key_exists($label, $incoming);
$valueForDefine = php_export_define_value('bool', $bool);
} elseif ($type === 'int') {
$raw = $incoming[$label] ?? '';
$valueForDefine = php_export_define_value('int', $raw);
} else {
$raw = $incoming[$label] ?? '';
$valueForDefine = php_export_define_value('string', $raw);
}
$line = "define('{$label}', {$valueForDefine});";
if (!empty($tooltip)) {
$line .= " // " . $tooltip; // Kommentare als Tooltip auch in config.php mitführen
}
$out[] = $line;
}
// Deprecated Block (nur in aktueller config.php vorhanden, nicht in example)
if (!empty($deprecatedKeys)) {
$out[] = "";
$out[] = "// --- Veraltete/entfernte Parameter (nur Referenz, werden nicht mehr genutzt) ---";
foreach ($deprecatedKeys as $key) {
$c = $current['defines'][$key];
$valueForDefine = php_export_define_value($c['type'], $c['value']);
$line = "// define('{$c['name']}', {$valueForDefine});";
if (!empty($c['comment'])) {
$line .= " // " . $c['comment'];
}
$out[] = $line;
}
}
$out[] = "?>";
$content = implode("\n", $out) . "\n";
try {
if (false === file_put_contents($CONFIG_FILE, $content)) {
$err = "Konnte config.php nicht schreiben: " . h($CONFIG_FILE);
} else {
$message = "Konfiguration gespeichert.";
// Nach dem Speichern neu parsen, damit UI aktuell ist
$current = parse_define_file($CONFIG_FILE);
$currentKeys = array_keys($current['defines']);
$deprecatedKeys = array_values(array_diff($currentKeys, $exampleKeys));
$sharedKeys = array_values(array_intersect($exampleKeys, $currentKeys));
}
} catch (Throwable $e) {
$err = "Fehler beim Schreiben: " . h($e->getMessage());
}
}
// ---- Werte für Formular vorbereiten ----
// Für jeden example-Key: Formularwert = aus aktueller config (falls vorhanden), sonst example default
function valueForForm(array $example, array $current, string $key) {
$ex = $example['defines'][$key] ?? null;
$cu = $current['defines'][$key] ?? null;
$type = $ex ? $ex['type'] : ($cu ? $cu['type'] : 'string');
$comment = $ex && $ex['comment'] !== '' ? $ex['comment'] : ($cu['comment'] ?? '');
if ($cu) {
$val = $cu['value'];
} else {
$val = $ex['value'];
}
return [$type, $val, $comment];
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="description" content="Konfiguration bearbeiten" />
<meta name="author" content="" />
<title>Konfiguration Epi Webview</title>
<link href="css/styles.css" rel="stylesheet" />
<link href="https://cdn.datatables.net/1.10.20/css/dataTables.bootstrap4.min.css" rel="stylesheet" crossorigin="anonymous" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/js/all.min.js" crossorigin="anonymous"></script>
<script src="js/jquery-3.5.1.min.js"></script>
<style>
.deprecated { color: #c62828; }
.cfg-help { margin-left: .35rem; cursor: help; }
.form-group { margin-bottom: .75rem; }
.small-muted { font-size:.85rem; color:#6c757d; }
.checkbox-inline { display:flex; align-items:center; gap:.5rem; }
.cfg-label { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
</style>
<script>
$(function(){
// Tooltips (Bootstrap 4)
$('[data-toggle="tooltip"]').tooltip();
// Side UI nachladen wie in deinem Template
loadSidenav();
loadFooter();
});
function loadSidenav(){
$('#layoutSidenav_nav').load('../sources/getSidenav.php');
}
function loadFooter(){
$('#footerholder').load('../sources/getFooter.php');
}
</script>
</head>
<body class="sb-nav-fixed">
<nav class="sb-topnav navbar navbar-expand navbar-dark bg-dark">
<a class="navbar-brand" href="index.php">Epi Webview</a>
<button class="btn btn-link btn-sm order-1 order-lg-0" id="sidebarToggle" type="button"><i class="fas fa-bars"></i></button>
<ul class="navbar-nav ml-auto ml-md-0"></ul>
</nav>
<div id="layoutSidenav">
<div id="layoutSidenav_nav"></div>
<div id="layoutSidenav_content">
<main>
<div class="container-fluid">
<h1 class="mt-4">Konfiguration</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item active">Dashboard / Konfiguration</li>
</ol>
<?php if ($message): ?>
<div class="alert alert-success"><?= h($message) ?></div>
<?php endif; ?>
<?php if ($err): ?>
<div class="alert alert-danger"><?= h($err) ?></div>
<?php endif; ?>
<div class="card mb-4">
<div class="card-header">
<i class="fas fa-sliders-h mr-1"></i>
Einstellungen
<span class="small-muted">( <code><?=h(basename($CONFIG_FILE))?></code> Source: <code><?=h(basename($EXAMPLE_FILE))?></code>)</span>
</div>
<div class="card-body">
<form method="post">
<input type="hidden" name="__save_config" value="1" />
<?php foreach ($example['ui'] as $item): ?>
<?php
if ($item['kind'] === 'section') {
echo '<h5 class="mt-3">'.h($item['title']).'</h5>';
continue;
}
if ($item['kind'] === 'hr') {
echo '<hr />';
continue;
}
if ($item['kind'] === 'note') {
echo '<p class="small-muted">'.h($item['text']).'</p>';
continue;
}
if ($item['kind'] !== 'define') {
continue;
}
$key = $item['name'];
// Nur Keys rendern, die in example vorkommen
if (!isset($example['defines'][$key])) continue;
[$type, $val, $tooltip] = valueForForm($example, $current, $key);
?>
<div class="form-group">
<label class="cfg-label" for="cfg_<?=h($key)?>">
<?= h($key) ?>
<?php if ($tooltip): ?>
<i class="far fa-question-circle cfg-help"
data-toggle="tooltip" data-placement="right"
title="<?=h($tooltip)?>"></i>
<?php endif; ?>
</label>
<?php if ($type === 'bool'): ?>
<div class="checkbox-inline">
<input type="checkbox"
id="cfg_<?=h($key)?>"
name="cfg[<?=h($key)?>]"
value="1" <?= $val ? 'checked' : '' ?> />
<span><?= $val ? 'Aktiviert' : 'Deaktiviert' ?></span>
</div>
<?php elseif ($type === 'int'): ?>
<input type="number" step="1" class="form-control"
id="cfg_<?=h($key)?>"
name="cfg[<?=h($key)?>]"
value="<?=h((string)$val)?>" />
<?php else: ?>
<input type="text" class="form-control"
id="cfg_<?=h($key)?>"
name="cfg[<?=h($key)?>]"
value="<?=h((string)$val)?>" />
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php if (!empty($deprecatedKeys)): ?>
<hr />
<h5 class="deprecated">Veraltete Parameter (nur in <code><?=h(basename($CONFIG_FILE))?></code>, nicht mehr in <code><?=h(basename($EXAMPLE_FILE))?></code>)</h5>
<?php foreach ($deprecatedKeys as $key): ?>
<?php $c = $current['defines'][$key]; ?>
<div class="form-group">
<label class="cfg-label deprecated" for="dep_<?=h($key)?>">
<?= h($key) ?>
<?php if (!empty($c['comment'])): ?>
<i class="far fa-question-circle cfg-help" data-toggle="tooltip" data-placement="right" title="<?=h($c['comment'])?>"></i>
<?php endif; ?>
</label>
<input type="text" class="form-control is-invalid" id="dep_<?=h($key)?>" value="<?=
h($c['type']==='bool' ? ($c['value']?'true':'false') : (string)$c['value'])
?>" disabled />
<div class="invalid-feedback">Dieser Parameter ist in der aktuellen Version nicht mehr vorgesehen.</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
<div class="mt-4">
<button type="submit" class="btn btn-primary"><i class="fas fa-save mr-1"></i> Speichern</button>
<span class="small-muted ml-2">Speichert nach <code><?=h($CONFIG_FILE)?></code></span>
</div>
</form>
</div>
</div>
</div>
</main>
<div id="footerholder"></div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="js/scripts.js"></script>
<script>
// Sidebar Toggle (falls benötigt)
$('#sidebarToggle').on('click', function(){ $('body').toggleClass('sb-sidenav-toggled'); });
</script>
</body>
</html>

View File

@@ -1,58 +1,59 @@
<?php <?php
//EPIRENT CONNECTION EINSTELLUNGEN// // @section: Epirent Verbindung
define('Epirent_Server', 'xxx'); define('Epirent_Server', 'xxx');
define('Epirent_Connectionprotocol', 'http'); define('Epirent_Connectionprotocol', 'http');
define('Epirent_Port', '8080'); define('Epirent_Port', '8080');
define('Epirent_Token', 'xxx'); define('Epirent_Token', 'xxx');
define('Epirent_Mandant', '0'); //NORMALERWEISE 0 define('Epirent_Mandant', '0'); //NORMALERWEISE 0
// @hr
//CREWBRAIN CONNECTION EINSTELLUNGEN-NUR AUSFÜLLEN WENN GEWÜNSCHT// // @section: Crewbrain Verbindung
// @note: Nur ausfüllen wenn gewünscht
define('CrewBrain_Username', 'xxx'); define('CrewBrain_Username', 'xxx');
define('CrewBrain_password', 'xxx'); define('CrewBrain_password', 'xxx');
define('CrewBrain_Server', 'xxx.crewbrain.com'); define('CrewBrain_Server', 'xxx.crewbrain.com');
define('CrewBrain_Connectionprotocol', 'https'); define('CrewBrain_Connectionprotocol', 'https');
define('CrewBrain_TaskListID', 6); define('CrewBrain_TaskListID', 6);
//CREWBRAIN SPEZIFISCHE EINSTELLUNGEN (AUFGABENLISTE)//
define('Enable_QR_Code_CrewBrainAufgaben', true); define('Enable_QR_Code_CrewBrainAufgaben', true);
// @hr
//EPIRENT SPEZIFISCHE EINSTELLUNGEN (CHECK IN & CHECK OUT) // @section: Epirent-Spezifische Einstellungen
define('Enable_QR_Code_CheckOut', false); //Zeigt statt der Packscheinnummer einen Scanbaren QR Code für den CheckOut an define('Enable_QR_Code_CheckOut', false); //Zeigt statt der Packscheinnummer einen Scanbaren QR Code für den CheckOut an
define('Enable_QR_Code_CheckIn', false); //Zeigt statt der Packscheinnummer einen Scanbaren QR Code für den CheckIn an define('Enable_QR_Code_CheckIn', false); //Zeigt statt der Packscheinnummer einen Scanbaren QR Code für den CheckIn an
define('Vorbereitungs_Zeitvariable', 'Packen'); //Name des zu verwendenden Zeitabschnitts, der Zusätzlich zur DispoZeit beim Check Out Angezeigt werden soll define('Vorbereitungs_Zeitvariable', 'Packen'); //Name des zu verwendenden Zeitabschnitts, der Zusätzlich zur DispoZeit beim Check Out Angezeigt werden soll
define('Nachbereitung_Zeitvariable', 'Rückpacken'); //Name des zu verwendenden Zeitabschnitts, der Zusätzlich zur DispoZeit beim Check In Angezeigt werden soll define('Nachbereitung_Zeitvariable', 'Rückpacken'); //Name des zu verwendenden Zeitabschnitts aus dem Auftrag, der die Nachbereitung angibt
define('Rückpacken_Zeitvariable', 'Rückpacken'); define('Rückpacken_Zeitvariable', 'Rückpacken'); //Name des zu verwendenden Zeitabschnitts aus dem Auftrag, der das Rückpacken angibt.
define('UseDeliveredForCheckOutCompleted', true); //Nutzt statt dem erfolgreichen abschließen eines Packscheins durch ausbuchen das erfolgreiche Liefern (Geliefert != 00.00.00)
/** -------------------- Row-Marking: Konfig Zusände -------------------- define('EnableScrollingCheckOut', true); //Aktiviert das automatische Scrollen der CheckOut Liste
* 1 = $packingjob->date_start (Dispo Start) define('EnableScrollingCheckIn', true); //Aktiviert das automatische Scrollen der CheckIn Liste
* 2 = $VorbereitungsTimeDetail->date_start (Vorbereitung Start) define('EnableScrollingAufgaben', true); //Aktiviert das automatische Scrollen der Aufgabenliste
* 3 = $PackingNoteDetail->date_packing (Packen Zeit) // @note: -------------------- CheckOutRowMarkSource: Konfig Zusände --------------------
* 4 = $PackingNoteDetail->date_delivery (Delivery Zeit) // @note: 1 = $packingjob->date_start (Dispo Start)
*/ // @note: 2 = $VorbereitungsTimeDetail->date_start (Vorbereitung Start)
// @note: 3 = $PackingNoteDetail->date_packing (Packen Zeit)
// @note: 4 = $PackingNoteDetail->date_delivery (Delivery Zeit)
define('CheckOutRowMarkSource', 4); define('CheckOutRowMarkSource', 4);
/** -------------------- Row-Marking: Konfig Zusände -------------------- // @note: -------------------- CheckInRowMarkSource: Konfig Zusände --------------------
* 1 = $packingjob->date_end (Dispo Ende) // @note: 1 = $packingjob->date_end (Dispo Ende)
* 2 = $NachbereitungssTimeDetail->date_start (Nachbereitung Start) // @note: 2 = $NachbereitungssTimeDetail->date_start (Nachbereitung Start)
* 3 = $RePackagingTimeDetail->date_start (Rückpacken Zeit AUS AUFTRAG) // @note: 3 = $RePackagingTimeDetail->date_start (Rückpacken Zeit AUS AUFTRAG)
* 4 = $PackingNoteDetail->date_redelivery (ReDelivery Zeit) // @note: 4 = $PackingNoteDetail->date_redelivery (ReDelivery Zeit)
*/
define('CheckInRowMarkSource', 4); define('CheckInRowMarkSource', 4);
define('CheckIn_UseDispoEndForRowMarking', false); //else: Use Same Variable as "Rueckpacken Zeitvariable" | Konfiguration, welche Zeit für die Zeilenmarkierung beim Check In Verwendet werden soll define('CheckIn_UseDispoEndForRowMarking', false); //else: Use Same Variable as "Rueckpacken Zeitvariable" | Konfiguration, welche Zeit für die Zeilenmarkierung beim Check In Verwendet werden soll
define('ShowCheckoutTimeOnCheckout', true); define('ShowCheckoutTimeOnCheckout', true); //Zeigt die Checkout Zeit im Checkout
define('ShowVorbereitungTimeOnCheckout', true); define('ShowVorbereitungTimeOnCheckout', true); //Zeigt die Vorbereitungs Zeitvariable im Checkout
define('ShowPackagingTimeOnCheckout', true); define('ShowPackagingTimeOnCheckout', true); //Zeigt die Packenzeit im Checkout
define('ShowDeliveryTimeOnCheckout', true); define('ShowDeliveryTimeOnCheckout', true); //Zeigt die Lieferzeit im Checkout
define('ShowTimesOnCheckout', true); define('ShowTimesOnCheckout', true); //Aktiviert das anzeigen der Uhrzeit im Checkout
// @hr
define('ShowCheckInTimeOnCheckin', true); define('ShowCheckInTimeOnCheckin', true); //Zeigt die CheckIn Zeit im CheckIn
define('ShowNachbereitungTimeOnCheckin', true); define('ShowNachbereitungTimeOnCheckin', true); //Zeigt die Nachbereitungs Zeitvariable im CheckIn
define('ShowRePackagingTimeOnCheckin', true); define('ShowRePackagingTimeOnCheckin', true); //Zeigt die Rückpackzeit im Checkin <br><b>Achtung: Zeit nur Im Auftrag festlegbar</b>
define('ShowReDeliveryTimeOnCheckin', true); define('ShowReDeliveryTimeOnCheckin', true); //Zeigt die geplante Rücklieferung im Checkin
define('ShowTimesOnCheckin', true); define('ShowTimesOnCheckin', true); //Aktiviert das anzeigen der Uhrzeit im CheckIn
// @section: Epirent-Spezifische Einstellungen - Shipping
define('ShowShippingIcons', true); //Zeigt Lieferung / Rücklieferungs Icons an define('ShowShippingIcons', true); //Zeigt Lieferung / Rücklieferungs Icons an
define('KurierContainsText', 'Kurier'); //Text, der in der Versandart enthalten sein muss (enthält), damit diese als KURIER erkannt wird. define('KurierContainsText', 'Kurier'); //Text, der in der Versandart enthalten sein muss (enthält), damit diese als KURIER erkannt wird.
define('SpeditionContainsText', 'Spedition'); //Text, der in der Versandart enthalten sein muss (enthält), damit diese als SPEDITION erkannt wird. define('SpeditionContainsText', 'Spedition'); //Text, der in der Versandart enthalten sein muss (enthält), damit diese als SPEDITION erkannt wird.
@@ -66,8 +67,7 @@ define('ShippingOutOrganizedStatus', 'Hinversand OK'); //Packschein Status (Epir
define('ShippingInOrganizedStatus', 'Rückversand OK'); //Packschein Status (EpirentDropdown) der Gematcht werden muss, dass der Rückversand Organisiert / Bestellt wurde define('ShippingInOrganizedStatus', 'Rückversand OK'); //Packschein Status (EpirentDropdown) der Gematcht werden muss, dass der Rückversand Organisiert / Bestellt wurde
define('ShippingOrganizedStatus', 'Versand OK'); //Packschein Status (EpirentDropdown) der Gematcht werden muss, dass der komplette Versand Organisiert / Bestellt wurde define('ShippingOrganizedStatus', 'Versand OK'); //Packschein Status (EpirentDropdown) der Gematcht werden muss, dass der komplette Versand Organisiert / Bestellt wurde
define('UseDeliveredForCheckOutCompleted', true); //Nutzt statt dem erfolgreichen abschließen eines Packscheins durch ausbuchen das erfolgreiche Liefern (Geliefert != 00.00.00) // @section: Epirent-Spezifische Einstellungen - Anzeigesortierung
define('CheckOut_FutureDays', -1); // Konfiguration, wie viele Tage in der Zukunft der CheckOut angezeigt werden soll. '-1' zeigt alle an. Abhängig von der Variablen CheckOut_UseDispoStartForRowMarking define('CheckOut_FutureDays', -1); // Konfiguration, wie viele Tage in der Zukunft der CheckOut angezeigt werden soll. '-1' zeigt alle an. Abhängig von der Variablen CheckOut_UseDispoStartForRowMarking
define('CheckIn_FutureDays', -1); // Konfiguration, wie viele Tage in der Zukunft der CheckIn angezeigt werden soll. '-1' zeigt alle an. Abhängig von der Variablen CheckIn_UseDispoEndForRowMarking define('CheckIn_FutureDays', -1); // Konfiguration, wie viele Tage in der Zukunft der CheckIn angezeigt werden soll. '-1' zeigt alle an. Abhängig von der Variablen CheckIn_UseDispoEndForRowMarking
define('SortCheckOut', 2); // Konfiguration, welcher Datensatz für die Sortierung Verwendet werden soll. Möglichkeiten '1': Packscheinnummer / '2': Dispostart define('SortCheckOut', 2); // Konfiguration, welcher Datensatz für die Sortierung Verwendet werden soll. Möglichkeiten '1': Packscheinnummer / '2': Dispostart

View File

@@ -25,6 +25,11 @@
<div class="sb-nav-link-icon"><i class="fas fa-list"></i></div> <div class="sb-nav-link-icon"><i class="fas fa-list"></i></div>
Pack- & Aufgabenmonitor Pack- & Aufgabenmonitor
</a> </a>
<div class="sb-sidenav-menu-heading">Config</div>
<a class="nav-link" href=editconfig.php>
<div class="sb-nav-link-icon"><i class="fas fa-tachometer-alt"></i></div>
Config
</a>
</div> </div>
</div> </div>
<div class="sb-sidenav-footer"> <div class="sb-sidenav-footer">