5 Commits

12 changed files with 1470 additions and 487 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"> <style>
$(document).ready(function(){ body { background-color: #000; }
refreshAufgabenTable(); .tableFixHead { overflow:auto; }
.tableFixHead thead th {
position: sticky;
top: 0;
z-index: 1;
background-color: #212529; /* .table-dark head */
}
.table-dark td, .table-dark th { vertical-align: middle; }
</style>
}); <script src="scripts/jquery-3.5.1.min.js"></script>
</head>
function refreshAufgabenTable(){ <body>
$('#AufgabenTableHolder').load('sources/getAufgabenTable.php', function(){
setTimeout(refreshAufgabenTable, 30000);
});
}
// function refreshCheckInTable(){
// $('#getCheckInTableHolder').load('sources/getCheckInTable.php', function(){
// setTimeout(refreshCheckInTable, 5000);
// });
// }
</script>
</head>
<body style="background-color: black;">
<div class="container-fluid">
<div class="row">
<div class="col-lg">
<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 = 60000;
$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

@@ -12,7 +12,7 @@ public function requestEpiApi(string $requestString){
$requestUrl = Epirent_Connectionprotocol."://".Epirent_Server.":".Epirent_Port.$requestString; $requestUrl = Epirent_Connectionprotocol."://".Epirent_Server.":".Epirent_Port.$requestString;
$response = $client->request('GET', $requestUrl,[ $response = $client->request('GET', $requestUrl,[
'headers' =>[ 'headers' =>[
'X-EPI-NO-SESSION' => 'True', 'X-EPI-NO-SESSION' => 'true',
'X-EPI-ACC-TOK' => Epirent_Token 'X-EPI-ACC-TOK' => Epirent_Token
] ]
]); ]);

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', 10000, 'checkout');
initOne('#checkin-scroll', '#checkin-table', '#getCheckInTableHolder', 'sources/getCheckInTable.php', 5000); initOne('#checkin-scroll', '#checkin-table', '#getCheckInTableHolder', 'sources/getCheckInTable.php', 10000, 'checkin');
initOne('#aufgaben-scroll', '#aufgaben-table', '#AufgabenTableHolder', 'sources/getAufgabenTable.php', 30000); initOne('#aufgaben-scroll', '#aufgaben-table', '#AufgabenTableHolder', 'sources/getAufgabenTable.php', 60000, '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', 10000, 'checkout');
initOne('#checkin-scroll', '#checkin-table', '#getCheckInTableHolder', 'sources/getCheckInTable.php', 10000, 'checkin');
});
</script>
<style> <style>
/* Scroll-Wrapper je Tabelle */ /* Scroll-Wrapper je Tabelle */
@@ -226,7 +279,7 @@ $Epi = new Epirent();
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<!-- Check-Out --> <!-- Check-Out -->
<div class="col-sm"> <div class="col-xl">
<h2 class="text-light">Check-Out</h2> <h2 class="text-light">Check-Out</h2>
<div class="tableFixHead" id="checkout-scroll"> <div class="tableFixHead" id="checkout-scroll">
<table class="table table-dark mb-0" id="checkout-table"> <table class="table table-dark mb-0" id="checkout-table">
@@ -274,7 +327,7 @@ $Epi = new Epirent();
</div> </div>
<!-- Check-In --> <!-- Check-In -->
<div class="col-sm"> <div class="col-xl">
<h2 class="text-light">Check-In</h2> <h2 class="text-light">Check-In</h2>
<div class="tableFixHead" id="checkin-scroll"> <div class="tableFixHead" id="checkin-scroll">
<table class="table table-dark mb-0" id="checkin-table"> <table class="table table-dark mb-0" id="checkin-table">

View File

@@ -36,7 +36,7 @@ Die Anwendung ist speziell für den Einsatz in Lagerprozessen entwickelt.
1. Repository klonen oder Dateien ins Webverzeichnis kopieren: 1. Repository klonen oder Dateien ins Webverzeichnis kopieren:
```bash ```bash
git clone http://srvgitea01.vtm.zone:3000/epi/EpiWebview git clone http://srvgitea01.vtm.zone:3000/epi/EpiWebview
2. Dashboard Aufrufen und auf die Config-Seite wechseln, notwendige Einstellungen vornehmen.
## Changelog ## Changelog
Verschoben in Releases (Git) Verschoben in Releases (Git)
--- ---

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>

200
dist/index.php vendored
View File

@@ -1,148 +1,78 @@
<?php
// index.php Dashboard Startseite (lädt Teil-HTMLs wie die Auftragsliste, ohne Auto-Refresh)
require('../config.php');
require('../EpiApi.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, shrink-to-fit=no" /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="description" content="" /> <title>Dashboard - EpiWebview</title>
<meta name="author" content="" />
<title>Dashboard - EpiWebview</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>
<script src="https://kit.fontawesome.com/93d71de8bc.js" crossorigin="anonymous"></script>
<link href="/src/fa/css/fontawesome.css" rel="stylesheet" /> <!-- Styles -->
<link href="/src/fa/css/brands.css" rel="stylesheet" /> <link href="css/styles.css" rel="stylesheet" />
<link href="/src/fa/css/solid.css" rel="stylesheet" /> <link href="https://cdn.datatables.net/1.10.20/css/dataTables.bootstrap4.min.css" rel="stylesheet" crossorigin="anonymous" />
<link href="/src/fa/css/sharp-thin.css" rel="stylesheet" />
<link href="/src/fa/css/sharp-duotone-thin.css" rel="stylesheet" />
<script type="text/javascript">
<!-- JS (nur 1x jQuery laden) -->
<script src="js/jquery-3.5.1.min.js"></script>
<script src="https://kit.fontawesome.com/93d71de8bc.js" crossorigin="anonymous"></script>
$(document).ready(function(){ <style>
refreshOrderTable(); .opacity-50 { opacity: .5; }
loadSidenav(); .card .h2, .card .display-4 { font-weight: 700; }
loadFooter(); .kpi-updated { font-size: .82rem; opacity: .85; }
}); </style>
function refreshOrderTable(){
$('#OrderTableHolder').load('../sources/getOrders.php', function(){ <script>
setTimeout(refreshOrderTable, 5000); // Einmalige Loads analog zur Auftragsliste
}); $(function () {
} loadSidenav();
function loadSidenav(){ loadFooter();
});
function loadSidenav() {
$('#layoutSidenav_nav').load('../sources/getSidenav.php'); $('#layoutSidenav_nav').load('../sources/getSidenav.php');
} }
function loadFooter(){
function loadFooter() {
$('#footerholder').load('../sources/getFooter.php'); $('#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"><i class="fas fa-bars"></i></button>
</nav>
<div id="layoutSidenav">
<div id="layoutSidenav_nav"></div>
<div id="layoutSidenav_content">
<main>
<div class="container-fluid">
<h1 class="mt-4">Dashboard</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item active">Dashboard</li>
</ol>
</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" href="#"><i class="fas fa-bars"></i></button>
</nav>
<div id="layoutSidenav">
<div id="layoutSidenav_nav">
</nav>
</div>
<div id="layoutSidenav_content">
<main>
<div class="container-fluid">
<h1 class="mt-4">Dashboard</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item active">Dashboard</li>
</ol>
<div class="row">
<div class="col-xl-3 col-md-6">
<div class="card bg-primary text-white mb-4">
<div class="card-body">Primary Card</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="#">View Details</a>
<div class="small text-white"><i class="fas fa-angle-right"></i></div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-warning text-white mb-4">
<div class="card-body">Warning Card</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="#">View Details</a>
<div class="small text-white"><i class="fas fa-angle-right"></i></div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-success text-white mb-4">
<div class="card-body">Success Card</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="#">View Details</a>
<div class="small text-white"><i class="fas fa-angle-right"></i></div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-danger text-white mb-4">
<div class="card-body">Danger Card</div>
<div class="card-footer d-flex align-items-center justify-content-between">
<a class="small text-white stretched-link" href="#">View Details</a>
<div class="small text-white"><i class="fas fa-angle-right"></i></div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xl-6">
<div class="card mb-4">
<div class="card-header">
<i class="fas fa-chart-area mr-1"></i>
Area Chart Example
</div>
<div class="card-body"><canvas id="myAreaChart" width="100%" height="40"></canvas></div>
</div>
</div>
<div class="col-xl-6">
<div class="card mb-4">
<div class="card-header">
<i class="fas fa-chart-bar mr-1"></i>
Bar Chart Example
</div>
<div class="card-body"><canvas id="myBarChart" width="100%" height="40"></canvas></div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<i class="fas fa-table mr-1"></i>
Aufträge
</div>
<div class="card-body">
<div class="table-responsive" id="OrderTableHolder">
</div> </div>
</div> </main>
</div> <div id="footerholder"></div>
</div> </div>
</main> </div>
<div id="footerholder"></div>
</div> <!-- Bootstrap Bundle (Popper inkl.) -->
</div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="js/jquery-3.5.1.min.js"></script> </body>
<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 src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.8.0/Chart.min.js" crossorigin="anonymous"></script>
<script src="assets/demo/chart-area-demo.js"></script>
<script src="assets/demo/chart-bar-demo.js"></script>
<script src="https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.datatables.net/1.10.20/js/dataTables.bootstrap4.min.js" crossorigin="anonymous"></script>
<script src="assets/demo/datatables-demo.js"></script>
</body>
</html> </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

193
sources/getCheckInCards.php Normal file
View File

@@ -0,0 +1,193 @@
<?php
// ../sources/getCheckInCards.php
error_reporting(E_ALL);
require('../config.php');
require('../EpiApi.php');
$Epi = new Epirent();
use chillerlan\QRCode\{ QRCode, QROptions };
require('../vendor/autoload.php');
const APP_TZ = 'Europe/Berlin';
/** ---------- Helpers (wie in deiner Check-In-Datei) ---------- */
function dt(?string $s): ?DateTimeImmutable {
if (!$s) return null;
try { return new DateTimeImmutable($s, new DateTimeZone(APP_TZ)); }
catch (Throwable $e) { return null; }
}
function dayStart(DateTimeImmutable $d): DateTimeImmutable {
return $d->setTime(0, 0, 0);
}
function rowClassForDate(?DateTimeImmutable $markDate, ?DateTimeImmutable $today): string {
if (!$markDate || !$today) return '';
if ($markDate == $today) return 'text-dark bg-warning';
if ($markDate < $today) return 'bg-danger';
return '';
}
/**
* Row-Marking Quelle (Check-In) identisch zu deiner Tabelle:
* 1 = $packingjob->date_end
* 2 = $NachbereitungsTimeDetail->date_start
* 3 = $RePackagingTimeDetail->date_start
* 4 = $PackingNoteDetail->date_redelivery
* Fallback: $packingjob->date_end
*/
function resolveRowMarkDateCheckIn(
$packingjob,
$NachbereitungsTimeDetail,
$RePackagingTimeDetail,
$PackingNoteDetail,
int $source
): ?DateTimeImmutable {
$candidate = null;
switch ($source) {
case 1: $candidate = dt($packingjob->date_end ?? null); break;
case 2: $candidate = dt($NachbereitungsTimeDetail->date_start ?? null); break;
case 3: $candidate = dt($RePackagingTimeDetail->date_start ?? null); break;
case 4: $candidate = dt($PackingNoteDetail->date_redelivery ?? null); break;
default: $candidate = null; break;
}
if (!$candidate) {
$candidate = dt($packingjob->date_end ?? null);
}
return $candidate ? dayStart($candidate) : null;
}
/** ---------- Daten holen (wie in deiner Check-In-Datei) ---------- */
// Offene Packingnotes für Check-IN-Seite: isco=False
$result = $Epi->requestEpiApi('/v1/packingnote/open?isco=False&cl=' . Epirent_Mandant);
$data_output = json_decode($result)->payload ?? [];
/** ---------- Zähler ---------- */
$today = dayStart(new DateTimeImmutable('today', new DateTimeZone(APP_TZ)));
$limit = null;
if (CheckIn_FutureDays != -1) {
$limit = $today->modify('+' . (int)CheckIn_FutureDays . ' day');
}
$cntOverdue = 0;
$cntToday = 0;
$cntFuture = 0;
/** ---------- Zählen mit exakt deiner Logik ---------- */
foreach ($data_output as $packingjob) {
if ($packingjob->is_archived == true) continue;
// OrderDetails (für Nachbereitung / Rückpacken)
$orderRes = $Epi->requestEpiApi('/v1/order/' . $packingjob->order_pk . '?cl=' . Epirent_Mandant);
$orderdetail_output = json_decode($orderRes)->payload[0] ?? null;
// PackingNote Details (für Redelivery)
$pnRes = $Epi->requestEpiApi('/v1/packingnote/' . $packingjob->primary_key . '?cl=' . Epirent_Mandant);
$PackingNoteDetail = json_decode($pnRes)->payload[0] ?? null;
// Zeit-Slots aus dem Schedule
$NachbereitungsTimeDetail = null;
$RePackagingTimeDetail = null;
if ($orderdetail_output && !empty($orderdetail_output->order_schedule)) {
foreach ($orderdetail_output->order_schedule as $scheduledetail) {
if ($scheduledetail->name == Nachbereitung_Zeitvariable) {
$NachbereitungsTimeDetail = $scheduledetail;
}
if ($scheduledetail->name == Rückpacken_Zeitvariable) {
$RePackagingTimeDetail = $scheduledetail;
}
}
}
// Row-Mark Source (mit Override-Flag)
$source = CheckIn_UseDispoEndForRowMarking ? 1 : (int)CheckInRowMarkSource;
$markDate = resolveRowMarkDateCheckIn(
$packingjob,
$NachbereitungsTimeDetail,
$RePackagingTimeDetail,
$PackingNoteDetail,
$source
);
if (!$markDate) continue;
// Fenster berücksichtigen (gleich wie in deiner Tabelle)
if (CheckIn_FutureDays != -1 && $limit && $markDate > $limit) {
continue;
}
// Zählen
if ($markDate < $today) {
$cntOverdue++;
} elseif ($markDate == $today) {
$cntToday++;
} else {
$cntFuture++;
}
}
$ts = (new DateTimeImmutable('now', new DateTimeZone(APP_TZ)))->format('H:i:s');
?>
<h3 class="mb-3">
<span class="badge badge-secondary d-block w-100 py-2 text-center">CheckIn</span>
</h3>
<div class="row">
<div class="col-xl-4 col-md-6">
<div class="card bg-danger text-white mb-4">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<div class="small">Überfällige CheckIns</div>
<div class="h2 mb-0"><?= (int)$cntOverdue ?></div>
</div>
<i class="fa-solid fa-triangle-exclamation fa-2x" style="opacity:.6"></i>
</div>
<div class="card-footer d-flex justify-content-between">
<span class="small text-white-50">Stand <?= htmlspecialchars($ts) ?></span>
<span class="small">bis <?= htmlspecialchars($today->format('d.m.Y')) ?></span>
</div>
</div>
</div>
<div class="col-xl-4 col-md-6">
<div class="card bg-warning text-dark mb-4">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<div class="small">Heutige CheckIns</div>
<div class="h2 mb-0"><?= (int)$cntToday ?></div>
</div>
<i class="fa-solid fa-boxes-packing fa-2x" style="opacity:.6"></i>
</div>
<div class="card-footer d-flex justify-content-between">
<span class="small text-muted">Stand <?= htmlspecialchars($ts) ?></span>
<span class="small"><?= htmlspecialchars($today->format('d.m.Y')) ?></span>
</div>
</div>
</div>
<div class="col-xl-4 col-md-6">
<div class="card bg-success text-white mb-4">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<div class="small">
Zukünftige CheckIns
<?php if (CheckIn_FutureDays != -1 && $limit): ?>
(≤ <?= htmlspecialchars($limit->format('d.m.Y')) ?>)
<?php endif; ?>
</div>
<div class="h2 mb-0"><?= (int)$cntFuture ?></div>
</div>
<i class="fa-solid fa-forward-step fa-2x" style="opacity:.6"></i>
</div>
<div class="card-footer d-flex justify-content-between">
<span class="small text-white-50">Stand <?= htmlspecialchars($ts) ?></span>
<span class="small">
<?php if (CheckIn_FutureDays == -1): ?>
unbegrenzt
<?php else: ?>
bis <?= htmlspecialchars($limit->format('d.m.Y')) ?>
<?php endif; ?>
</span>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,189 @@
<?php
// sources/getCheckoutTable.php zählt statt Tabellenausgabe drei KPIs als Karten
error_reporting(E_ALL);
require('../config.php');
require('../EpiApi.php');
$Epi = new Epirent();
const APP_TZ = 'Europe/Berlin';
/** ---- Helpers (aus deiner bisherigen Datei) ---- */
function dt(?string $s): ?DateTimeImmutable {
if (!$s) return null;
try { return new DateTimeImmutable($s, new DateTimeZone(APP_TZ)); }
catch (Throwable $e) { return null; }
}
function dayStart(DateTimeImmutable $d): DateTimeImmutable {
return $d->setTime(0, 0, 0);
}
/**
* Quelle für das Markierungsdatum (identisch zu deiner Liste):
* 1 = $packingjob->date_start
* 2 = $VorbereitungsTimeDetail->date_start
* 3 = $PackingNoteDetail->date_packing
* 4 = $PackingNoteDetail->date_delivery
* Fallback: $packingjob->date_start
*/
function resolveRowMarkDate($packingjob, $VorbereitungsTimeDetail, $PackingNoteDetail, int $source): ?DateTimeImmutable {
$candidate = null;
switch ($source) {
case 1: $candidate = dt($packingjob->date_start ?? null); break;
case 2: $candidate = dt($VorbereitungsTimeDetail->date_start ?? null); break;
case 3: $candidate = dt($PackingNoteDetail->date_packing ?? null); break;
case 4: $candidate = dt($PackingNoteDetail->date_delivery ?? null); break;
default:$candidate = null; break;
}
if (!$candidate) $candidate = dt($packingjob->date_start ?? null);
return $candidate ? dayStart($candidate) : null;
}
/** ---- Daten laden wie bisher (Checkout-Quelle) ---- */
if (UseDeliveredForCheckOutCompleted) {
// isco=False -> Checkout-Ansicht offen
$result = $Epi->requestEpiApi('/v1/packingnote/open?isco=False&cl=' . Epirent_Mandant);
} else {
// isci=False -> Alternative Logik (wie in deiner Datei)
$result = $Epi->requestEpiApi('/v1/packingnote/open?isci=False&cl=' . Epirent_Mandant);
}
$data_output = json_decode($result)->payload ?? [];
/** ---- Zähler vorbereiten ---- */
$today = dayStart(new DateTimeImmutable('today', new DateTimeZone(APP_TZ)));
$limit = null; // optionales Fenster wie in deiner Liste
if (CheckOut_FutureDays != -1) {
$limit = $today->modify('+' . (int)CheckOut_FutureDays . ' day');
}
$cntOverdue = 0;
$cntToday = 0;
$cntFuture = 0;
/** ---- Iteration identisch zur bisherigen Logik ---- */
foreach ($data_output as $packingjob) {
// Details nur holen, wenn nötig & wie gehabt
$PackingNoteDetail = null;
if ($packingjob->is_archived != true && UseDeliveredForCheckOutCompleted) {
$PackingNoteDetailResult = $Epi->requestEpiApi('/v1/packingnote/' . $packingjob->primary_key . '?cl=' . Epirent_Mandant);
$PackingNoteDetail = json_decode($PackingNoteDetailResult)->payload[0] ?? null;
}
// Aufnahmebedingung exakt wie bei dir:
if (
$packingjob->is_archived != true
&& (
!UseDeliveredForCheckOutCompleted
|| ($PackingNoteDetail && ($PackingNoteDetail->date_delivered !== "0000-00-00" || (int)$PackingNoteDetail->time_delivered !== 0))
)
&& ($PackingNoteDetail && (int)$PackingNoteDetail->is_all_out !== 0)
) {
// OrderDetails (für Vorbereitungszeit)
$result = $Epi->requestEpiApi('/v1/order/' . $packingjob->order_pk . '?cl=' . Epirent_Mandant);
$orderdetail_output = (json_decode($result)->payload[0] ?? null);
// PackingNoteDetail nachladen, falls oben noch nicht geholt
if (!UseDeliveredForCheckOutCompleted) {
$PackingNoteDetailResult = $Epi->requestEpiApi('/v1/packingnote/' . $packingjob->primary_key . '?cl=' . Epirent_Mandant);
$PackingNoteDetail = json_decode($PackingNoteDetailResult)->payload[0] ?? null;
if (!$PackingNoteDetail) continue;
}
// Vorbereitungs-Zeitdetail ermitteln
$VorbereitungsTimeDetail = null;
if ($orderdetail_output && !empty($orderdetail_output->order_schedule)) {
foreach ($orderdetail_output->order_schedule as $scheduledetail) {
if ($scheduledetail->name == Vorbereitungs_Zeitvariable) {
$VorbereitungsTimeDetail = $scheduledetail;
break;
}
}
}
// Markierungsdatum wie in der Tabelle
$markDate = resolveRowMarkDate($packingjob, $VorbereitungsTimeDetail, $PackingNoteDetail, (int)CheckOutRowMarkSource);
if (!$markDate) continue;
// Optionales Zukunftsfenster berücksichtigen (wie bisher)
if (CheckOut_FutureDays != -1 && $limit && $markDate > $limit) {
// außerhalb des Fensters ignorieren
continue;
}
// Zählen
if ($markDate < $today) {
$cntOverdue++;
} elseif ($markDate == $today) {
$cntToday++;
} else { // $markDate > $today
$cntFuture++;
}
}
}
// Zeitstempel für die Karten
$ts = (new DateTimeImmutable('now', new DateTimeZone(APP_TZ)))->format('H:i:s');
/** ---- Ausgabe: 3 Bootstrap-Karten ---- */
?>
<h3 class="mb-3">
<span class="badge badge-secondary d-block w-100 py-2 text-center">Checkout</span>
</h3>
<div class="row">
<div class="col-xl-4 col-md-6">
<div class="card bg-danger text-white mb-4">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<div class="small">Überfällige CheckOuts</div>
<div class="h2 mb-0"><?= (int)$cntOverdue ?></div>
</div>
<i class="fa-solid fa-triangle-exclamation fa-2x" style="opacity:.6"></i>
</div>
<div class="card-footer d-flex justify-content-between">
<span class="small text-white-50">Stand <?= htmlspecialchars($ts) ?></span>
<span class="small">bis <?= htmlspecialchars($today->format('d.m.Y')) ?></span>
</div>
</div>
</div>
<div class="col-xl-4 col-md-6">
<div class="card bg-warning text-dark mb-4">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<div class="small">Heutige CheckOuts</div>
<div class="h2 mb-0"><?= (int)$cntToday ?></div>
</div>
<i class="fa-solid fa-dolly fa-2x" style="opacity:.6"></i>
</div>
<div class="card-footer d-flex justify-content-between">
<span class="small text-muted">Stand <?= htmlspecialchars($ts) ?></span>
<span class="small"><?= htmlspecialchars($today->format('d.m.Y')) ?></span>
</div>
</div>
</div>
<div class="col-xl-4 col-md-6">
<div class="card bg-success text-white mb-4">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<div class="small">Zukünftige CheckOuts<?= (CheckOut_FutureDays != -1 ? ' (≤ '.$limit->format('d.m.Y').')' : '') ?></div>
<div class="h2 mb-0"><?= (int)$cntFuture ?></div>
</div>
<i class="fa-solid fa-forward-step fa-2x" style="opacity:.6"></i>
</div>
<div class="card-footer d-flex justify-content-between">
<span class="small text-white-50">Stand <?= htmlspecialchars($ts) ?></span>
<span class="small">
<?php if (CheckOut_FutureDays == -1): ?>
unbegrenzt
<?php else: ?>
bis <?= htmlspecialchars($limit->format('d.m.Y')) ?>
<?php endif; ?>
</span>
</div>
</div>
</div>
</div>

View File

@@ -71,6 +71,7 @@ function rowClassForDate(?DateTimeImmutable $markDate, ?DateTimeImmutable $today
if (UseDeliveredForCheckOutCompleted) { if (UseDeliveredForCheckOutCompleted) {
$result = $Epi->requestEpiApi('/v1/packingnote/open?isco=False&cl=' . Epirent_Mandant); $result = $Epi->requestEpiApi('/v1/packingnote/open?isco=False&cl=' . Epirent_Mandant);
} else { } else {
$result = $Epi->requestEpiApi('/v1/packingnote/open?isci=False&cl=' . Epirent_Mandant); $result = $Epi->requestEpiApi('/v1/packingnote/open?isci=False&cl=' . Epirent_Mandant);
} }
@@ -107,15 +108,17 @@ foreach ($data_output as $packingjob) {
if ($packingjob->is_archived != true && UseDeliveredForCheckOutCompleted) { if ($packingjob->is_archived != true && UseDeliveredForCheckOutCompleted) {
$PackingNoteDetailResult = $Epi->requestEpiApi('/v1/packingnote/' . $packingjob->primary_key . '?cl=' . Epirent_Mandant); $PackingNoteDetailResult = $Epi->requestEpiApi('/v1/packingnote/' . $packingjob->primary_key . '?cl=' . Epirent_Mandant);
$PackingNoteDetail = json_decode($PackingNoteDetailResult)->payload[0]; $PackingNoteDetail = json_decode($PackingNoteDetailResult)->payload[0];
} }
if ( if (
$packingjob->is_archived != true ($packingjob->is_archived ?? false) != true
&& ( && (
!UseDeliveredForCheckOutCompleted ((int)($PackingNoteDetail->is_all_out ?? 0)) !== 0
|| ($PackingNoteDetail->date_delivered !== "0000-00-00" || (int)$PackingNoteDetail->time_delivered !== 0) || (($PackingNoteDetail->date_delivered ?? "0000-00-00") === "0000-00-00"
|| (int)($PackingNoteDetail->time_delivered ?? 0) === 0)
) )
&& (int)$PackingNoteDetail->is_all_out !== 0
) { ) {
//get OrderDetails //get OrderDetails
$result = $Epi->requestEpiApi('/v1/order/' . $packingjob->order_pk . '?cl=' . Epirent_Mandant); $result = $Epi->requestEpiApi('/v1/order/' . $packingjob->order_pk . '?cl=' . Epirent_Mandant);

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">