New Filestructure for Docker

This commit is contained in:
2026-07-31 23:53:14 +02:00
parent 1e10ed4de8
commit 5bcf2a3546
239 changed files with 710 additions and 2000 deletions
+797
View File
@@ -0,0 +1,797 @@
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
// Show all errors except deprecation notices (these come from vendor libraries
// that aren't yet typed for newer PHP versions). Long-term fix: update
// dependencies to versions compatible with your PHP runtime.
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
if (!isset($baseDir)) {
$baseDir = $_SERVER['DOCUMENT_ROOT'];
}
require_once $baseDir . '/../scripts/session_functions.php';
ini_wkvs_session(true);
$csrf_token = $_SESSION['csrf_token'] ?? '';
$access_granted_wkl = check_user_permission('wk_leitung', true) ?? false;
if ( ! $access_granted_wkl ) :
$logintype = 'wk_leitung';
require $baseDir . '/../scripts/login/login.php';
$logintype = '';
else :
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="png" href="/intern/img/icon.png">
<title>Intern - Rechnungen</title>
<link rel="stylesheet" href="/intern/css/rechnungen.css">
<link rel="stylesheet" href="/intern/css/sidebar.css">
<link rel="stylesheet" href="/files/fonts/fonts.css">
<script src="/intern/js/jquery/jquery-3.7.1.min.js"></script>
<script src="/intern/js/chart/chart.js"></script>
<link rel="stylesheet" href="/intern/css/user.css">
<script>
(function () {
let lastWrite = 0;
const interval = 200;
window.addEventListener('scroll', function () {
const now = Date.now();
if (now - lastWrite >= interval) {
sessionStorage.setItem('scrollY', window.scrollY);
lastWrite = now;
}
}, { passive: true });
})();
</script>
</head>
<body class="rechnungen">
<?php
require $baseDir . '/../scripts/db/db-functions.php';
require $baseDir . '/../scripts/db/db-tables.php';
$type = 'wkl';
$dbconnection = require $baseDir . '/../scripts/db/db-verbindung-script.php';
if ($dbconnection['success'] !== true){
echo 'Critical DB Error.';
exit;
}
$currentPage = 'rechnungen';
require $baseDir . '/../scripts/sidebar/sidebar.php';
setlocale(LC_TIME, 'de_DE.UTF-8');
$svgbezahlt = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="10" fill="#28a745"/><path d="M7 12l3 3 7-7" stroke="white" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>';
$svgpending = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 2h12v2a6 6 0 0 1-6 6 6 6 0 0 1-6-6V2zm0 20h12v-2a6 6 0 0 0-6-6 6 6 0 0 0-6 6v2zm6-10c1.657 0 3-1.343 3-3H9c0 1.657 1.343 3 3 3z" fill="#0073aa"/></svg>';
$svgnichtbezahlt = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="10" fill="#dc3545"/><path d="M8 8l8 8M16 8l-8 8" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
$currentYear = (date('n') > 6) ? date('Y') + 1 : date('Y');
$sql = "SELECT
o.*,
iu.username AS order_creator
FROM
$db_tabelle_verbuchte_startgebueren o
LEFT JOIN
$db_tabelle_intern_benutzende iu
ON iu.id = o.user_id
WHERE o.order_status <> 0
ORDER BY
o.order_status ASC,
o.timestamp DESC;
";
$sel = $mysqli->prepare($sql);
if (!$sel->execute()) {
http_response_code(500);
exit;
}
$allRechnungenUnsorted = [];
$res = $sel->get_result();
while ($row = $res->fetch_assoc()) {
$allRechnungenUnsorted[] = $row;
}
foreach ($allRechnungenUnsorted as $r) {
$allRechnungen[$r['order_status']][] = $r;
}
function titleStatus(int $int) : string {
switch ($int) {
case 1:
return "Zahlungseingang überprüfen";
case 2:
return "Bezahlte Rechnungen";
case 3:
return "Stornierte Rechnungen";
default:
return "FEHLER BEI NAMENSGENERRIERUNG";
}
}
function classStatus(int $int) : string {
switch ($int) {
case 1:
return "inProcess";
case 2:
return "payed";
case 3:
return "storniert";
default:
return "notPayed";
}
}
?>
<div class="internMenuDiv">
<div class="closeInternMenuMobileDiv"></div>
<div class="innerInternMenuDiv">
<div class="header-text-conatiner"></div>
<div class="referenzDiv">
<h3 class="containerHeading">Rechnung als bezahlt eintagen</h3>
<label for="scorNumber">Referenznummer<br><br>
<div class="referenzDivInputDiv">
<span>RF</span>
<input
type="text"
id="scorNumber"
inputmode="numeric"
autocomplete="off"
placeholder="12 3456 7890"
maxlength="19"
>
</div>
</label>
<!-- Hidden field for backend -->
<input type="hidden" id="scorNumberRaw" name="scor_reference">
<button id="submitScorNumber">Bestätigen</button>
</div>
</div>
</div>
<section class="bgSection">
<?php sidebarRender('modal'); ?>
<div class="headerDivTrainer">
<div class="headingPanelDiv">
<h2 class="headingPanel">Rechnungen</h2>
<h3 class="headingPanelUser"><?= $currentYear ?></h3>
</div>
<div class="menuWrapper">
<div class="trainerBurgerMenuDiv">
<svg viewBox="0 0 24 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 2a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v24a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1z" stroke="currentColor" stroke-width="1.5"/><path d="M7 5h10M7 8h10M7 11h5" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>
<path stroke="currentColor" stroke-dasharray="2 1" d="M4 16h16"/>
<rect x="6" y="18" width="5" height="5" rx=".5" stroke="currentColor" stroke-width="1.2"/>
<path fill="currentColor" d="M8 20h1v1H8z"/>
<path d="M13 19h4m-4 2h4m-4 2h2" stroke="currentColor" stroke-linecap="round"/>
</svg>
</div>
<?php sidebarRender('button'); ?>
</div>
</div>
<script>
const menuburger = document.querySelector('.trainerBurgerMenuDiv');
const menudiv = document.querySelector('.internMenuDiv');
//const menubg = document.querySelector('.menuBg');
const content = document.querySelector('.bgSection');
const closeMenuMobile = document.querySelector('.closeInternMenuMobileDiv');
const storageKey = "trainerInternMenuOpen";
const isOpen = localStorage.getItem(storageKey) === "true";
document.addEventListener('keydown', function (e) {
// Mac = Option, Windows/Linux = Alt
const isOptionOrAlt = e.altKey || e.metaKey;
if (e.key.toLowerCase() === 'm') {
menuburger.classList.add("menuTransition");
menudiv.classList.add("menuTransition");
content.classList.add("menuTransition");
menuburger.classList.toggle("open");
menudiv.classList.toggle("open");
content.classList.toggle("open");
const isOpenEl =
menudiv.classList.contains("open") &&
menuburger.classList.contains("open");
localStorage.setItem(storageKey, isOpenEl);
}
});
if (isOpen) {
menuburger.classList.add("open");
menudiv.classList.add("open");
content.classList.add("open");
}
menuburger.addEventListener("click", function () {
menuburger.classList.add("menuTransition");
menudiv.classList.add("menuTransition");
content.classList.add("menuTransition");
menuburger.classList.toggle("open");
menudiv.classList.toggle("open");
content.classList.toggle("open");
const isOpenEl =
menudiv.classList.contains("open") &&
menuburger.classList.contains("open");
localStorage.setItem(storageKey, isOpenEl);
});
closeMenuMobile.addEventListener("click", function () {
const isOpenEl =
menudiv.classList.contains("open") &&
menuburger.classList.contains("open");
if (isOpenEl) {
menuburger.classList.remove("open");
menudiv.classList.remove("open");
content.classList.remove("open");
localStorage.setItem(storageKey, false);
}
});
</script>
</div>
<div class="containerDiv">
<h3 class="containerHeading">Alle Rechnungen:</h3>
<?php if (!isset($allRechnungen) || !is_array($allRechnungen) || count($allRechnungen) < 1) : ?>
<h3>Keine erstellten Rechungen vorhanden</h3>
<?php else :?>
<div style="width: 200px;">
<canvas id="rechnungenOverviewChart"></canvas>
</div>
<div>
<p class="labelBulkSelect">Bulk Select:</p>
<div class="bulkSelectDiv">
<div class="customSelect" id="bulkSelectedOption" data-value="">
<button type="button" class="selectTrigger bulkSelect" aria-expanded="false">
<span class="selectLabel">Aktion auswählen</span>
<svg class="selectArrow" xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' height="14" width="14">
<path d='M6 9L12 15L18 9' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/>\
</svg>
</button>
<ul class="selectOptions">
<li data-value="deleteEntrys">Rechnungen stornieren</li>
<li data-value="payEntrys">Rechnungen bezahlt</li>
</ul>
</div>
<input type="submit" name="apply_bulk_action" class="bulkSelectSubmit" value="Anwenden">
</div>
<?php $totalOrderVolumeArray = []; ?>
<?php foreach ($allRechnungen as $status => $oneRechnungsStatus) : ?>
<?php $totalOrderVolume = 0; $totalPayedVolume = 0; ?>
<h2 class="titleSingleProg"><?= titleStatus(intval($status)) ?></h2>
<table class="wkvsTabelle widefat striped">
<thead>
<tr>
<th style="width:20px;">
<label class="checkbox">
<input type="checkbox" class="masterCheckbox" data-type="<?= $status ?>">
<span class="checkbox-ui"></span>
</label>
</th>
<th>Rechnungsnummer</th>
<th>Status</th>
<th>Typ</th>
<th>Ersteller</th>
<th>Datum</th>
<th>Änderung</th>
<th>Betrag</th>
</tr>
</thead>
<tbody>
<?php foreach ($oneRechnungsStatus as $row) : ?>
<tr>
<td>
<label class="checkbox">
<input type="checkbox" name="bulk_ids[]" class="rowCheckbox" data-type="<?= $status ?>" value="<?= intval($row['order_id']) ?>">
<span class="checkbox-ui"></span>
</label>
</td>
<td>
<?php if (file_exists($baseDir . "/../private-files/rechnungen/" . intval($row['order_id']) . ".pdf")) : ?>
<a href="/intern/wk-leitung/rechnungen-viewer?order_id=<?= intval($row['order_id']) ?>" target="_blank">
<?= intval($row['order_id']) ?>
</a>
<?php else: ?>
<?= intval($row['order_id']) ?>
<?php endif; ?>
</td>
<td><?php
switch (intval($status)) {
case '2':
echo $svgbezahlt;
break;
case '1':
echo $svgpending;
break;
default:
echo $svgnichtbezahlt;
break;
}?> </td>
<td><?= htmlspecialchars($row['order_type']) ?></td>
<td><?= htmlspecialchars($row['order_creator']) ?></td>
<td><?= htmlspecialchars(strftime('%d. %B %Y', (new DateTime($row['timestamp']))->getTimestamp())) ?></td>
<td><?= htmlspecialchars(strftime('%d. %B %Y', (new DateTime($row['edited_at']))->getTimestamp())) ?></td>
<td>CHF <?= htmlspecialchars(number_format(floatval($row['preis']), 2)) ?></td>
<?php $totalOrderVolume += floatval($row['preis']); ?>
</tr>
<?php endforeach; ?>
<tr class="totalTr">
<td colspan="7" class="totalTd"></td>
<td class="totalValue"><span class="<?= classStatus(intval($status)) ?>">CHF <?= htmlspecialchars(number_format(floatval($totalOrderVolume), 2))?></span></td>
</tr>
<?php $totalOrderVolumeArray[$status] = number_format(floatval($totalOrderVolume), 2); ?>
</tbody>
</table>
<?php endforeach; ?>
<div>
<button class="buttonDownload" id="downloadRechnungenProtokoll">Protokoll Rechnungsänderungen herunterladen</button>
</div>
<?php endif; ?>
</div>
<!--<div class="containerDiv"><h3 class="headingAlleTurnerinnen">Alle Turnerinnen:</h3>
<?php //else : ?>
<p>Noch keine Datensätze vorhanden.</p>
<?php // endif; ?>
-->
<div class="msgDiv"></div>
</section>
<?php endif; ?>
<script>
const csrf_token = "<?= $csrf_token ?>";
const $input = $('#scorNumber');
const $rawInput = $('#scorNumberRaw');
$input.on('input', function() {
let value = $(this).val().replace(/\D/g, ''); // remove non-digits
// Optional: limit total digits to 21 (SCOR standard)
value = value.slice(0, 21);
// Update hidden raw value
$rawInput.val(value);
// Format: first 2 digits, then groups of 4
let formatted = '';
if (value.length > 0) {
// First 2 digits
formatted += value.substr(0, 2);
// Remaining digits
let remaining = value.substr(2);
if (remaining.length > 0) {
formatted += ' ' + remaining.match(/.{1,4}/g).join(' ');
}
}
$(this).val(formatted);
});
$('#downloadRechnungenProtokoll').on('click', function() {
const $button = $(this);
$button.addClass('opacity50');
const url = '/intern/scripts/rechnungen/ajax-neu_protokoll.php';
fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: new URLSearchParams({
csrf_token
})
})
.then(res => {
if (!res.ok) throw new Error('Network response was not ok');
return res.blob().then(blob => ({ type: 'blob', data: blob }));
})
.then(result => {
if (result.type === 'blob') {
const url = window.URL.createObjectURL(result.data);
const a = document.createElement('a');
a.href = url;
a.download = "Protokoll_Rechnungen.pdf";
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
displayMsg(1, "Protokoll wurde erfolgreich heuntergeladen.");
}
$button.removeClass('opacity50');
})
.catch(err => {
displayMsg(0, "Error processing request:", err);
console.error("Error processing request:", err);
$button.removeClass('opacity50');
});
});
$('#submitScorNumber').on('click', function(e) {
e.preventDefault(); // prevent form submission if it's a button
const SCOR = $('#scorNumberRaw').val();
fetch('/intern/scripts/rechnungen/ajax_update_order_status_by_scor.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
csrf_token,
scor: SCOR
})
})
.then(res => res.json().then(data => ({ status: res.status, ok: res.ok, data })))
.then(({ status, ok, data }) => {
if (!ok) {
throw {
status,
message: data.message || 'Update failed'
};
}
window.location.reload();
})
.catch(err => {
displayMsg(0, `${err.message}`);
});
});
$('.bulkSelectSubmit').on('click', function (e) {
e.preventDefault();
const valBulkSelect = $('#bulkSelectedOption').data('value');
if (valBulkSelect === 'deleteEntrys' || valBulkSelect === 'payEntrys') {
let arrayIds = [];
$('.rowCheckbox').each(function () {
if ($(this).prop('checked')) {
arrayIds.push($(this).val());
}
});
if (Array.isArray(arrayIds) && arrayIds.length) {
const params = new URLSearchParams();
arrayIds.forEach(id => {
params.append('ids[]', id);
});
params.append('csrf_token', csrf_token);
params.append('action', valBulkSelect);
fetch('/intern/scripts/rechnungen/ajax_update_order_status.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params
})
.then(res => res.json().then(data => ({ status: res.status, ok: res.ok, data })))
.then(({ status, ok, data }) => {
if (!ok) {
throw {
status,
message: data.message || 'Update failed'
};
}
window.location.reload();
})
.catch(err => {
displayMsg(0, `${err.message} (HTTP ${err.status ?? 'unknown'})`);
});
}
}
});
document.querySelectorAll('.masterCheckbox').forEach(master => {
const programm = master.dataset.type;
const checkboxes = document.querySelectorAll('.rowCheckbox[data-type="' + programm + '"]');
master.addEventListener("change", function() {
checkboxes.forEach(cb => cb.checked = master.checked);
});
checkboxes.forEach(cb => {
cb.addEventListener("change", function() {
master.checked = [...checkboxes].every(c => c.checked);
});
});
});
function displayMsg(type, msg) {
const colors = ["#900000ff", "#00b200ff"];
if (type !== 0 && type !== 1) return;
// idx is ALWAYS a valid non-negative index now
const $div = $('<div class="msgBox"></div>')
.css({'border-color': colors[type]})
.text(msg);
$('.msgDiv').append($div);
// trigger entry animation
setTimeout(() => {
$div.addClass('show');
}, 200);
setTimeout(() => {
// First: set the transition properties
$div.css({
'transition': 'all 1s ease'
});
// Next frame: apply the transform so the transition animates
requestAnimationFrame(() => {
$div.removeClass('show');
$div.css({
'transform': 'translateX(calc(100% + 40px)) scaleY(0)' //scaleY(0.5)
});
});
}, 5000);
// auto-remove
setTimeout(() => {
$div.remove();
}, 6500);
}
</script>
<script>
$(function () {
$(".selectTrigger").on("click", function (e) {
e.stopPropagation();
const $select = $(this).closest(".customSelect");
const $options = $select.find(".selectOptions");
$(".selectOptions").not($options).hide();
$(".selectTrigger").not(this).attr("aria-expanded", "false");
$options.toggle();
$(this).attr("ariaExpanded", $options.is(":visible"));
});
$(".selectOptions li").on("click", function () {
const $item = $(this);
const $select = $item.closest(".customSelect");
$select.find(".selectLabel").text($item.text());
$select.attr("data-value", $item.data("value"));
$item
.addClass("selected")
.siblings()
.removeClass("selected");
$select.find(".selectOptions").hide();
$select.find(".selectTrigger").attr("aria-expanded", "false");
});
$(document).on("click", function () {
$(".selectOptions").hide();
$(".selectTrigger").attr("aria-expanded", "false");
});
});
</script>
<script>
// Funktion, um den Tooltip-Container zu erstellen oder zurückzugeben
const getOrCreateTooltip = (chart) => {
// Suche nach einem Element mit einer spezifischen Klasse
let tooltipEl = chart.canvas.parentNode.querySelector('.chartjs-tooltip');
if (!tooltipEl) {
tooltipEl = document.createElement('div');
// Füge eine spezifische Klasse hinzu, damit wir es später leicht finden können
tooltipEl.classList.add('chartjs-tooltip');
// Setze die Basis-Stile
tooltipEl.style.background = 'rgba(0, 0, 0, 0.7)';
tooltipEl.style.borderRadius = '3px';
tooltipEl.style.color = 'white';
tooltipEl.style.opacity = 1;
tooltipEl.style.pointerEvents = 'none';
tooltipEl.style.position = 'absolute';
tooltipEl.style.transform = 'translate(-50%, 0)';
tooltipEl.style.transition = 'all .1s ease';
const table = document.createElement('table');
table.style.margin = '0px';
// Optional: Füge eine Klasse zum Table hinzu, falls du es global stylen möchtest
table.classList.add('chartjs-tooltip-table');
tooltipEl.appendChild(table);
chart.canvas.parentNode.appendChild(tooltipEl);
}
return tooltipEl;
};
const externalTooltipHandler = (context) => {
const {chart, tooltip} = context;
// Verwende die korrigierte Funktion
const tooltipEl = getOrCreateTooltip(chart);
// Verstecken, wenn der Tooltip keine Daten hat
if (tooltip.opacity === 0) {
tooltipEl.style.opacity = 0;
return;
}
// Text setzen
if (tooltip.body) {
const titleLines = tooltip.title || [];
const bodyLines = tooltip.body.map(b => b.lines);
const tableHead = document.createElement('thead');
titleLines.forEach(title => {
const tr = document.createElement('tr');
tr.style.borderWidth = 0;
const th = document.createElement('th');
th.style.borderWidth = 0;
const text = document.createTextNode(title);
th.appendChild(text);
tr.appendChild(th);
tableHead.appendChild(tr);
});
const tableBody = document.createElement('tbody');
bodyLines.forEach((body, i) => {
const colors = tooltip.labelColors[i];
const span = document.createElement('span');
span.style.background = colors.backgroundColor;
span.style.borderColor = colors.borderColor;
span.style.borderWidth = '2px';
span.style.marginRight = '10px';
span.style.height = '10px';
span.style.width = '10px';
span.style.display = 'inline-block';
const tr = document.createElement('tr');
tr.style.backgroundColor = 'inherit';
tr.style.borderWidth = 0;
const td = document.createElement('td');
td.style.borderWidth = 0;
const text = document.createTextNode(body);
td.appendChild(span);
td.appendChild(text);
tr.appendChild(td);
tableBody.appendChild(tr);
});
const tableRoot = tooltipEl.querySelector('table');
// Alte Inhalte entfernen
while (tableRoot.firstChild) {
tableRoot.firstChild.remove();
}
// Neue Inhalte hinzufügen
tableRoot.appendChild(tableHead);
tableRoot.appendChild(tableBody);
}
// Position berechnen
const {offsetLeft: positionX, offsetTop: positionY} = chart.canvas;
// Tooltip anzeigen, positionieren und Stile setzen
tooltipEl.style.opacity = 1;
tooltipEl.style.left = positionX + tooltip.caretX + 'px';
tooltipEl.style.top = positionY + tooltip.caretY + 'px';
tooltipEl.style.font = tooltip.options.bodyFont.string;
tooltipEl.style.padding = tooltip.options.padding + 'px ' + tooltip.options.padding + 'px';
};
const ctx = document.getElementById('rechnungenOverviewChart').getContext('2d');
new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Pendant', 'Bezahlt'],
datasets: [{
// Die Daten bleiben hier als Zahlen (z.B. 150, 200)
data: ["<?= $totalOrderVolumeArray[1] ?? 0 ?>", "<?= $totalOrderVolumeArray[2] ?? 0 ?>"],
backgroundColor: ['#36a2eb', '#4bc0c0']
}]
},
options: {
plugins: {
legend: {
position: 'bottom'
},
tooltip: {
enabled: false, // Standard-Tooltip deaktivieren
external: externalTooltipHandler, // Ihren Handler verwenden
callbacks: {
// Diese Funktion wird aufgerufen, um den Text für jeden Datensatz im Tooltip zu formatieren
label: function(context) {
// Der Wert (context.raw) wird als Zahl genommen, mit "CHF" versehen und gerundet
const value = context.raw;
let label = 'CHF ' + value;
return label;
}
}
}
}
}
});
</script>