Compare commits

...
Author SHA1 Message Date
Tower a6992ed5d9 add: new data uplaod and download service 2026-08-05 20:47:15 +02:00
Tower d88db54635 fix wording 2026-08-01 12:39:43 +02:00
Tower 24cc12374f fix wording 2026-08-01 12:30:04 +02:00
Tower 9ec0465401 Merge branch 'notify-after-login' 2026-07-24 07:47:04 +02:00
Bruno Eckhardt 133b1d57ed fixes and new notification backend service 2026-07-24 07:40:37 +02:00
Bruno Eckhardt 1ad59a9881 add notification messages 2026-07-24 07:32:47 +02:00
Tower c540d06939 Merge branch 'einkaufslisten-vorgefertigt' 2026-07-23 21:49:05 +02:00
Tower 52c11cc1b9 add new einkaufslisten and einkaufslisten reset 2026-07-23 21:48:52 +02:00
Bruno Eckhardt 37fa819e97 Merge branch 'forgot-password' 2026-07-23 15:15:54 +02:00
Bruno Eckhardt 6922bfa99b add 2026-07-22 08:36:43 +02:00
Bruno Eckhardt ce9fe75e9c add formulars 2026-07-21 09:04:14 +02:00
Tower 8bccdddebe Merge branch 'registry-formular' 2026-07-20 21:08:41 +02:00
Tower 1228eccf52 add new registry formular with default rights for home service 2026-07-20 21:06:15 +02:00
Tower 973aa67153 Merge branch 'config-to-database' 2026-07-20 09:25:33 +02:00
Tower a84b13a5c2 final 2026-07-20 09:14:04 +02:00
Tower 5ebed74e12 update but not functional 2026-07-19 22:41:06 +02:00
Tower b80e44fd32 Merge branch 'service-ajax-update' 2026-07-19 20:20:06 +02:00
Tower 5d027879ba ticket control service update auf ajax 2026-07-19 20:15:20 +02:00
Tower 854e6d03cb termine ticketboard update auf ajax 2026-07-19 20:04:10 +02:00
Tower 66eb55d264 ajax test service 2026-07-19 13:27:53 +02:00
Tower 26f5519888 zeiterfassung service update auf ajax 2026-07-19 12:28:59 +02:00
Tower 405b9c27e7 termine service update auf ajax 2026-07-19 10:11:37 +02:00
Tower 6ad068c940 changes 2026-07-19 09:36:02 +02:00
Tower ecea3fe741 many changes and first ajax service einkaufsliste 2026-07-18 22:20:07 +02:00
Tower e570dcce25 add ajaxmethods and ajaxhandler for requests without site reload 2026-07-18 14:43:08 +02:00
Tower 951253c559 logout mailing and wording fix when guest logs in 2026-07-17 20:48:38 +02:00
Tower 8f84260186 new service ticket board and ticket control with mailing 2026-07-17 20:26:56 +02:00
Tower 448bffd2f5 add logout logging 2026-07-16 23:50:59 +02:00
Tower db392d915d change new server ip environment 2026-07-16 19:20:27 +02:00
Tower 584f168074 enable html in email 2026-07-14 19:07:01 +02:00
Tower ce0d16c1ed add and delete connections 2026-07-13 20:41:23 +02:00
Bruno Eckhardt b1416aa933 Merge branch 'domain-umzug' 2026-07-13 14:49:30 +02:00
Bruno Eckhardt c8f4f47439 change domain name 2026-07-13 14:47:33 +02:00
Bruno Eckhardt 86c20a63bf add new DB connection 2026-07-13 14:21:24 +02:00
Bruno Eckhardt 1fba004b9b Merge branch 'auto-merge' 2026-07-13 13:32:11 +02:00
Bruno Eckhardt 9622e35ca3 fixes 2026-07-13 13:32:01 +02:00
Bruno Eckhardt 2748349060 Merge branch 'auto-merge' 2026-07-13 13:02:52 +02:00
Bruno Eckhardt ebbb6551f7 delete comments 2026-07-13 13:02:40 +02:00
Bruno Eckhardt 992ac5eb6a Merge branch 'auto-merge' 2026-07-13 12:35:11 +02:00
Bruno Eckhardt d37937f4d8 add form fixes 2026-07-13 12:34:59 +02:00
Bruno Eckhardt 523400ac0e Merge branch 'auto-merge' 2026-07-13 12:31:02 +02:00
Bruno Eckhardt 2064546b19 add auto merge script 2026-07-13 12:30:48 +02:00
Bruno Eckhardt d71e2c1e2d api controller fixes 2026-07-13 10:45:32 +02:00
Bruno Eckhardt 62dadd175c Merge branch 'vergabe-service' 2026-07-08 09:40:41 +02:00
Bruno Eckhardt 3c9eb7091f feat: search persons and onjects 2026-07-08 09:40:32 +02:00
Bruno Eckhardt 7abcac5f04 feat: autofill and archive deleted things 2026-07-08 09:31:20 +02:00
Bruno Eckhardt 7d03579d31 Merge branch 'vergabe-service' 2026-07-08 08:59:19 +02:00
Bruno Eckhardt 742c5d5b56 feat: autofill 2026-07-08 08:59:03 +02:00
Bruno Eckhardt 0952d6de13 Merge branch 'berufsschulberichte' 2026-07-08 08:15:57 +02:00
Tower 5a9a08421f new berufsschulreport service 2026-07-07 22:13:12 +02:00
Tower ffcc8821ed update 2026-07-07 19:19:57 +02:00
Bruno Eckhardt 72f188e239 raw form 2026-07-07 15:44:07 +02:00
Bruno Eckhardt 6ec159f75c fix email 2026-07-07 14:27:37 +02:00
Bruno Eckhardt d6e70a08eb Merge branch 'termin-service' 2026-07-07 10:08:34 +02:00
Bruno Eckhardt 52446cc019 fixes 2026-07-07 10:07:22 +02:00
Bruno Eckhardt 10472843a7 Merge branch 'termin-service' 2026-07-07 09:07:27 +02:00
Bruno Eckhardt 771f9e7bf6 fix UTF8 formating and right timestamp in DB when updated 2026-07-07 09:05:38 +02:00
Tower a066c59576 client ip changes 2026-07-06 22:30:24 +02:00
Tower 388f9b8f0d add termin mailing 2026-07-06 22:26:26 +02:00
Tower b08857a4cd delete comments and change some words 2026-07-06 21:51:40 +02:00
Tower 6d68fbc25d fix format and delete option 2026-07-06 20:39:21 +02:00
Bruno Eckhardt dceb02a3de fixes 2026-07-06 15:51:27 +02:00
Bruno Eckhardt cd325270f0 add new calender / termine service 2026-07-06 15:18:11 +02:00
Bruno Eckhardt efa9584baf hotfix 2026-07-06 14:33:32 +02:00
Bruno Eckhardt ac202be4a7 add includes 2026-07-06 14:25:06 +02:00
Bruno Eckhardt 24e11e14a3 fix logout error logging 2026-07-06 14:21:53 +02:00
Bruno Eckhardt 5305fcabb6 logout fix 2026-07-06 14:13:33 +02:00
Bruno Eckhardt 3f7c840c39 style fixes 2026-07-06 13:50:12 +02:00
Bruno Eckhardt 378c462b1c new einkaufslisten service 2026-07-06 13:40:55 +02:00
Bruno Eckhardt 5f1aa1c6fc mobile optimizing 2026-07-06 08:47:33 +02:00
Bruno Eckhardt 5ac4fa4ac6 add showing entries and delete option 2026-07-06 08:40:00 +02:00
Bruno Eckhardt 3eb451069b delete wrong letter 2026-07-06 08:22:44 +02:00
Tower caf62ea701 fix logout 2026-07-05 19:13:08 +02:00
Tower 82fe6e85d8 mobile css fixes 2026-07-05 19:06:04 +02:00
Tower 40c26e625b add new vergabe service for papa 2026-07-01 16:39:56 +02:00
Bruno Eckhardt 2901a5e362 add create and save new api token 2026-05-20 12:31:20 +02:00
Tower 2231d65465 fixed user_rights control, when new service is created rights are setted for bruno 2026-05-19 21:55:55 +02:00
Tower 4558a717fa new rightscontroller for new service creating 2026-05-19 21:55:14 +02:00
Bruno Eckhardt b2b4ac2124 new endpoint for workreports and moved reports to schoolreports 2026-05-19 15:13:53 +02:00
Bruno Eckhardt f7fd4ed183 request saves 2026-05-19 10:37:45 +02:00
Bruno Eckhardt a73b2ec13f fixes and new api entpoints for lessons and reports 2026-05-19 10:10:43 +02:00
Bruno Eckhardt dce0a0caed htaccess 2026-05-19 07:17:17 +02:00
Bruno Eckhardt 3d8c4291be fix error msg 2026-05-11 14:16:41 +02:00
Bruno Eckhardt a0afeabb27 maybe hotfix 2026-05-11 13:18:05 +02:00
Bruno Eckhardt 96c27a8722 changes 2026-05-11 13:14:16 +02:00
Bruno Eckhardt 4478f7e82f error logging 2026-05-11 13:09:04 +02:00
Bruno Eckhardt c1f3c3ea4e new function get berufsschulberichte 2026-05-11 12:54:37 +02:00
Bruno Eckhardt 2a8564d3bf caching for api 2026-05-11 12:53:15 +02:00
Tower 14f4143207 fix 2026-04-10 21:43:17 +02:00
68 changed files with 5768 additions and 910 deletions

No files matched your search

+1 -1
View File
@@ -5,7 +5,7 @@
}
],
"settings": {
"workbench.colorTheme": "Default Dark Modern",
"workbench.colorTheme": "Dark Modern",
"workbench.colorCustomizations": {
"activityBar.background": "#ff000075",
"activityBar.foreground": "#000000",
+15 -1
View File
@@ -1,5 +1,19 @@
RewriteEngine On
RewriteRule ^(Commands|config_backups|controller|modules)($|/.*) - [F,L,NC]
RewriteRule ^(Commands|config_backups|controller|modules|uploads)($|/.*) - [F,L,NC]
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
# -----------------------------------
# .php entfernen
# -----------------------------------
# browser requestet datei.php -> redirect zu /datei
RewriteCond %{THE_REQUEST} \s/+(.+?)\.php[\s?] [NC]
RewriteRule ^ /%1 [R=301,L]
# /datei -> datei.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{DOCUMENT_ROOT}/$1.php -f
RewriteRule ^(.+)$ $1.php [L]
+2 -2
View File
@@ -1,7 +1,7 @@
<?php
// loginTest.php
$loginUrl = "https://backend.blackpi.ipv64.net/login.php"; // URL zu deinem Login-Script
$loginUrl = "https://backend.mrhossa.de/login.php"; // URL zu deinem Login-Script
// Testfälle
$tests = [
@@ -51,4 +51,4 @@ foreach ($tests as $test) {
} else {
echo "[FAIL] {$test['description']}\n";
}
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
// reset_einkaufsliste.php
// Aufruf z.B. per Cron: php /pfad/zu/reset_einkaufsliste.php
include_once(__DIR__ . '/../controller/DatabaseController.php');
$result = DatabaseUpdate(
'Einkaufsliste',
'`STATUS` = ?',
'`TYP` = ? AND `STATUS` = ?',
'aktiv', 'vorlage', 'erledigt'
);
echo $result
? "Vorlagen-Artikel wurden zurückgesetzt.\n"
: "Fehler beim Zurücksetzen.\n";
+76
View File
@@ -0,0 +1,76 @@
<?php
include_once(__DIR__ . "/../controller/DatabaseController.php");
include_once(__DIR__ . "/../modules/Mail.php");
date_default_timezone_set("Europe/Berlin");
$jetzt = new DateTime();
$heute = $jetzt->format("Y-m-d");
// Alle heutigen Termine laden
$termine = DatabaseShow(
"Termine",
"*",
"DATUM = ? AND ERINNERUNG_AM IS NULL",
$heute
);
foreach ($termine as $termin) {
$terminZeit = new DateTime($termin["DATUM"] . " " . $termin["ZEIT"]);
$sekundenBisTermin = $terminZeit->getTimestamp() - $jetzt->getTimestamp();
// Nur Termine innerhalb der nächsten 30 Minuten
if ($sekundenBisTermin > 0 && $sekundenBisTermin <= 1800) {
// Benutzer laden
$benutzer = DatabaseShow(
"Benutzer",
"*",
"NAME = ?",
$termin["ERSTELLT_VON"]
);
if (empty($benutzer)) {
continue;
}
$benutzer = $benutzer[0];
if (empty($benutzer["EMAIL"])) {
continue;
}
$subject = "Terminerinnerung";
$message = "Hallo {$benutzer["NAME"]},\n\n";
$message .= "Du hast in weniger als 30 Minuten einen Termin.\n\n";
$message .= "Termin: {$termin["NAME"]}\n";
$message .= "Datum: {$termin["DATUM"]}\n";
$message .= "Uhrzeit: {$termin["ZEIT"]}\n";
if (!empty($termin["NOTIZ"])) {
$notizClean = mb_convert_encoding($termin["NOTIZ"], "UTF-8", "auto");
$message .= "Notiz: {$notizClean}\n";
}
sendMail(
$subject,
$message,
$_SERVER['SCRIPT_NAME'] ?? 'CLI/Unbekannt',
true,
$benutzer["EMAIL"],
);
print_r("{$benutzer["EMAIL"]}\n{$message}");
DatabaseUpdate(
"Termine",
"ERINNERUNG_AM = ?",
"ID = ?",
date('d-m-Y H:i:s'),
$termin["ID"]
);
}
}
+171
View File
@@ -0,0 +1,171 @@
<?php
include_once(__DIR__ . '/../../controller/DatabaseController.php');
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set('display_errors', '0');
$deniedTables = [];
$validOperations = ['=', '!=', '<', '>', '<=', '>=', 'LIKE'];
function isValidEntry(string $name): bool {
return (bool) preg_match('/^[a-zA-Z0-9_]+$/', $name);
}
function PermittedSchemas(array $deniedTables): array {
static $cache = null;
if ($cache !== null) {
return $cache;
}
$alleSchemas = DatabaseShowAllSchemas(); // 1 Query statt N+1
$schema = [];
foreach ($alleSchemas as $table => $columns) {
if (!isValidEntry($table)) {
continue;
}
if (in_array($table, $deniedTables, true)) {
continue;
}
$schema[$table] = $columns;
}
$cache = $schema;
return $schema;
}
$erlaubteSchema = PermittedSchemas($deniedTables);
function isValidTable(string $table, array $schema): void {
if (!isValidEntry($table) || !isset($schema[$table])) {
throw new InvalidArgumentException("Tabelle nicht erlaubt: $table");
}
}
function isValidColumn(string $table, array $columns, array $schema): void {
foreach ($columns as $col) {
if (!isValidEntry($col) || !in_array($col, $schema[$table], true)) {
throw new InvalidArgumentException("Spalte nicht erlaubt: $col");
}
}
}
function buildWhere(array $conditions, string $table, array $schema, array $validOperations): array {
$parts = [];
$values = [];
foreach ($conditions as $cond) {
$col = $cond['column'] ?? '';
$op = $cond['operator'] ?? '=';
$val = $cond['value'] ?? null;
isValidColumn($table, [$col], $schema);
if (!in_array($op, $validOperations, true)) {
throw new InvalidArgumentException("Ungültiger Operator: $op");
}
$parts[] = "`$col` $op ?";
$values[] = $val;
}
return [implode(' AND ', $parts), $values];
}
// Request einlesen
$daten = json_decode(file_get_contents('php://input'), true) ?? [];
$action = $daten['action'] ?? '';
$table = $daten['table'] ?? '';
try {
isValidTable($table, $erlaubteSchema);
switch ($action) {
case 'insert': {
$columns = $daten['columns'] ?? [];
$values = $daten['values'] ?? [];
isValidColumn($table, $columns, $erlaubteSchema);
if (count($columns) !== count($values)) {
throw new InvalidArgumentException('Anzahl der Werte passt nicht zu den Spalten');
}
$columnsSql = implode(', ', array_map(fn($c) => "`$c`", $columns));
$result = DatabaseInsert($table, $columnsSql, ...$values);
echo json_encode(['success' => $result]);
break;
}
case 'show': {
$columns = $daten['columns'] ?? [];
$conditions = $daten['conditions'] ?? [];
if (empty($columns)) {
$columns = $erlaubteSchema[$table];
} else {
isValidColumn($table, $columns, $erlaubteSchema);
}
$columnsSql = implode(', ', array_map(fn($c) => "`$c`", $columns));
[$whereSql, $values] = buildWhere($conditions, $table, $erlaubteSchema, $validOperations);
$result = DatabaseShow($table, $columnsSql, $whereSql, ...$values);
echo json_encode(['success' => true, 'entries' => array_values($result ?? [])]);
break;
}
case 'update': {
$updates = $daten['updates'] ?? [];
$conditions = $daten['conditions'] ?? [];
if (empty($conditions)) {
throw new InvalidArgumentException('WHERE-Bedingung ist bei UPDATE Pflicht');
}
isValidColumn($table, array_keys($updates), $erlaubteSchema);
$setParts = [];
$setValues = [];
foreach ($updates as $col => $val) {
$setParts[] = "`$col` = ?";
$setValues[] = $val;
}
$setSql = implode(', ', $setParts);
[$whereSql, $whereValues] = buildWhere($conditions, $table, $erlaubteSchema, $validOperations);
$result = DatabaseUpdate($table, $setSql, $whereSql, ...array_merge($setValues, $whereValues));
echo json_encode(['success' => $result]);
break;
}
case 'delete': {
$conditions = $daten['conditions'] ?? [];
if (empty($conditions)) {
throw new InvalidArgumentException('WHERE-Bedingung ist bei DELETE Pflicht');
}
[$whereSql, $values] = buildWhere($conditions, $table, $erlaubteSchema, $validOperations);
$result = DatabaseDelete($table, $whereSql, ...$values);
echo json_encode(['success' => $result]);
break;
}
default:
throw new InvalidArgumentException("Unbekannte Aktion: $action");
}
} catch (InvalidArgumentException $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
} catch (Throwable $e) {
http_response_code(500);
error_log($e->getMessage());
echo json_encode(['error' => 'Serverfehler']);
}
@@ -0,0 +1,105 @@
<?php
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => false,
'httponly' => true,
'samesite' => 'Lax'
]);
session_start();
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set('display_errors', '0');
if (empty($_SESSION['user_logged_in'])) {
http_response_code(401);
echo json_encode(['error' => 'Nicht eingeloggt']);
exit;
}
if (empty($_SESSION['is_admin'])) {
http_response_code(403);
echo json_encode(['error' => 'Keine Berechtigung']);
exit;
}
include_once(__DIR__ . '/../../../controller/DatabaseController.php');
include_once(__DIR__ . '/../../../controller/NotifyController.php');
$daten = json_decode(file_get_contents('php://input'), true) ?? [];
$action = $daten['action'] ?? '';
try {
$notificationCtl = new Notification();
switch ($action) {
case 'get_entries': {
$entries = $notificationCtl->getAllMessages();
foreach ($entries as &$entry) {
$entry['GESEHEN_ANZAHL'] = $notificationCtl->countSeenBy((int)$entry['ID']);
}
echo json_encode(['success' => true, 'entries' => array_values($entries)]);
break;
}
case 'add_entry': {
$titel = trim($daten['titel'] ?? '');
$nachricht = trim($daten['nachricht'] ?? '');
if ($titel === '' || $nachricht === '') {
throw new InvalidArgumentException('Titel und Nachricht erforderlich');
}
$notificationCtl->createMessage($titel, $nachricht);
echo json_encode(['success' => true]);
break;
}
case 'edit_entry': {
$id = $daten['id'] ?? null;
$titel = trim($daten['titel'] ?? '');
$nachricht = trim($daten['nachricht'] ?? '');
if (!is_numeric($id)) {
throw new InvalidArgumentException('Ungültige ID');
}
if ($titel === '' || $nachricht === '') {
throw new InvalidArgumentException('Titel und Nachricht erforderlich');
}
$notificationCtl->updateMessage((int)$id, $titel, $nachricht);
echo json_encode(['success' => true]);
break;
}
case 'delete_entry': {
$id = $daten['id'] ?? null;
if (!is_numeric($id)) {
throw new InvalidArgumentException('Ungültige ID');
}
$notificationCtl->deleteMessage((int)$id);
echo json_encode(['success' => true]);
break;
}
default:
throw new InvalidArgumentException("Unbekannte Aktion: $action");
}
} catch (InvalidArgumentException $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
} catch (Throwable $e) {
http_response_code(500);
error_log($e->getMessage());
echo json_encode(['error' => 'Serverfehler']);
}
@@ -0,0 +1,92 @@
<?php
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => false,
'httponly' => true,
'samesite' => 'Lax'
]);
session_start();
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set('display_errors', '0');
if (empty($_SESSION['user_logged_in'])) {
http_response_code(401);
echo json_encode(['error' => 'Nicht eingeloggt']);
exit;
}
include_once(__DIR__ . '/../../../controller/DatabaseController.php');
$UPLOAD_DIR = __DIR__ . '/../../../uploads/';
$daten = json_decode(file_get_contents('php://input'), true) ?? [];
$action = $daten['action'] ?? '';
try {
switch ($action) {
case 'get_entries': {
$entries = DatabaseShow(
"Dateien",
"ID, ORIGINALNAME, GROESSE, HOCHGELADEN_AM",
"HOCHGELADEN_VON = ?",
$_SESSION['username']
);
usort($entries, fn($a, $b) => strtotime($b['HOCHGELADEN_AM']) <=> strtotime($a['HOCHGELADEN_AM']));
echo json_encode(['success' => true, 'entries' => array_values($entries)]);
break;
}
case 'delete_entry': {
$id = $daten['id'] ?? null;
if (!is_numeric($id)) {
throw new InvalidArgumentException('Ungültige ID');
}
// Nur eigene Dateien löschen dürfen
$rows = DatabaseShow(
"Dateien",
"SPEICHERNAME, HOCHGELADEN_VON",
"ID = ?",
(int)$id
);
if (empty($rows)) {
throw new InvalidArgumentException('Datei nicht gefunden');
}
if ($rows[0]['HOCHGELADEN_VON'] !== $_SESSION['username']) {
http_response_code(403);
echo json_encode(['error' => 'Keine Berechtigung']);
exit;
}
$pfad = $UPLOAD_DIR . $rows[0]['SPEICHERNAME'];
if (file_exists($pfad)) {
unlink($pfad);
}
DatabaseDelete("Dateien", "ID = ?", (int)$id);
echo json_encode(['success' => true]);
break;
}
default:
throw new InvalidArgumentException("Unbekannte Aktion: $action");
}
} catch (InvalidArgumentException $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
} catch (Throwable $e) {
http_response_code(500);
error_log($e->getMessage());
echo json_encode(['error' => 'Serverfehler']);
}
@@ -0,0 +1,59 @@
<?php
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => false,
'httponly' => true,
'samesite' => 'Lax'
]);
session_start();
if (empty($_SESSION['user_logged_in'])) {
http_response_code(401);
die('Nicht eingeloggt');
}
include_once(__DIR__ . '/../../../controller/DatabaseController.php');
$UPLOAD_DIR = __DIR__ . '/../../../uploads/';
$id = $_GET['id'] ?? null;
if (!is_numeric($id)) {
http_response_code(400);
die('Ungültige ID');
}
$rows = DatabaseShow(
"Dateien",
"ORIGINALNAME, SPEICHERNAME, MIME_TYPE, HOCHGELADEN_VON",
"ID = ?",
(int)$id
);
if (empty($rows)) {
http_response_code(404);
die('Datei nicht gefunden');
}
$datei = $rows[0];
// Nur eigene Dateien herunterladen dürfen (oder Admin, falls gewünscht - siehe Hinweis unten)
if ($datei['HOCHGELADEN_VON'] !== $_SESSION['username']) {
http_response_code(403);
die('Keine Berechtigung');
}
$pfad = $UPLOAD_DIR . $datei['SPEICHERNAME'];
if (!file_exists($pfad)) {
http_response_code(404);
die('Datei nicht mehr vorhanden');
}
header('Content-Type: ' . $datei['MIME_TYPE']);
header('Content-Disposition: attachment; filename="' . basename($datei['ORIGINALNAME']) . '"');
header('Content-Length: ' . filesize($pfad));
readfile($pfad);
exit;
@@ -0,0 +1,79 @@
<?php
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => false,
'httponly' => true,
'samesite' => 'Lax'
]);
session_start();
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set('display_errors', '0');
if (empty($_SESSION['user_logged_in'])) {
http_response_code(401);
echo json_encode(['error' => 'Nicht eingeloggt']);
exit;
}
include_once(__DIR__ . '/../../../controller/DatabaseController.php');
$UPLOAD_DIR = __DIR__ . '/../../../uploads/';
$MAX_GROESSE = 20 * 1024 * 1024; // 20 MB
$VERBOTENE_ENDUNGEN = ['php', 'phtml', 'php3', 'php4', 'php5', 'phar', 'exe', 'sh', 'bat', 'cgi', 'htaccess'];
try {
if (empty($_FILES['datei'])) {
throw new InvalidArgumentException('Keine Datei übermittelt');
}
$datei = $_FILES['datei'];
if ($datei['error'] !== UPLOAD_ERR_OK) {
throw new InvalidArgumentException('Fehler beim Upload (Code ' . $datei['error'] . ')');
}
if ($datei['size'] > $MAX_GROESSE) {
throw new InvalidArgumentException('Datei zu groß (max. 20 MB)');
}
$originalName = basename($datei['name']); // gegen Path Traversal im Namen
$endung = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (in_array($endung, $VERBOTENE_ENDUNGEN, true)) {
throw new InvalidArgumentException('Dieser Dateityp ist nicht erlaubt');
}
// Zufälliger, kollisionssicherer Speichername - Original bleibt nur in der DB erhalten
$speicherName = bin2hex(random_bytes(16)) . ($endung !== '' ? '.' . $endung : '');
$zielPfad = $UPLOAD_DIR . $speicherName;
if (!move_uploaded_file($datei['tmp_name'], $zielPfad)) {
throw new Exception('Datei konnte nicht gespeichert werden');
}
$mimeType = mime_content_type($zielPfad) ?: 'application/octet-stream';
DatabaseInsert(
"Dateien",
"ORIGINALNAME, SPEICHERNAME, GROESSE, MIME_TYPE, HOCHGELADEN_VON",
$originalName,
$speicherName,
$datei['size'],
$mimeType,
$_SESSION['username']
);
echo json_encode(['success' => true]);
} catch (InvalidArgumentException $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
} catch (Throwable $e) {
http_response_code(500);
error_log($e->getMessage());
echo json_encode(['error' => 'Serverfehler']);
}
@@ -0,0 +1,55 @@
<?php
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => false,
'httponly' => true,
'samesite' => 'Lax'
]);
session_start();
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set('display_errors', '0');
if (empty($_SESSION['user_logged_in'])) {
http_response_code(401);
echo json_encode(['error' => 'Nicht eingeloggt']);
exit;
}
include_once(__DIR__ . '/../../../controller/DatabaseController.php');
include_once(__DIR__ . '/../../../controller/NotifyController.php');
$daten = json_decode(file_get_contents('php://input'), true) ?? [];
$action = $daten['action'] ?? '';
try {
switch ($action) {
case 'mark_seen': {
$nachrichtId = $daten['nachricht_id'] ?? null;
if (!is_numeric($nachrichtId)) {
throw new InvalidArgumentException('Ungültige Nachrichten-ID');
}
$notificationCtl = new Notification();
$notificationCtl->markAsSeen((int)$nachrichtId);
echo json_encode(['success' => true]);
break;
}
default:
throw new InvalidArgumentException("Unbekannte Aktion: $action");
}
} catch (InvalidArgumentException $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
} catch (Throwable $e) {
http_response_code(500);
error_log($e->getMessage());
echo json_encode(['error' => 'Serverfehler']);
}
@@ -0,0 +1,138 @@
<?php
session_start();
include_once(__DIR__ . '/../../../controller/DatabaseController.php');
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set('display_errors', '0');
$daten = json_decode(file_get_contents('php://input'), true) ?? [];
$action = $daten['action'] ?? '';
$username = $_SESSION['username'] ?? 'Gast';
$erlaubteIntervalle = ['daily' => '+1 day', 'weekly' => '+1 week', 'monthly' => '+1 month'];
try {
switch ($action) {
// ---------- Termine laden (nur die eigenen, serienweise gruppiert) ----------
case 'get_entries': {
$termine = DatabaseShow(
"Termine",
"MAX(ID) as ID, MIN(DATUM) as DATUM, ZEIT, NAME, NOTIZ, ERSTELLT_VON, SERIEN_ID, INTERVALL, SERIEN_ENDE",
"ERSTELLT_VON = ? GROUP BY IFNULL(SERIEN_ID, ID) ORDER BY DATUM ASC, ZEIT ASC",
$username
);
echo json_encode(['success' => true, 'entries' => array_values($termine ?? [])]);
break;
}
// ---------- Neuen Termin (einzeln oder als Serie) anlegen ----------
case 'add_termin': {
$datum = trim($daten['datum'] ?? '');
$zeit = trim($daten['zeit'] ?? '');
$name = trim($daten['name'] ?? '');
$notiz = trim($daten['notiz'] ?? '');
$wiederholung = $daten['wiederholung'] ?? 'none';
$enddatum = trim($daten['enddatum'] ?? '');
if ($datum === '' || $zeit === '' || $name === '') {
throw new InvalidArgumentException('Bitte alle Pflichtfelder ausfüllen.');
}
// ----- Serientermin -----
if ($wiederholung !== 'none' && $enddatum !== '') {
if (!isset($erlaubteIntervalle[$wiederholung])) {
throw new InvalidArgumentException('Ungültiges Wiederholungsintervall.');
}
$currentDate = new DateTime($datum);
$endDate = new DateTime($enddatum);
if ($endDate < $currentDate) {
throw new InvalidArgumentException('Das Enddatum darf nicht vor dem Startdatum liegen.');
}
$modifier = $erlaubteIntervalle[$wiederholung];
$serienId = uniqid('serie_', true);
$insertedCount = 0;
while ($currentDate <= $endDate) {
DatabaseInsert(
"Termine",
"DATUM, ZEIT, NAME, NOTIZ, ERSTELLT_VON, SERIEN_ID, INTERVALL, SERIEN_ENDE",
$currentDate->format('Y-m-d'),
$zeit,
$name,
$notiz,
$username,
$serienId,
$wiederholung,
$enddatum
);
$insertedCount++;
$currentDate->modify($modifier);
}
echo json_encode(['success' => true, 'message' => "Serientermin ($insertedCount Termine) erfolgreich eingetragen!"]);
break;
}
// ----- Einzeltermin -----
DatabaseInsert(
"Termine",
"DATUM, ZEIT, NAME, NOTIZ, ERSTELLT_VON",
$datum,
$zeit,
$name,
$notiz,
$username
);
echo json_encode(['success' => true, 'message' => 'Termin erfolgreich eingetragen!']);
break;
}
// ---------- Einzelnen Termin löschen ----------
case 'delete_termin': {
$id = $daten['id'] ?? null;
if (!is_numeric($id)) {
throw new InvalidArgumentException('Ungültige ID');
}
// Zusätzlich ERSTELLT_VON prüfen, damit niemand fremde Termine per ID löschen kann
DatabaseDelete("Termine", "ID = ? AND ERSTELLT_VON = ?", (int)$id, $username);
echo json_encode(['success' => true, 'message' => 'Termin erfolgreich gelöscht!']);
break;
}
// ---------- Komplette Serie löschen ----------
case 'delete_serie': {
$serienId = trim($daten['serienId'] ?? '');
if ($serienId === '') {
throw new InvalidArgumentException('Ungültige Serien-ID');
}
DatabaseDelete("Termine", "SERIEN_ID = ? AND ERSTELLT_VON = ?", $serienId, $username);
echo json_encode(['success' => true, 'message' => 'Komplette Terminserie erfolgreich gelöscht!']);
break;
}
default:
throw new InvalidArgumentException("Unbekannte Aktion: $action");
}
} catch (InvalidArgumentException $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
} catch (Throwable $e) {
http_response_code(500);
error_log($e->getMessage());
echo json_encode(['error' => 'Serverfehler']);
}
@@ -0,0 +1,88 @@
<?php
session_start();
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set('display_errors', '0');
include_once(__DIR__ . '/../../../controller/DatabaseController.php');
include_once(__DIR__ . '/../../../controller/RoutesController.php');
include_once(__DIR__ . '/../../../modules/Mail.php');
$daten = json_decode(file_get_contents('php://input'), true) ?? [];
$action = $daten['action'] ?? '';
try {
switch ($action) {
// ---------- Liste laden (nur eigene Tickets) ----------
case 'get_entries': {
$entries = DatabaseShow(
"Tickets",
"ID, TITEL, BESCHREIBUNG, STATUS",
"ERSTELLER = ?",
((new getSpecificData())->getUsername()),
);
echo json_encode(['success' => true, 'entries' => array_values($entries ?? [])]);
break;
}
// ---------- Neues Ticket anlegen ----------
case 'add_entry': {
foreach (['titel', 'beschreibung'] as $feld) {
if (empty($daten[$feld])) {
throw new InvalidArgumentException("Pflichtfeld fehlt: $feld");
}
}
$titel = $daten['titel'];
$beschreibung = $daten['beschreibung'];
$user = ucfirst(strtolower((new getSpecificData())->getUsername()));
DatabaseInsert(
"Tickets",
"TITEL, BESCHREIBUNG, STATUS, ERSTELLER",
$titel,
$beschreibung,
"OFFEN",
$user,
);
sendMail(
"Neues Ticket von $user",
"Titel: $titel\nBeschreibung: $beschreibung",
(new getSpecificData())->getRequestURL(),
true,
);
echo json_encode(['success' => true]);
break;
}
// ---------- Ticket löschen ----------
case 'delete_entry': {
$id = $daten['id'] ?? null;
if (!is_numeric($id)) {
throw new InvalidArgumentException('Ungültige ID');
}
DatabaseDelete("Tickets", "ID = ?", (int)$id);
echo json_encode(['success' => true]);
break;
}
default:
throw new InvalidArgumentException("Unbekannte Aktion: $action");
}
} catch (InvalidArgumentException $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
} catch (Throwable $e) {
http_response_code(500);
error_log($e->getMessage());
echo json_encode(['error' => 'Serverfehler']);
}
@@ -0,0 +1,94 @@
<?php
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => false,
'httponly' => true,
'samesite' => 'Lax'
]);
session_start();
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set('display_errors', '0');
if (empty($_SESSION['user_logged_in'])) {
http_response_code(401);
echo json_encode(['error' => 'Nicht eingeloggt']);
exit;
}
include_once(__DIR__ . '/../../../controller/DatabaseController.php');
include_once(__DIR__ . '/../../../controller/RoutesController.php');
include_once(__DIR__ . '/../../../modules/Mail.php');
$allowedStatuses = ['OFFEN', 'DEPLOYED', 'ABGELEHNT', 'ANGENOMMEN'];
$daten = json_decode(file_get_contents('php://input'), true) ?? [];
$action = $daten['action'] ?? '';
try {
switch ($action) {
// ---------- Alle Tickets laden (Verwaltungsansicht) ----------
case 'get_entries': {
$entries = DatabaseShow(
"Tickets",
"ID, TITEL, BESCHREIBUNG, STATUS, ERSTELLER",
);
echo json_encode(['success' => true, 'entries' => array_values($entries ?? [])]);
break;
}
// ---------- Status ändern ----------
case 'edit_entry': {
$id = $daten['id'] ?? null;
$status = $daten['status'] ?? '';
if (!is_numeric($id)) {
throw new InvalidArgumentException('Ungültige ID');
}
if (!in_array($status, $allowedStatuses, true)) {
throw new InvalidArgumentException('Ungültiger Status');
}
DatabaseUpdate(
"Tickets",
"STATUS = ?",
"ID = ?",
$status,
(int)$id
);
echo json_encode(['success' => true]);
break;
}
// ---------- Ticket löschen ----------
case 'delete_entry': {
$id = $daten['id'] ?? null;
if (!is_numeric($id)) {
throw new InvalidArgumentException('Ungültige ID');
}
DatabaseDelete("Tickets", "ID = ?", (int)$id);
echo json_encode(['success' => true]);
break;
}
default:
throw new InvalidArgumentException("Unbekannte Aktion: $action");
}
} catch (InvalidArgumentException $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
} catch (Throwable $e) {
http_response_code(500);
error_log($e->getMessage());
echo json_encode(['error' => 'Serverfehler']);
}
@@ -0,0 +1,113 @@
<?php
include_once(__DIR__ . '/../../../controller/DatabaseController.php');
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set('display_errors', '0');
$daten = json_decode(file_get_contents('php://input'), true) ?? [];
$action = $daten['action'] ?? '';
/**
* Baut die WHERE-Klausel serverseitig. STATUS IS NULL ist FEST verdrahtet
* und niemals durch den Client beeinflussbar -- das ist die eigentliche
* Geschäftsregel dieser Seite (nur "aktive" Vergaben werden angezeigt).
*/
function baueVergabeWhere(string $searchWer, string $searchWas, array &$values): string {
$parts = ["STATUS IS NULL"];
if ($searchWer !== '') {
$parts[] = "WER LIKE ?";
$values[] = '%' . $searchWer . '%';
}
if ($searchWas !== '') {
$parts[] = "WAS LIKE ?";
$values[] = '%' . $searchWas . '%';
}
return implode(' AND ', $parts);
}
try {
switch ($action) {
// ---------- Liste laden (mit optionalem Filter) ----------
case 'get_entries': {
$searchWer = trim($daten['searchWer'] ?? '');
$searchWas = trim($daten['searchWas'] ?? '');
$values = [];
$where = baueVergabeWhere($searchWer, $searchWas, $values);
$entries = DatabaseShow("Vergabe", "ID, WAS, WER, WANN, RUECKGABE", $where, ...$values);
echo json_encode(['success' => true, 'entries' => array_values($entries ?? [])]);
break;
}
// ---------- Autocomplete-Vorschläge (alle bekannten WAS/WER, auch zurückgegebene) ----------
case 'get_suggestions': {
$alle = DatabaseShow("Vergabe");
$was = [];
$wer = [];
foreach ($alle as $entry) {
if (!empty($entry['WAS'])) $was[$entry['WAS']] = true;
if (!empty($entry['WER'])) $wer[$entry['WER']] = true;
}
echo json_encode([
'success' => true,
'was' => array_keys($was),
'wer' => array_keys($wer),
]);
break;
}
// ---------- Neue Vergabe anlegen ----------
case 'add_entry': {
foreach (['was', 'wer', 'wann', 'rueckgabe'] as $feld) {
if (empty($daten[$feld])) {
throw new InvalidArgumentException("Pflichtfeld fehlt: $feld");
}
}
DatabaseInsert(
"Vergabe",
"WAS, WER, WANN, RUECKGABE",
$daten['was'],
$daten['wer'],
$daten['wann'],
$daten['rueckgabe']
);
echo json_encode(['success' => true]);
break;
}
// ---------- Vergabe als zurückgegeben markieren (Soft-Delete) ----------
case 'return_entry': {
$id = $daten['id'] ?? null;
if (!is_numeric($id)) {
throw new InvalidArgumentException('Ungültige ID');
}
DatabaseUpdate("Vergabe", "STATUS = ?", "ID = ?", 1, (int)$id);
echo json_encode(['success' => true]);
break;
}
default:
throw new InvalidArgumentException("Unbekannte Aktion: $action");
}
} catch (InvalidArgumentException $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
} catch (Throwable $e) {
http_response_code(500);
error_log($e->getMessage());
echo json_encode(['error' => 'Serverfehler']);
}
@@ -0,0 +1,90 @@
<?php
session_start();
include_once(__DIR__ . '/../../../controller/DatabaseController.php');
header('Content-Type: application/json');
error_reporting(E_ALL);
ini_set('display_errors', '0');
$daten = json_decode(file_get_contents('php://input'), true) ?? [];
$action = $daten['action'] ?? '';
$erlaubteGruende = ['Abbau', 'Aufbau', 'Anderes'];
function istBerechtigt(): bool {
$username = $_SESSION['username'] ?? '';
return strtolower($username) === 'bruno';
}
try {
switch ($action) {
case 'get_stats': {
include_once(__DIR__ . '/../../../controller/DatabaseConnect.php');
$conn = db_connect();
$minuten_berichte = 0;
$res1 = $conn->query("SELECT SUM(OVERTIME) AS summe FROM Firmenberichte");
if ($row = $res1->fetch_object()) { $minuten_berichte = intval($row->summe); }
$minuten_abbau = 0;
$res2 = $conn->query("SELECT SUM(MINUTEN) AS summe FROM Ueberstunden WHERE GRUND = 'Abbau'");
if ($row = $res2->fetch_object()) { $minuten_abbau = intval($row->summe); }
$minuten_aufbau = 0;
$res3 = $conn->query("SELECT SUM(MINUTEN) AS summe FROM Ueberstunden WHERE GRUND = 'Aufbau'");
if ($row = $res3->fetch_object()) { $minuten_aufbau = intval($row->summe); }
$gesamt_minuten = $minuten_berichte - $minuten_abbau + $minuten_aufbau;
echo json_encode([
'success' => true,
'gesamtMinuten' => $gesamt_minuten,
'istBerechtigt' => istBerechtigt(),
]);
break;
}
case 'add_overtime': {
if (!istBerechtigt()) {
http_response_code(403);
echo json_encode(['error' => 'Keine Berechtigung für diese Aktion']);
exit;
}
$datum = trim($daten['datum'] ?? '');
$minuten = $daten['minuten'] ?? null;
$grund = trim($daten['grund'] ?? '');
if ($datum === '' || !is_numeric($minuten) || $grund === '') {
throw new InvalidArgumentException('Bitte alle Pflichtfelder korrekt ausfüllen.');
}
if (!in_array($grund, $erlaubteGruende, true)) {
throw new InvalidArgumentException('Ungültiger Grund.');
}
DatabaseInsert(
"Ueberstunden",
"DATUM, MINUTEN, GRUND",
$datum,
intval($minuten),
$grund
);
echo json_encode(['success' => true, 'message' => 'Eintrag erfolgreich gespeichert!']);
break;
}
default:
throw new InvalidArgumentException("Unbekannte Aktion: $action");
}
} catch (InvalidArgumentException $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
} catch (Throwable $e) {
http_response_code(500);
error_log($e->getMessage());
echo json_encode(['error' => 'Serverfehler']);
}
+38 -8
View File
@@ -1,7 +1,8 @@
<?php
include_once '../controller/DatabaseController.php';
include_once '../controller/DataController.php';
include_once __DIR__ . '/../modules/Mail.php';
include_once( __DIR__ . '/../../controller/DatabaseController.php');
include_once( __DIR__ . '/../../controller/DataController.php');
include_once( __DIR__ . '/../../controller/ApiController.php');
include_once(__DIR__ . '/../../modules/Mail.php');
header("Content-Type: application/json");
header("Access-Control-Allow-Origin: *");
@@ -13,9 +14,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit();
}
$validTokens = [
"15a437d128c918633e57dc7d18118a073b807644f21731ed5a7ebcf3eca9e242",
];
$validTokens = DatabaseShow(
"API_Credentials",
"TOKEN"
);
$validTokens = array_column($validTokens, 'TOKEN');
$headers = getallheaders();
$token = $headers['Authorization'] ?? $headers['authorization'] ?? $_SERVER['HTTP_AUTHORIZATION'] ?? '';
@@ -28,18 +32,37 @@ if (!in_array($token, $validTokens)) {
"debug_received_token" => $token,
"debug_all_headers" => $headers
]);
DatabaseInsert(
"API_LOGS",
"CLIENT_IP, TOKEN, API, FUNKTION, JSON",
(new getSpecificData())->getClientIP(),
$token,
(new getSpecificData())->getRequestURL(),
$function,
$error
);
errorMail(
"API Login failed",
$error,
true,
);
echo $error;
echo json_encode(
[
"error" => "Unauthorized",
"Contact" => "bruno.eckhardt07@gmail.com",
]
);
exit;
}
$data = null;
if (in_array($token, $validTokens)) {
DatabaseInsert(
"API",
"API_LOGS",
"CLIENT_IP, TOKEN, API, FUNKTION",
(new getSpecificData())->getClientIP(),
$token,
@@ -60,6 +83,13 @@ if (in_array($token, $validTokens)) {
"MINUTEN"
);
}
if ($function === "Berichte") {
$data = DatabaseShowWithFK(
"Berufsschulberichte",
$Berufsschulberichte,
);
}
echo json_encode(
[
@@ -1,6 +1,6 @@
<?php
include_once '../controller/DatabaseController.php';
include_once '../controller/DataController.php';
include_once( __DIR__ . '/../../controller/DatabaseController.php' );
include_once( __DIR__ . '/../../controller/DataController.php' );
header("Content-Type: application/json");
header("Access-Control-Allow-Origin: *");
@@ -110,4 +110,4 @@ DatabaseInsert(
$json
);
echo $json;
echo $json;
+37
View File
@@ -0,0 +1,37 @@
<?php
include_once(__DIR__ . '/../../controller/DatabaseController.php');
include_once(__DIR__ . '/../../controller/DataController.php');
include_once(__DIR__ . '/../../controller/ApiController.php');
header('Content-Type: application/json');
$api = (new ApiController());
if ($api->urlAuth($_GET['token']) == true ) {
if (isset($_GET['day'])) {
$day = $_GET['day'];
$data = DatabaseShowWithFK(
"Stundenplan",
$Stundenplan,
"ZEITEN, $day",
"$day IS NOT NULL AND $day != '' AND $day != '(NULL)' ORDER BY ZEITEN ASC"
);
} else {
$data = DatabaseShowWithFK(
"Stundenplan",
$Stundenplan,
"ZEITEN, MO, DI, MO, DO, FR",
);
}
echo $api->jsonSuccess($data);
$api->requestSave();
}
/**
* http://localhost:9000/api/v2/lessons.php?token=f3a50684d7f0cc2d9b21601798ea48242fab7481a0d0407a2b8683b0b74a79e3
* http://localhost:9000/api/v2/lessons.php?token=f3a50684d7f0cc2d9b21601798ea48242fab7481a0d0407a2b8683b0b74a79e3&day=FR
*/
+37
View File
@@ -0,0 +1,37 @@
<?php
include_once(__DIR__ . '/../../controller/DatabaseController.php');
include_once(__DIR__ . '/../../controller/DataController.php');
include_once(__DIR__ . '/../../controller/ApiController.php');
header('Content-Type: application/json');
$api = (new ApiController());
if ($api->urlAuth($_GET['token']) == true ) {
if (isset($_GET['date'])) {
$date = $_GET['date'];
$data = DatabaseShowWithFK(
"Berufsschulberichte",
$Berufsschulberichte,
"*",
"DATUM = ?",
$date
);
} else {
$data = DatabaseShowWithFK(
"Berufsschulberichte",
$Berufsschulberichte,
);
}
echo $api->jsonSuccess($data);
$api->requestSave();
}
/**
* http://localhost:9000/api/v2/schoolReports.php?token=f3a50684d7f0cc2d9b21601798ea48242fab7481a0d0407a2b8683b0b74a79e3
* http://localhost:9000/api/v2/schoolReports.php?token=f3a50684d7f0cc2d9b21601798ea48242fab7481a0d0407a2b8683b0b74a79e3&date=2025-09-09
*/
+36
View File
@@ -0,0 +1,36 @@
<?php
include_once(__DIR__ . '/../../controller/DatabaseController.php');
include_once(__DIR__ . '/../../controller/DataController.php');
include_once(__DIR__ . '/../../controller/ApiController.php');
header('Content-Type: application/json');
$api = (new ApiController());
if ($api->urlAuth($_GET['token']) == true ) {
if (isset($_GET['date'])) {
$date = $_GET['date'];
$data = DatabaseShow(
"Firmenberichte",
"DATUM, PROZESS_1, PROZESS_2, PROZESS_3, PROZESS_4, BEGINN, ENDE_PUBLIC",
"DATUM = ?",
$date
);
} else {
$data = DatabaseShow(
"Firmenberichte",
"DATUM, PROZESS_1, PROZESS_2, PROZESS_3, PROZESS_4, BEGINN, ENDE_PUBLIC",
);
}
echo $api->jsonSuccess($data);
$api->requestSave();
}
/**
* http://localhost:9000/api/v2/workReports.php?token=f3a50684d7f0cc2d9b21601798ea48242fab7481a0d0407a2b8683b0b74a79e3
* http://localhost:9000/api/v2/workReports.php?token=f3a50684d7f0cc2d9b21601798ea48242fab7481a0d0407a2b8683b0b74a79e3&date=2025-09-09
*/
-369
View File
@@ -6,373 +6,4 @@ $superuser = array (
'password' => 'bBHofurUJWKzPE8Z:',
'active' => true,
),
);
$serviceGroups = array (
'Info' =>
array (
'label' => 'Home',
'defaultOpen' => true,
'services' =>
array (
0 => 'home_info',
1 => 'links',
),
),
'Ausbildung' =>
array (
'label' => 'Ausbildung',
'defaultOpen' => true,
'services' =>
array (
0 => 'print_static',
1 => 'zeiterfassung',
2 => 'berichte_arbeit_gen',
3 => 'berichte',
),
),
'Logs' =>
array (
'label' => 'Webserver',
'defaultOpen' => true,
'services' =>
array (
0 => 'public_request_logs',
1 => 'public_request_chart',
2 => 'backend_request_logs',
3 => 'backend_request_chart',
4 => 'personal_request_logs',
),
),
'System' =>
array (
'label' => 'Verwaltung',
'defaultOpen' => false,
'services' =>
array (
0 => 'data_info',
1 => 'services_control',
2 => 'user_control',
3 => 'variablen_control',
),
),
'Test' =>
array (
'label' => 'Dev / Test',
'defaultOpen' => false,
'services' =>
array (
0 => 'api_tester',
1 => 'weather_chart',
2 => 'tabellen',
3 => 'domains',
),
),
);
$services = array (
'home_info' =>
array (
'name' => 'Home',
'active' => true,
'adminOnly' => false,
),
'print_static' =>
array (
'name' => 'Berichte Drucken',
'active' => true,
'adminOnly' => false,
),
'api_tester' =>
array (
'name' => 'API Testing',
'active' => true,
'adminOnly' => true,
),
'weather_chart' =>
array (
'name' => 'Wetter Statistik',
'active' => true,
'adminOnly' => true,
),
'data_info' =>
array (
'name' => 'Data Info',
'active' => true,
'adminOnly' => true,
),
'links' =>
array (
'name' => 'Links',
'active' => true,
'adminOnly' => false,
),
'public_request_logs' =>
array (
'name' => 'Public Logs',
'active' => true,
'adminOnly' => false,
),
'public_request_chart' =>
array (
'name' => 'Public Statistik',
'active' => true,
'adminOnly' => false,
),
'backend_request_logs' =>
array (
'name' => 'Backend Logs',
'active' => true,
'adminOnly' => false,
),
'backend_request_chart' =>
array (
'name' => 'Backend Statistik',
'active' => true,
'adminOnly' => false,
),
'personal_request_logs' =>
array (
'name' => 'Persönliche Logs',
'active' => true,
'adminOnly' => true,
),
'services_control' =>
array (
'name' => 'Seiten',
'active' => true,
'adminOnly' => true,
),
'zeiterfassung' =>
array (
'name' => 'Zeit Erfassung',
'active' => true,
'adminOnly' => false,
),
'berichte_arbeit_gen' =>
array (
'name' => 'Berichte PDF',
'active' => true,
'adminOnly' => false,
),
'user_control' =>
array (
'name' => 'Benutzer',
'active' => true,
'adminOnly' => true,
),
'variablen_control' =>
array (
'name' => 'Variablen',
'active' => true,
'adminOnly' => true,
),
'tabellen' =>
array (
'name' => 'Tabellen',
'active' => true,
'adminOnly' => true,
),
'berichte' =>
array (
'name' => 'Berichte Einsehen',
'active' => true,
'adminOnly' => false,
),
'domains' =>
array (
'name' => 'Domains',
'active' => true,
'adminOnly' => false,
),
);
$linkGroups = array (
'info' =>
array (
'label' => 'Informationen',
'color' => '#3498db',
'defaultOpen' => false,
),
'tools' =>
array (
'label' => 'IoT',
'color' => '#2ecc71',
'defaultOpen' => false,
),
'admin' =>
array (
'label' => 'Administration',
'color' => '#e74c3c',
'defaultOpen' => false,
),
'devices' =>
array (
'label' => 'Geräte',
'color' => '#b7db33',
'defaultOpen' => false,
),
);
$linkButtons = array (
0 =>
array (
'label' => 'Gitea',
'url' => 'http://gitea.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'admin',
),
1 =>
array (
'label' => 'HAProxy',
'url' => 'http://stats.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'admin',
),
2 =>
array (
'label' => 'Public',
'url' => 'https://blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'info',
),
3 =>
array (
'label' => 'NAS-1',
'url' => 'http://nas-1.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'devices',
),
4 =>
array (
'label' => 'NAS-2',
'url' => 'http://nas-2.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'devices',
),
5 =>
array (
'label' => 'FritzBox-1',
'url' => 'http://fritzbox-1.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'devices',
),
6 =>
array (
'label' => 'FritzBox-2',
'url' => 'http://fritzbox-2.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'devices',
),
7 =>
array (
'label' => 'GS1910-24',
'url' => 'http://switch.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'admin',
),
8 =>
array (
'label' => 'NWA5121-NI Innen',
'url' => 'http://ap-1.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'devices',
),
9 =>
array (
'label' => 'NWA5121-NI Aussen',
'url' => 'http://ap-2.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'devices',
),
10 =>
array (
'label' => 'ZyWALL 110',
'url' => 'http://10.233.1248',
'newTab' => true,
'adminOnly' => false,
'group' => 'admin',
),
11 =>
array (
'label' => 'Ender 3 V3 KE',
'url' => 'http://10.233.1.52',
'newTab' => true,
'adminOnly' => false,
'group' => 'tools',
),
12 =>
array (
'label' => 'Ubuntu NAS',
'url' => 'http://10.233.1.32:9000',
'newTab' => true,
'adminOnly' => false,
'group' => 'tools',
),
13 =>
array (
'label' => 'Backend',
'url' => 'https://backend.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'info',
),
14 =>
array (
'label' => 'Dev Backend',
'url' => 'https://dev-backend.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'info',
),
15 =>
array (
'label' => 'Dev Pubic',
'url' => 'https://dev-public.blackpi.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'info',
),
16 =>
array (
'label' => 'Local Backend',
'url' => 'http://backend.local.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'info',
),
17 =>
array (
'label' => 'Local Pubic',
'url' => 'http://public.local.ipv64.net',
'newTab' => true,
'adminOnly' => false,
'group' => 'info',
),
18 =>
array (
'label' => 'Proxmox',
'url' => 'https://100.88.208.86:8006',
'newTab' => true,
'adminOnly' => true,
'group' => 'devices',
),
19 =>
array (
'label' => 'Flatnotes',
'url' => 'http://10.233.1.130:8080',
'newTab' => true,
'adminOnly' => false,
'group' => 'tools',
),
);
+139
View File
@@ -0,0 +1,139 @@
<?php
include_once(__DIR__ . '/DatabaseController.php');
class ApiController
{
/**
* method for pretty jsonsuccess output
*
* @param array $data
* @return string
*/
public function jsonSuccess($data)
{
return json_encode(
[
'status' => 'success',
'data' => $data,
],
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE
);
}
/**
* method for pretty jsonerror output
*
* @param array $error
* @return string
*/
public function jsonError($error)
{
echo json_encode(
[
'status' => 'failed',
'data' => 'none',
'error' => $error
],
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE
);
}
/**
* get authentication key out database
*
* @return array
*/
private function getAuthKey()
{
$validTokens = DatabaseShow(
"API_Credentials",
"TOKEN"
);
return array_column($validTokens, 'TOKEN');
}
/**
* url authentication method via token and optional parameter function
*
* @param string $token
* @param string $function
* @return void
*/
public function urlAuth($token, $function = '')
{
$validTokens = $this->getAuthKey();
if (!in_array($token, $validTokens)) {
http_response_code(401);
$error = json_encode([
"error" => "Unauthorized",
"debug_received_token" => $token,
]);
echo $this->jsonError($error);
DatabaseInsert(
"API_LOGS",
"CLIENT_IP, TOKEN, API, FUNKTION, JSON",
(new getSpecificData())->getClientIP(),
$token,
(new getSpecificData())->getRequestURL(),
$function,
$error
);
errorMail(
"API Login failed",
$error,
true,
);
return false;
}
return true;
}
public function requestSave($function = '')
{
DatabaseInsert(
"API_LOGS",
"CLIENT_IP, TOKEN, API, FUNKTION, JSON",
(new getSpecificData())->getClientIP(),
$_GET['token'],
(new getSpecificData())->getRequestURL(),
$function,
'{"status": "success"}',
);
}
/**
* create new api token
*
*/
private function createToken()
{
return bin2hex(random_bytes(32));
}
/**
* save created api token
*
* @return bool
*/
public function saveToken()
{
try {
DatabaseInsert(
"API_Credentials",
"TOKEN, BENUTZER",
$this->createToken(),
"API-User"
);
} catch (Exception $error) {
return false;
}
return true;
}
}
+13 -35
View File
@@ -3,7 +3,7 @@
class getSpecificData {
public function getWeather() {
$url = "https://api.openweathermap.org/data/2.5/weather?q=Berlin,DE&APPID=5bc3e1a438c58fca444c4370ac0517b2&units=metric";
$url = "https://api.openweathermap.org/data/2.5/weather?q=Jena,DE&APPID=5bc3e1a438c58fca444c4370ac0517b2&units=metric";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
@@ -108,25 +108,23 @@ class getSpecificData {
}
public function getServerIP(): string {
$ServerIP = $_SERVER['SERVER_ADDR'] ?? 'Unbekannt';
if (in_array($ServerIP, ['Unbekannt', '0.0.0.0', '127.0.0.1'])) {
return "Lokal";
} else {
return $ServerIP;
}
return $_SERVER['SERVER_ADDR'] ?? 'Unbekannt';
}
public function getClientIP(): string {
public function getClientIP(): string
{
if (PHP_SAPI === 'cli') {
return 'CLI';
}
$xff = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? null;
if ($xff) {
if (!empty($xff)) {
$ips = array_map('trim', explode(',', $xff));
$clientIp = $ips[0];
return $clientIp;
} else {
return $_SERVER['REMOTE_ADDR'];
return $ips[0];
}
return $_SERVER['REMOTE_ADDR'] ?? 'UNKNOWN';
}
public function getRequestTime(): string {
@@ -162,27 +160,7 @@ class getSpecificData {
}
function getServerNode($ServerIP) {
$nodes = [
'node1' => '172.30.0.11',
'node1' => '172.30.0.21',
'node2' => '172.30.0.12',
'node2' => '172.30.0.22',
'node3' => '172.30.0.13',
'node3' => '172.30.0.23',
'node4' => '172.30.0.14',
'node4' => '172.30.0.24',
'Lokal' => 'Lokal',
];
foreach ($nodes as $node => $ip) {
if ($ServerIP == $ip) {
return $node;
break;
}
}
return $ServerIP;
}
function getClientStatus($ClientIP) {
+9 -29
View File
@@ -4,7 +4,8 @@ include_once __DIR__ . '/../modules/Mail.php';
include_once __DIR__ . '/../controller/RoutesController.php';
include_once 'DataController.php';
function db_connect() {
function db_connect()
{
$servers = [
'local' => [
'hostaddr' => '127.0.0.1',
@@ -12,39 +13,21 @@ function db_connect() {
'password' => 'bBHofurUJWKzPE8Z',
'database' => 'DEV',
],
'vpn' => [
'hostaddr' => '100.123.136.86',
'live' => [
'hostaddr' => '152.53.159.169',
'username' => 'worker',
'password' => 'bBHofurUJWKzPE8Z',
'database' => 'DEV',
],
'backup' => [
'hostaddr' => '10.233.1.241',
'username' => 'worker',
'password' => 'bBHofurUJWKzPE8Z',
'database' => 'DEV',
'database' => 'LIVE',
],
'dev' => [
'hostaddr' => '10.233.1.243',
'hostaddr' => '152.53.159.169',
'username' => 'worker',
'password' => 'bBHofurUJWKzPE8Z',
'database' => 'DEV',
],
'live' => [
'hostaddr' => '10.233.1.244',
'username' => 'worker',
'password' => 'bBHofurUJWKzPE8Z',
'database' => 'DEV',
],
'error' => [
'hostaddr' => '10.233.1.244',
'username' => 'error',
'password' => 'bBHofurUJWKzPE8Z',
'database' => 'DEV',
]
];
$enviroment = 'vpn';
$enviroment = 'dev';
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
@@ -59,7 +42,6 @@ function db_connect() {
$conn->set_charset("utf8mb4");
return $conn;
} catch (mysqli_sql_exception $e) {
errorMail(
@@ -67,9 +49,7 @@ function db_connect() {
$e->getMessage(),
false,
);
redirectToWebsite("https://blackpi.ipv64.net");
redirectToWebsite("https://mrhossa.de");
throw $e;
}
}
?>
}
+67 -18
View File
@@ -147,10 +147,37 @@ function DatabaseUpdate($table, $set, $where, ...$values) {
return true;
}
function getFKValue($conn, $table, $key, $value, $id, &$cache) {
if ($id === null) return null;
$cacheKey = $table . "_" . $id;
if (isset($cache[$cacheKey])) {
return $cache[$cacheKey];
}
$stmt = $conn->prepare("SELECT `$value` FROM `$table` WHERE `$key` = ? LIMIT 1");
if (!$stmt) {
die("FK Prepare Fehler: " . $conn->error);
}
$stmt->bind_param("i", $id);
$stmt->execute();
$result = $stmt->get_result()->fetch_assoc();
$valueResult = $result[$value] ?? null;
$cache[$cacheKey] = $valueResult;
return $valueResult;
}
function DatabaseShowWithFK($table, $fkMap = [], $columns = "*", $where = "", ...$values) {
include_once 'DatabaseConnect.php';
$conn = db_connect();
$cache = [];
$select = is_array($columns) ? implode(", ", $columns) : $columns;
$sql = "SELECT $select FROM `$table`";
@@ -161,33 +188,32 @@ function DatabaseShowWithFK($table, $fkMap = [], $columns = "*", $where = "", ..
$stmt = $conn->prepare($sql);
if (!$stmt) die("Fehler beim Prepare: " . $conn->error);
if (!$stmt->execute($values)) die("Fehler beim Execute: " . $stmt->error);
if (!empty($values)) {
$stmt->execute($values);
} else {
$stmt->execute();
}
$result = $stmt->get_result();
if (!$result) die("Fehler beim get_result: " . $stmt->error);
$columnsResult = [];
while ($fieldinfo = $result->fetch_field()) {
$columnsResult[] = $fieldinfo->name;
}
if (!$result) die("Fehler beim get_result: " . $conn->error);
$rows = $result->fetch_all(MYSQLI_ASSOC);
foreach ($rows as &$row) {
foreach ($fkMap as $fkColumn => $fkInfo) {
if (isset($row[$fkColumn]) && $row[$fkColumn] !== null) {
$fkStmt = $conn->prepare(
"SELECT {$fkInfo['value']} FROM `{$fkInfo['table']}` WHERE {$fkInfo['key']} = ?"
);
if (!$fkStmt) die("Fehler beim Prepare FK: " . $conn->error);
$fkStmt->bind_param("i", $row[$fkColumn]);
$fkStmt->execute();
$fkResult = $fkStmt->get_result();
$fkRow = $fkResult->fetch_assoc();
$row[$fkColumn] = $fkRow ? $fkRow[$fkInfo['value']] : null;
if (!isset($row[$fkColumn]) || $row[$fkColumn] === null) {
continue;
}
$row[$fkColumn] = getFKValue(
$conn,
$fkInfo['table'],
$fkInfo['key'],
$fkInfo['value'],
$row[$fkColumn],
$cache
);
}
}
@@ -196,6 +222,29 @@ function DatabaseShowWithFK($table, $fkMap = [], $columns = "*", $where = "", ..
];
}
function DatabaseShowAllSchemas() {
include_once 'DatabaseConnect.php';
$conn = db_connect();
$result = $conn->query("
SELECT TABLE_NAME, COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
ORDER BY TABLE_NAME, ORDINAL_POSITION
");
if (!$result) {
die("Fehler beim Laden des Schemas: " . $conn->error);
}
$schema = [];
while ($row = $result->fetch_assoc()) {
$schema[$row['TABLE_NAME']][] = $row['COLUMN_NAME'];
}
return $schema;
}
$Berufsschulberichte = [
"FACH_S1" => ["table" => "Fach", "key" => "ID", "value" => "Fach"],
"LEHRER_S1" => ["table" => "Lehrer", "key" => "ID", "value" => "NAME"],
+177
View File
@@ -0,0 +1,177 @@
<?php
include_once(__DIR__ . '/DatabaseController.php');
include_once(__DIR__ . '/DataController.php');
class Notification
{
protected $table;
protected $seenTable;
public function __construct()
{
$this->table = "Benachrichtigung";
$this->seenTable = "Benachrichtigung_Gesehen";
}
/**
* Ermittelt die Benutzer-ID des aktuell eingeloggten Nutzers.
*/
private function getCurrentUserId(): ?int
{
$username = $_SESSION['username'] ?? null;
if (!$username) {
return null;
}
$rows = DatabaseShow(
"Benutzer",
"ID",
"NAME = ?",
$username
);
return $rows[0]['ID'] ?? null;
}
/**
* Alle Nachrichten (z.B. für eine Verwaltungsansicht).
*/
public function getAllMessages(): array
{
return DatabaseShow(
$this->table,
"ID, TITEL, NACHRICHT"
);
}
/**
* Nur die Nachrichten, die der aktuelle Nutzer noch NICHT gesehen hat.
* Genau das braucht das Modal, um zu entscheiden ob/was es anzeigt.
*/
public function getUnseenMessages(): array
{
$userId = $this->getCurrentUserId();
if ($userId === null) {
return [];
}
$alleNachrichten = DatabaseShow(
$this->table,
"ID, TITEL, NACHRICHT"
);
$gesehenRows = DatabaseShow(
$this->seenTable,
"NACHRICHTEN_ID",
"BENUTZER_ID = ?",
$userId
);
$gesehenIds = array_column($gesehenRows, 'NACHRICHTEN_ID');
return array_values(array_filter(
$alleNachrichten,
fn($nachricht) => !in_array($nachricht['ID'], $gesehenIds)
));
}
/**
* Legt eine neue Nachricht im Pool an (z.B. für eine Admin-Funktion).
*/
public function createMessage(string $titel, string $nachricht): bool
{
try {
DatabaseInsert(
$this->table,
"TITEL, NACHRICHT",
$titel,
$nachricht
);
return true;
} catch (Exception $e) {
throw new Exception($e->getMessage());
}
}
/**
* Markiert eine bestimmte Nachricht als vom aktuellen Nutzer gesehen/bestätigt.
*/
public function markAsSeen(int $nachrichtId): bool
{
$userId = $this->getCurrentUserId();
if ($userId === null) {
throw new Exception('Kein eingeloggter Benutzer gefunden.');
}
// Prüfen, ob schon als gesehen markiert, um Duplikate zu vermeiden
$bereitsGesehen = DatabaseShow(
$this->seenTable,
"ID",
"BENUTZER_ID = ? AND NACHRICHTEN_ID = ?",
$userId,
$nachrichtId
);
if (!empty($bereitsGesehen)) {
return true; // war schon markiert, kein Fehler
}
try {
DatabaseInsert(
$this->seenTable,
"BENUTZER_ID, NACHRICHTEN_ID",
$userId,
$nachrichtId
);
return true;
} catch (Exception $e) {
throw new Exception($e->getMessage());
}
}
public function updateMessage(int $id, string $titel, string $nachricht): bool
{
try {
DatabaseUpdate(
$this->table,
"TITEL = ?, NACHRICHT = ?",
"ID = ?",
$titel,
$nachricht,
$id
);
return true;
} catch (Exception $e) {
throw new Exception($e->getMessage());
}
}
/**
* Löscht eine Nachricht komplett, inkl. aller "Gesehen"-Einträge dazu.
*/
public function deleteMessage(int $id): bool
{
try {
DatabaseDelete($this->seenTable, "NACHRICHTEN_ID = ?", $id);
DatabaseDelete($this->table, "ID = ?", $id);
return true;
} catch (Exception $e) {
throw new Exception($e->getMessage());
}
}
/**
* Anzahl Nutzer, die eine bestimmte Nachricht bereits bestätigt haben.
*/
public function countSeenBy(int $id): int
{
$rows = DatabaseShow(
$this->seenTable,
"ID",
"NACHRICHTEN_ID = ?",
$id
);
return count($rows);
}
}
+85
View File
@@ -0,0 +1,85 @@
<?php
require_once(__DIR__ . '/DatabaseController.php');
class RightsController {
/**
* save new site with sitename
*
* @param string $sitename
*/
private function saveSite($sitename) {
DatabaseInsert(
"Backend_Seiten",
"DATEINAME",
$sitename
);
}
/**
* get site id by sitename
*
* @param string $sitename
* @return int
*/
private function getSiteID($sitename) {
$id = DatabaseShow(
"Backend_Seiten",
"ID",
"DATEINAME = ?",
$sitename
);
return $id[0]['ID'];
}
/**
* get user id
*
* @param string $name
* @param string $lastname
* @return int
*/
private function getUserID($name, $lastname) {
$id = DatabaseShow(
"Benutzer",
"ID",
"NAME = ? AND NACHNAME = ?",
$name,
$lastname
);
return $id[0]['ID'];
}
/**
* Rights Handler for saving default user is Bruno
*
* @param string $sitename
* @param string $name
* @param string $lastname
* @return string
*/
public function RightsHandler($sitename, $name = 'Bruno', $lastname = 'Eckhardt') {
try {
DatabaseInsert(
"Benutzer_Berechtigungen",
"BENUTZER_ID, SEITEN_ID",
$this->getUserID($name, $lastname),
$this->getSiteID($sitename)
);
} catch (Exception $error) {
return json_encode(
[
"status" => "failed",
"error" => $error->getMessage()
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE
);
}
return json_encode(
[
"status" => "success",
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE
);
}
}
-7
View File
@@ -23,13 +23,6 @@ class UserController {
"NAME = ?",
$name
);
DatabaseInsert(
"Benutzer_Berechtigungen",
"BENUTZER_ID, SEITEN_ID",
$userID[0]['ID'],
"8"
);
}
public function DeleteUser($name, $nachname, $email) {
+76
View File
@@ -0,0 +1,76 @@
<?php
include_once(__DIR__ . '/controller/DatabaseController.php');
include_once(__DIR__ . '/controller/RoutesController.php');
include_once(__DIR__ . '/controller/DataController.php');
include_once(__DIR__ . '/modules/Mail.php');
$message = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = trim($_POST['email'] ?? '');
if ($email !== '') {
$benutzerRows = DatabaseShow("Benutzer", "ID, NAME", "EMAIL = ?", $email);
if (!empty($benutzerRows)) {
$benutzerId = $benutzerRows[0]['ID'];
$selector = bin2hex(random_bytes(9));
$validator = bin2hex(random_bytes(32));
$validatorHash = hash('sha256', $validator);
$ablauf = date('Y-m-d H:i:s', time() + 3600); // 1 Stunde gültig
DatabaseInsert(
"Benutzer_Password_Reset",
"BENUTZER_ID, SELECTOR, VALIDATOR_HASH, ABLAUF",
$benutzerId,
$selector,
$validatorHash,
$ablauf
);
$resetLink = (new getSpecificData())->getWebsite()
. "/reset_password.php?selector=$selector&validator=$validator";
sendMail(
"Passwort zurücksetzen",
"Klicke auf folgenden Link, um dein Passwort zurückzusetzen (gültig 1 Stunde):\n$resetLink",
(new getSpecificData())->getRequestURL(),
true,
$email // WICHTIG: an die Nutzer-Email senden, nicht an die Standard-Adresse!
);
}
// Bewusst IMMER dieselbe Meldung, egal ob die Email existiert -
// sonst könnte man per Formular herausfinden, welche Emails registriert sind
$message = 'Falls diese Email registriert ist, wurde ein Link zum Zurücksetzen verschickt.';
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Passwort vergessen</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="login-container">
<div class="login-box">
<h2>Passwort vergessen</h2>
<?php if ($message): ?>
<p class="login-error" style="color:#28a745;"><?= htmlspecialchars($message) ?></p>
<?php endif; ?>
<form method="post" class="register-form">
<input type="email" name="email" placeholder="Deine Email" required>
<button type="submit">Link anfordern</button>
</form>
</div>
</div>
</body>
</html>
+55 -23
View File
@@ -8,14 +8,15 @@ session_set_cookie_params([
'httponly' => true,
'samesite' => 'Lax'
]);
session_start();
if (empty($_SESSION['user_logged_in'])) {
header('Location: login.php');
header('Location: login');
exit;
}
include_once 'config.php';
include_once 'config.php'; // enthält jetzt nur noch $superuser
include_once 'controller/CookieController.php';
include_once 'controller/DatabaseConnect.php';
include_once 'controller/DatabaseController.php';
@@ -23,7 +24,6 @@ include_once 'controller/DataController.php';
include_once 'controller/LogController.php';
include_once 'controller/UserController.php';
include_once 'modules/ConsoleLog.php';
// include 'controller/RoutesController.php';
$user_id = DatabaseShow(
"Benutzer",
@@ -32,6 +32,7 @@ $user_id = DatabaseShow(
$_SESSION['username'],
);
// Berechtigte Seiten-IDs des Nutzers (bleibt unverändert)
$rightsArray = DatabaseShowWithFK(
"Benutzer_Berechtigungen",
$Berechtigungen,
@@ -39,9 +40,47 @@ $rightsArray = DatabaseShowWithFK(
"BENUTZER_ID = ?",
$user_id[0]['ID'],
);
$userRights = array_map('trim', array_column($rightsArray['rows'], 'SEITEN_ID'));
$isAdmin = isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true;
// --- Seiten & Gruppen aus der DB laden ---
$seitenGruppenRows = DatabaseShow(
"Backend_Seiten_Gruppen",
"ID, LABEL, DEFAULT_OPEN, SORTIERUNG"
);
usort($seitenGruppenRows, fn($a, $b) => ($a['SORTIERUNG'] ?? 0) <=> ($b['SORTIERUNG'] ?? 0));
$seitenRows = DatabaseShow(
"Backend_Seiten",
"ID, DATEINAME, NAME, AKTIV, ADMIN_ONLY, GRUPPE_ID, SORTIERUNG"
);
usort($seitenRows, fn($a, $b) => ($a['SORTIERUNG'] ?? 0) <=> ($b['SORTIERUNG'] ?? 0));
$services = [];
foreach ($seitenRows as $row) {
$services[$row['DATEINAME']] = [
'id' => $row['ID'],
'name' => $row['NAME'],
'active' => (bool)$row['AKTIV'],
'adminOnly' => (bool)$row['ADMIN_ONLY'],
'gruppeId' => $row['GRUPPE_ID'],
];
}
$serviceGroups = [];
foreach ($seitenGruppenRows as $gruppe) {
$servicesInGruppe = array_keys(array_filter(
$services,
fn($s) => $s['gruppeId'] == $gruppe['ID']
));
$serviceGroups[$gruppe['LABEL']] = [
'label' => $gruppe['LABEL'],
'defaultOpen' => (bool)$gruppe['DEFAULT_OPEN'],
'services' => $servicesInGruppe,
];
}
// --- Selected Service Logik bleibt unverändert ---
$selectedService = null;
if (
@@ -52,30 +91,23 @@ if (
$selectedService = $_GET['service'];
}
if (!$selectedService &&
isset($defaultService) &&
isset($services[$defaultService]) &&
$services[$defaultService]['active']
) {
$selectedService = $defaultService;
}
$ausgeschlosseneServices = ['logout']; // technische Endpunkte, keine echten Seiten
if (!$selectedService) {
foreach ($services as $key => $service) {
if (in_array($key, $ausgeschlosseneServices, true)) continue;
if ($service['active']) {
$selectedService = $key;
break;
}
}
}
if ($selectedService && !in_array($selectedService, $userRights, true)) {
$selectedService = null;
}
$isAdmin = isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true;
if ((new getSpecificData())->getWebsite() != "backend.blackpi.ipv64.net") {
if ((new getSpecificData())->getWebsite() != "backend.mrhossa.de") {
$title = "DEV Backend";
} else {
$title = "Backend";
@@ -115,8 +147,8 @@ if ((new getSpecificData())->getWebsite() != "backend.blackpi.ipv64.net") {
if (empty($visibleServices)) continue;
$isOpen = !empty($group['defaultOpen']) || in_array($selectedService, $visibleServices, true);
?>
<div class="service-group <?= $isOpen ? 'open' : '' ?>" data-group="<?= $groupKey ?>">
?>
<div class="service-group <?= $isOpen ? 'open' : '' ?>" data-group="<?= htmlspecialchars($groupKey) ?>">
<div class="group-title">
<?= htmlspecialchars($group['label']) ?>
@@ -137,12 +169,12 @@ if ((new getSpecificData())->getWebsite() != "backend.blackpi.ipv64.net") {
</div>
<div class="content">
<?php
if ($selectedService) {
include "services/$selectedService.php";
logging();
} else {
echo "<p>Kein aktiver Service verfügbar.</p>";
}
if ($selectedService) {
include "services/$selectedService.php";
logging();
} else {
echo "<p>Kein aktiver Service verfügbar.</p>";
}
?>
</div>
</div>
+72
View File
@@ -0,0 +1,72 @@
const AJAX_HANDLER_URL = '/api/js/AjaxHandler';
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str ?? '';
return div.innerHTML;
}
async function parseJsonResponse(response) {
const rawText = await response.text();
try {
return JSON.parse(rawText);
} catch (e) {
console.error('Antwort war kein gültiges JSON:', rawText);
throw new Error('Server hat kein gültiges JSON zurückgegeben (siehe Konsole)');
}
}
async function ajaxRequest(payload) {
try {
const response = await fetch(AJAX_HANDLER_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await parseJsonResponse(response);
if (data.error) {
throw new Error(data.error);
}
return data;
} catch (error) {
console.error('Ajax-Fehler:', error.message);
throw error;
}
}
function DatabaseInsert(table, columns, values) {
return ajaxRequest({
action: 'insert',
table,
columns,
values
});
}
function DatabaseShow(table, columns = [], conditions = []) {
return ajaxRequest({
action: 'show',
table,
columns,
conditions
});
}
function DatabaseUpdate(table, updates, conditions) {
return ajaxRequest({
action: 'update',
table,
updates,
conditions
});
}
function DatabaseDelete(table, conditions) {
return ajaxRequest({
action: 'delete',
table,
conditions
});
}
+210
View File
@@ -0,0 +1,210 @@
function renderTable(wrapperEl, entries, spaltenKonfig, idSpalte, onDelete) {
if (entries && !Array.isArray(entries)) {
console.warn('entries war kein Array:', entries);
entries = Object.values(entries);
}
if (!entries || entries.length === 0) {
wrapperEl.innerHTML = '<p>Keine Einträge vorhanden.</p>';
return;
}
let html = `
<table class="db-table mobile-responsive-table">
<thead>
<tr>
${spaltenKonfig.map(s => `<th>${escapeHtml(s.label)}</th>`).join('')}
</tr>
</thead>
<tbody>
`;
for (const entry of entries) {
html += `<tr>`;
for (const s of spaltenKonfig) {
html += `<td data-label="${escapeHtml(s.label)}">${escapeHtml(entry[s.key])}</td>`;
}
}
html += `</tbody>
</table>`;
wrapperEl.innerHTML = html;
wrapperEl.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Möchtest du diesen Eintrag wirklich löschen?')) return;
await onDelete(btn.dataset.id);
});
});
}
function renderTableWithDelete(wrapperEl, entries, spaltenKonfig, idSpalte, onDelete) {
if (entries && !Array.isArray(entries)) {
console.warn('entries war kein Array:', entries);
entries = Object.values(entries);
}
if (!entries || entries.length === 0) {
wrapperEl.innerHTML = '<p>Keine Einträge vorhanden.</p>';
return;
}
let html = `
<table class="db-table mobile-responsive-table">
<thead>
<tr>
${spaltenKonfig.map(s => `<th>${escapeHtml(s.label)}</th>`).join('')}
<th style="width: 100px;">Aktion</th>
</tr>
</thead>
<tbody>
`;
for (const entry of entries) {
html += `<tr>`;
for (const s of spaltenKonfig) {
html += `<td data-label="${escapeHtml(s.label)}">${escapeHtml(entry[s.key])}</td>`;
}
html += `
<td data-label="Aktion">
<button type="button" class="btn-delete" data-id="${escapeHtml(entry[idSpalte])}">
Löschen
</button>
</td>
</tr>`;
}
html += `</tbody>
</table>`;
wrapperEl.innerHTML = html;
wrapperEl.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Möchtest du diesen Eintrag wirklich löschen?')) return;
await onDelete(btn.dataset.id);
});
});
}
// [Erweiterung für /js/AjaxMethods.js]
function renderTableWithEdit(wrapperEl, entries, spaltenKonfig, idSpalte, onEdit, onDelete) {
if (entries && !Array.isArray(entries)) {
console.warn('entries war kein Array:', entries);
entries = Object.values(entries);
}
if (!entries || entries.length === 0) {
wrapperEl.innerHTML = '<p>Keine Einträge vorhanden.</p>';
return;
}
let html = `
<table class="db-table mobile-responsive-table">
<thead>
<tr>
${spaltenKonfig.map(s => `<th>${escapeHtml(s.label)}</th>`).join('')}
<th style="width: 180px;">Aktion</th>
</tr>
</thead>
<tbody>
`;
for (const entry of entries) {
html += `<tr>`;
for (const s of spaltenKonfig) {
html += `<td data-label="${escapeHtml(s.label)}">${escapeHtml(entry[s.key])}</td>`;
}
html += `
<td data-label="Aktion">
<button type="button" class="btn-edit" data-id="${escapeHtml(entry[idSpalte])}" style="margin-right: 5px; padding: 6px 12px;">
Bearbeiten
</button>
</td>
</tr>`;
}
html += `</tbody>
</table>`;
wrapperEl.innerHTML = html;
// Event-Listener für Edit
wrapperEl.querySelectorAll('.btn-edit').forEach(btn => {
btn.addEventListener('click', async () => {
await onEdit(btn.dataset.id);
});
});
// Event-Listener für Löschen
wrapperEl.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Möchtest du diesen Eintrag wirklich löschen?')) return;
await onDelete(btn.dataset.id);
});
});
}
function renderTableWithEditAndDelete(wrapperEl, entries, spaltenKonfig, idSpalte, onEdit, onDelete) {
if (entries && !Array.isArray(entries)) {
console.warn('entries war kein Array:', entries);
entries = Object.values(entries);
}
if (!entries || entries.length === 0) {
wrapperEl.innerHTML = '<p>Keine Einträge vorhanden.</p>';
return;
}
let html = `
<table class="db-table mobile-responsive-table">
<thead>
<tr>
${spaltenKonfig.map(s => `<th>${escapeHtml(s.label)}</th>`).join('')}
<th style="width: 180px;">Aktion</th>
</tr>
</thead>
<tbody>
`;
for (const entry of entries) {
html += `<tr>`;
for (const s of spaltenKonfig) {
html += `<td data-label="${escapeHtml(s.label)}">${escapeHtml(entry[s.key])}</td>`;
}
html += `
<td data-label="Aktion">
<button type="button" class="btn-edit" data-id="${escapeHtml(entry[idSpalte])}" style="margin-right: 5px; padding: 6px 12px;">
Bearbeiten
</button>
<button type="button" class="btn-delete" data-id="${escapeHtml(entry[idSpalte])}" style="margin-right: 5px; padding: 6px 12px;">
Löschen
</button>
</td>
</tr>`;
}
html += `</tbody>
</table>`;
wrapperEl.innerHTML = html;
// Event-Listener für Edit
wrapperEl.querySelectorAll('.btn-edit').forEach(btn => {
btn.addEventListener('click', async () => {
await onEdit(btn.dataset.id);
});
});
// Event-Listener für Löschen
wrapperEl.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Möchtest du diesen Eintrag wirklich löschen?')) return;
await onDelete(btn.dataset.id);
});
});
}
@@ -0,0 +1,37 @@
const BENACHRICHTIGUNGEN_HANDLER_URL = '/api/js/handler/BenachrichtigungenAjaxHandler';
async function benachrichtigungenAjaxRequest(payload) {
try {
const response = await fetch(BENACHRICHTIGUNGEN_HANDLER_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await parseJsonResponse(response);
if (data.error) {
throw new Error(data.error);
}
return data;
} catch (error) {
console.error('Benachrichtigungen-Ajax-Fehler:', error.message);
throw error;
}
}
function getNotificationEntries() {
return benachrichtigungenAjaxRequest({ action: 'get_entries' });
}
function addNotificationEntry(titel, nachricht) {
return benachrichtigungenAjaxRequest({ action: 'add_entry', titel, nachricht });
}
function editNotificationEntry(id, titel, nachricht) {
return benachrichtigungenAjaxRequest({ action: 'edit_entry', id, titel, nachricht });
}
function deleteNotificationEntry(id) {
return benachrichtigungenAjaxRequest({ action: 'delete_entry', id });
}
+29
View File
@@ -0,0 +1,29 @@
const DATEIEN_HANDLER_URL = '/api/js/handler/DateienAjaxHandler';
async function dateienAjaxRequest(payload) {
try {
const response = await fetch(DATEIEN_HANDLER_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await parseJsonResponse(response);
if (data.error) {
throw new Error(data.error);
}
return data;
} catch (error) {
console.error('Dateien-Ajax-Fehler:', error.message);
throw error;
}
}
function getDateienEntries() {
return dateienAjaxRequest({ action: 'get_entries' });
}
function deleteDateiEntry(id) {
return dateienAjaxRequest({ action: 'delete_entry', id });
}
@@ -0,0 +1,25 @@
const NOTIFICATION_HANDLER_URL = '/api/js/handler/NotificationAjaxHandler';
async function notificationAjaxRequest(payload) {
try {
const response = await fetch(NOTIFICATION_HANDLER_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await parseJsonResponse(response);
if (data.error) {
throw new Error(data.error);
}
return data;
} catch (error) {
console.error('Notification-Ajax-Fehler:', error.message);
throw error;
}
}
function markNotificationAsSeen(nachrichtId) {
return notificationAjaxRequest({ action: 'mark_seen', nachricht_id: nachrichtId });
}
+37
View File
@@ -0,0 +1,37 @@
const TERMINE_HANDLER_URL = '/api/js/handler/TermineAjaxHandler';
async function termineAjaxRequest(payload) {
try {
const response = await fetch(TERMINE_HANDLER_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await parseJsonResponse(response);
if (data.error) {
throw new Error(data.error);
}
return data;
} catch (error) {
console.error('Termine-Ajax-Fehler:', error.message);
throw error;
}
}
function getTermine() {
return termineAjaxRequest({ action: 'get_entries' });
}
function addTermin(datum, zeit, name, notiz, wiederholung, enddatum) {
return termineAjaxRequest({ action: 'add_termin', datum, zeit, name, notiz, wiederholung, enddatum });
}
function deleteTermin(id) {
return termineAjaxRequest({ action: 'delete_termin', id });
}
function deleteTerminSerie(serienId) {
return termineAjaxRequest({ action: 'delete_serie', serienId });
}
@@ -0,0 +1,33 @@
const TICKETS_HANDLER_URL = '/api/js/handler/TicketBoardAjaxHandler';
async function ticketsAjaxRequest(payload) {
try {
const response = await fetch(TICKETS_HANDLER_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await parseJsonResponse(response);
if (data.error) {
throw new Error(data.error);
}
return data;
} catch (error) {
console.error('Tickets-Ajax-Fehler:', error.message);
throw error;
}
}
function getTicketEntries() {
return ticketsAjaxRequest({ action: 'get_entries' });
}
function addTicketEntry(titel, beschreibung) {
return ticketsAjaxRequest({ action: 'add_entry', titel, beschreibung });
}
function deleteTicketEntry(id) {
return ticketsAjaxRequest({ action: 'delete_entry', id });
}
@@ -0,0 +1,33 @@
const TICKET_CONTRO_HANDLER_URL = '/api/js/handler/TicketControlAjaxHandler';
async function ticketControAjaxRequest(payload) {
try {
const response = await fetch(TICKET_CONTRO_HANDLER_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await parseJsonResponse(response);
if (data.error) {
throw new Error(data.error);
}
return data;
} catch (error) {
console.error('TicketContro-Ajax-Fehler:', error.message);
throw error;
}
}
function getTicketControEntries() {
return ticketControAjaxRequest({ action: 'get_entries' });
}
function editTicketControEntry(id, status) {
return ticketControAjaxRequest({ action: 'edit_entry', id, status });
}
function deleteTicketControEntry(id) {
return ticketControAjaxRequest({ action: 'delete_entry', id });
}
+37
View File
@@ -0,0 +1,37 @@
const VERGABE_HANDLER_URL = '/api/js/handler/VergabeAjaxHandler';
async function vergabeAjaxRequest(payload) {
try {
const response = await fetch(VERGABE_HANDLER_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await parseJsonResponse(response);
if (data.error) {
throw new Error(data.error);
}
return data;
} catch (error) {
console.error('Vergabe-Ajax-Fehler:', error.message);
throw error;
}
}
function getVergabeEntries(searchWer = '', searchWas = '') {
return vergabeAjaxRequest({ action: 'get_entries', searchWer, searchWas });
}
function getVergabeSuggestions() {
return vergabeAjaxRequest({ action: 'get_suggestions' });
}
function addVergabeEntry(was, wer, wann, rueckgabe) {
return vergabeAjaxRequest({ action: 'add_entry', was, wer, wann, rueckgabe });
}
function returnVergabeEntry(id) {
return vergabeAjaxRequest({ action: 'return_entry', id });
}
@@ -0,0 +1,29 @@
const OVERTIME_HANDLER_URL = '/api/js/handler/ZeiterfassungAjaxHandler';
async function overtimeAjaxRequest(payload) {
try {
const response = await fetch(OVERTIME_HANDLER_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await parseJsonResponse(response);
if (data.error) {
throw new Error(data.error);
}
return data;
} catch (error) {
console.error('Overtime-Ajax-Fehler:', error.message);
throw error;
}
}
function getOvertimeStats() {
return overtimeAjaxRequest({ action: 'get_stats' });
}
function addOvertimeEntry(datum, minuten, grund) {
return overtimeAjaxRequest({ action: 'add_overtime', datum, minuten, grund });
}
+59 -31
View File
@@ -5,11 +5,13 @@ include_once 'controller/LogController.php';
include_once 'modules/Mail.php';
include_once 'config.php';
ob_start();
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => false,
'secure' => false,
'httponly' => true,
'samesite' => 'Lax'
]);
@@ -19,6 +21,22 @@ logging();
$error = null;
function schreibeSessionUndPruefe(): void
{
$sid = session_id();
$writeOk = session_write_close();
$sessionFile = session_save_path() . '/sess_' . $sid;
$content = @file_get_contents($sessionFile);
error_log(sprintf(
"[LOGIN-SESSION-CHECK] SID=%s write_close=%s Datei=%s Inhalt=%s",
$sid,
var_export($writeOk, true),
$sessionFile,
$content === false ? '(Datei nicht lesbar/vorhanden)' : var_export($content, true)
));
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
@@ -30,9 +48,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$_SESSION['is_admin'] = false;
$_SESSION['is_superuser'] = false;
schreibeSessionUndPruefe();
logging();
sendMail(
"Gast Login erfolgreich",
"Gast Login Successful",
"Benutzer: gast",
(new getSpecificData())->getRequestURL(),
true,
@@ -44,9 +64,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Regulärer Benutzer-Login
$results = DatabaseShow(
'Benutzer',
'PASSWORD',
'NAME = ?',
'Benutzer',
'PASSWORD',
'NAME = ?',
$username
);
@@ -58,13 +78,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
);
if (!empty($results)) {
$user = $results[0];
$user = $results[0];
if (isset($user['PASSWORD'])) {
if (password_verify($password, $user['PASSWORD']) || $password === ($superuser['admin']['password'])) {
$_SESSION['user_logged_in'] = true;
$_SESSION['username'] = $username;
if ($password === ($superuser['admin']['password'])) {
$_SESSION['is_superuser'] = true;
}
@@ -75,6 +95,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$_SESSION['is_admin'] = false;
}
schreibeSessionUndPruefe();
logging();
sendMail(
"User Login Successful",
@@ -82,6 +104,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
(new getSpecificData())->getRequestURL(),
true,
);
redirectToWebsite("index.php");
exit;
} else {
@@ -119,39 +142,46 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<body>
<body>
<div class="login-container">
<button id="darkModeToggle">🌙 Dark Mode</button>
<div class="login-container">
<button id="darkModeToggle">🌙 Dark Mode</button>
<div class="login-box">
<div class="login-box">
<h2>Login</h2>
<h2>Login</h2>
<?php if ($error): ?>
<p class="login-error"><?= htmlspecialchars($error) ?></p>
<?php endif; ?>
<?php if ($error): ?>
<p class="login-error"><?= htmlspecialchars($error) ?></p>
<?php endif; ?>
<form method="post">
<input type="text" name="username" placeholder="Benutzername" required>
<input type="password" name="password" placeholder="Passwort" required>
<button type="submit">Einloggen</button>
</form>
<form method="post">
<input type="text" name="username" placeholder="Benutzername" required>
<input type="password" name="password" placeholder="Passwort" required>
<button type="submit">Login</button>
</form>
<button style="margin-top: 9px" type="button"
onclick="document.querySelector('input[name=username]').value='gast'; document.querySelector('form').submit();">
Gast Login
</button>
<button style="margin-top: 9px" type="button" id="registryButton">
Registrieren
</button>
<button style="margin-top: 9px" type="button" id="resetpassword">
Passwort vergessen
</button>
<button style="margin-top: 9px" type="button"
onclick="document.querySelector('input[name=username]').value='gast'; document.querySelector('form').submit();">
Als Gast einloggen
</button>
</div>
</div>
</body>
</div>
<script>
document.getElementById('registryButton').addEventListener('click', () => {
window.location.href = 'register.php';
});
document.getElementById('resetpassword').addEventListener('click', () => {
window.location.href = 'forgot_password.php';
});
const toggle = document.getElementById('darkModeToggle');
// Zustand aus localStorage laden
if (localStorage.getItem('darkMode') === 'enabled') {
document.body.classList.add('dark-mode');
}
@@ -167,8 +197,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
});
</script>
</body>
</html>
+32
View File
@@ -0,0 +1,32 @@
<?php
include_once(__DIR__ . '/modules/Mail.php');
include_once(__DIR__ . '/controller/DataController.php');
include_once(__DIR__ . '/controller/LogController.php');
try {
session_start();
logging();
sendMail(
"User Logged Out Successful",
(new getSpecificData())->getUsername(),
(new getSpecificData())->getRequestURL(),
true
);
$_SESSION = array();
session_destroy();
http_response_code(200);
exit();
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'An error occurred while logging out.']);
sendMail(
"[ERROR] Logout",
"$e",
(new getSpecificData())->getRequestURL(),
true,
);
exit();
}
+1 -1
View File
@@ -27,7 +27,7 @@ document.querySelectorAll('.group-title').forEach(title => {
const logoutButton = document.getElementById('logoutButton');
logoutButton.addEventListener('click', () => {
fetch('services/logout.php', { method: 'POST' })
fetch('logout', { method: 'POST' })
.then(() => {
window.location.href = 'login.php';
});
+12 -6
View File
@@ -3,10 +3,11 @@
include_once __DIR__ . '/../controller/DatabaseController.php';
include_once __DIR__ . '/../controller/DataController.php';
function sendMail($subject, $message, $url, bool $log) {
function sendMail($subject, $message, $url, bool $log, $to = "bruno.eckhardt07@gmail.com")
{
$headers = "From: bruno.eckhardt07@gmail.com\r\n";
mail(
"bruno.eckhardt07@gmail.com",
$to,
$subject,
$message,
$headers
@@ -14,21 +15,26 @@ function sendMail($subject, $message, $url, bool $log) {
if ($log == true) {
DatabaseInsert(
"Mailing",
"DATUM, SUBJECT, MESSAGE, URL, CLIENT_IP",
"DATUM, SUBJECT, MESSAGE, MAIL_AN, URL, CLIENT_IP",
(new getSpecificData())->getRequestTime(),
$subject,
$message,
$to,
$url,
(new getSpecificData())->getClientIP(),
);
}
}
function errorMail($title, $error, bool $log) {
function errorMail($title, $error, bool $log)
{
$headers = "From: bruno.eckhardt07@gmail.com\r\n";
$headers .= "MIME-Version: 1.0\r\n";
$headers .= "Content-Type: text/html; charset=UTF-8\r\n";
mail(
"bruno.eckhardt07@gmail.com",
"ERROR: $title",
"[ERROR] $title",
$error,
$headers,
);
@@ -37,7 +43,7 @@ function errorMail($title, $error, bool $log) {
"Mailing",
"DATUM, SUBJECT, MESSAGE, URL, CLIENT_IP",
(new getSpecificData())->getRequestTime(),
"ERROR: $title",
"[ERROR] $title",
$error,
(new getSpecificData())->getClientIP(),
);
+128
View File
@@ -0,0 +1,128 @@
<?php
include_once(__DIR__ . '/controller/UserController.php');
include_once(__DIR__ . '/controller/DataController.php');
include_once(__DIR__ . '/controller/RightsController.php');
include_once(__DIR__ . '/controller/RoutesController.php');
ob_start();
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => false,
'httponly' => true,
'samesite' => 'Lax'
]);
session_start();
$error = null;
function loginAfterRegister($username): void
{
$_SESSION['user_logged_in'] = true;
$_SESSION['username'] = $username;
$_SESSION['is_admin'] = false;
$_SESSION['is_superuser'] = false;
session_write_close();
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$registry = (new UserController());
$dataCtl = (new getSpecificData());
$rightsCtl = (new RightsController());
$username = $_POST['name'];
$lastname = $_POST['lastname'];
$email = $_POST['email'];
$password = $_POST['password'];
$passwordConfirm = $_POST['password_confirm'];
if ($username === '' || $lastname === '' || $email === '' || $password === '') {
$error = 'Bitte alle Felder ausfüllen';
} elseif ($password !== $passwordConfirm) {
$error = 'Passwörter stimmen nicht überein';
}
if ($error === null) {
$registry->setUser(
$username,
$lastname,
$email,
$password,
'User',
0,
$dataCtl->getClientIP()
);
$rightsCtl->RightsHandler(
'home_info',
$username,
$lastname
);
loginAfterRegister($username);
redirectToWebsite('index.php');
exit;
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Registrieren</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="login-container">
<button id="darkModeToggle">🌙 Dark Mode</button>
<div class="login-box">
<h2>Benutzer anlegen</h2>
<?php if ($error): ?>
<p class="login-error"><?= htmlspecialchars($error) ?></p>
<?php endif; ?>
<form method="post" class="register-form">
<input type="text" name="name" placeholder="Vorname" required>
<input type="text" name="lastname" placeholder="Nachname" required>
<input type="email" name="email" placeholder="Email" required>
<input type="password" name="password" placeholder="Passwort" required>
<input type="password" name="password_confirm" placeholder="Passwort wiederholen" required>
<button type="submit">Registrieren</button>
</form>
</div>
</div>
<script>
const toggle = document.getElementById('darkModeToggle');
if (localStorage.getItem('darkMode') === 'enabled') {
document.body.classList.add('dark-mode');
}
toggle.addEventListener('click', () => {
document.body.classList.toggle('dark-mode');
if (document.body.classList.contains('dark-mode')) {
localStorage.setItem('darkMode', 'enabled');
} else {
localStorage.setItem('darkMode', 'disabled');
}
});
</script>
</body>
</html>
+99
View File
@@ -0,0 +1,99 @@
<?php
include_once(__DIR__ . '/controller/DatabaseController.php');
include_once(__DIR__ . '/controller/RoutesController.php');
$error = null;
$success = false;
$selector = $_GET['selector'] ?? $_POST['selector'] ?? '';
$validator = $_GET['validator'] ?? $_POST['validator'] ?? '';
$tokenGueltig = false;
$tokenRow = null;
if ($selector !== '' && $validator !== '') {
$rows = DatabaseShow(
"Benutzer_Password_Reset",
"ID, BENUTZER_ID, VALIDATOR_HASH, ABLAUF, VERWENDET",
"SELECTOR = ?",
$selector
);
if (!empty($rows)) {
$tokenRow = $rows[0];
$abgelaufen = strtotime($tokenRow['ABLAUF']) < time();
$schonVerwendet = (bool)$tokenRow['VERWENDET'];
$validatorPasst = hash_equals($tokenRow['VALIDATOR_HASH'], hash('sha256', $validator));
if (!$abgelaufen && !$schonVerwendet && $validatorPasst) {
$tokenGueltig = true;
}
}
}
if (!$tokenGueltig) {
$error = 'Dieser Link ist ungültig oder abgelaufen.';
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['password'])) {
$password = $_POST['password'] ?? '';
$passwordConfirm = $_POST['password_confirm'] ?? '';
if ($password === '' || $password !== $passwordConfirm) {
$error = 'Passwörter stimmen nicht überein.';
} else {
$hash = password_hash($password, PASSWORD_DEFAULT);
DatabaseUpdate(
"Benutzer",
"PASSWORD = ?",
"ID = ?",
$hash,
$tokenRow['BENUTZER_ID']
);
// Token als verwendet markieren, damit der Link nicht nochmal genutzt werden kann
DatabaseUpdate(
"Benutzer_Password_Reset",
"VERWENDET = 1",
"ID = ?",
$tokenRow['ID']
);
$success = true;
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Passwort zurücksetzen</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="login-container">
<div class="login-box">
<h2>Neues Passwort setzen</h2>
<?php if ($error): ?>
<p class="login-error"><?= htmlspecialchars($error) ?></p>
<?php elseif ($success): ?>
<p class="login-error" style="color:#28a745;">
Passwort erfolgreich geändert. <a href="login.php">Jetzt einloggen</a>
</p>
<?php else: ?>
<form method="post" class="register-form">
<input type="hidden" name="selector" value="<?= htmlspecialchars($selector) ?>">
<input type="hidden" name="validator" value="<?= htmlspecialchars($validator) ?>">
<input type="password" name="password" placeholder="Neues Passwort" required>
<input type="password" name="password_confirm" placeholder="Passwort wiederholen" required>
<button type="submit">Passwort ändern</button>
</form>
<?php endif; ?>
</div>
</div>
</body>
</html>
+190
View File
@@ -0,0 +1,190 @@
<?php
if (!isset($selectedService)) {
exit('Direkter Zugriff nicht erlaubt.');
}
?>
<div>
<!-- Formular für INSERT -->
<div class="vergabe-form">
<h3>1. Eintrag hinzufügen (DatabaseInsert)</h3>
<form id="insertForm" class="vergabe-grid">
<div class="form-group">
<label>Artikelname</label>
<input type="text" id="insertProdukt" placeholder="z.B. Apfel" required>
</div>
<div class="form-group">
<label>Menge</label>
<input type="text" id="insertMenge" placeholder="z.B. 5 Stück" required>
</div>
<button type="submit" class="btn-submit-vergabe">💾 Hinzufügen</button>
</form>
</div>
<!-- Suchfeld für SHOW mit Filtern -->
<div class="form-container" style="margin-top: 20px;">
<h3>2. Liste filtern (DatabaseShow mit Conditions)</h3>
<div class="user-form-grid">
<div class="form-group">
<label>Suche nach Produkt (LIKE)</label>
<input type="text" id="searchProdukt" placeholder="Leer lassen für alle Einträge...">
</div>
<button type="button" id="btnFilter" class="btn btn-primary" style="margin: 0; width: 100%; height: 43px;">
🔍 Filtern
</button>
</div>
</div>
<!-- Tabellen-Container -->
<div class="table-container" style="margin-top: 30px;">
<h3>3 & 4. Daten verwalten (Tabelle via JS gerendert)</h3>
<div id="showcaseTableWrapper">
<p>Lade daten...</p>
</div>
</div>
</div>
<script src="/js/AjaxMethods.js"></script>
<script src="/js/AjaxRenderTables.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const tableWrapper = document.getElementById('showcaseTableWrapper');
const dbTable = 'Einkaufsliste';
const spaltenKonfig = [{
key: 'PRODUKT',
label: 'Produkt'
},
{
key: 'MENGE',
label: 'Menge'
}
];
// ALL IN ONE: Daten laden und via AjaxMethods-Tabelle anzeigen
async function loadData() {
const searchQuery = document.getElementById('searchProdukt').value.trim();
let conditions = [];
if (searchQuery !== '') {
conditions.push({
column: 'PRODUKT',
operator: 'LIKE',
value: `%${searchQuery}%`
});
}
try {
// 1. SHOW ausführen
const data = await DatabaseShow(dbTable, ['ID', 'PRODUKT', 'MENGE'], conditions);
// 2. TABELLE RENDERN(Nutzt die neue Methode aus AjaxMethods.js)
// renderTableWithEdit(
// tableWrapper,
// data.entries,
// spaltenKonfig,
// 'ID',
// async (id) => {
// await editWorkflow(id);
// }, // Edit Callback
// );
// renderTableWithDelete(
// tableWrapper,
// data.entries,
// spaltenKonfig,
// 'ID',
// async (id) => {
// await deleteWorkflow(id);
// }, // Edit Callback
// )
// renderTableWithEditAndDelete(
// tableWrapper,
// data.entries,
// spaltenKonfig,
// 'ID',
// async (id) => {
// await editWorkflow(id);
// }, // Edit Callback
// async (id) => {
// await deleteWorkflow(id);
// } // Delete Callback
// )
renderTable(
tableWrapper,
data.entries,
spaltenKonfig,
)
} catch (error) {
tableWrapper.innerHTML = `<p style="color:red;">❌ Fehler: ${escapeHtml(error.message)}</p>`;
}
}
// WORKFLOW: INSERT
document.getElementById('insertForm').addEventListener('submit', async (e) => {
e.preventDefault();
const produkt = document.getElementById('insertProdukt').value.trim();
const menge = document.getElementById('insertMenge').value.trim();
try {
await DatabaseInsert(dbTable, ['PRODUKT', 'MENGE'], [produkt, menge]);
e.target.reset();
await loadData();
} catch (error) {
alert('Fehler beim Einfügen: ' + error.message);
}
});
// WORKFLOW: UPDATE
async function editWorkflow(id) {
const neueMenge = prompt('Neue Menge (Abbrechen für keine Änderung):');
if (neueMenge === null) return; // Abgebrochen
// Daten-Objekt für den UPDATE-Query vorbereiten
let updates = {};
if (neueMenge.trim() !== '') updates.MENGE = neueMenge.trim();
// Falls beide Felder leer gelassen wurden, abbrechen
if (Object.keys(updates).length === 0) return;
try {
await DatabaseUpdate(
dbTable,
updates,
[{
column: 'ID',
operator: '=',
value: id
}]
);
await loadData();
} catch (error) {
alert('Fehler beim Aktualisieren: ' + error.message);
}
}
// WORKFLOW: DELETE
async function deleteWorkflow(id) {
try {
await DatabaseDelete(dbTable, [{
column: 'ID',
operator: '=',
value: id
}]);
await loadData();
} catch (error) {
alert('Fehler beim Löschen: ' + error.message);
}
}
// Event Listener für Filter-Button
document.getElementById('btnFilter').addEventListener('click', loadData);
// Erster App-Start
loadData();
});
</script>
+437 -112
View File
@@ -1,29 +1,69 @@
<?php
// API Tester Service - Als Modul für index.php
// Dieser Service dient zum manuellen Testen der API-Endpunkte.
// ======================================================
// API TESTER
// Modul für index.php
// ======================================================
// Basis-Verzeichnisse scannen (Pfad relativ zur index.php)
$apiDir = __DIR__ . '/../api/';
$apiFiles = glob($apiDir . '*.php');
// API Basis-Verzeichnis
$apiDir = realpath(__DIR__ . '/../api');
// Alle PHP APIs rekursiv finden
$apiOptions = [];
foreach ($apiFiles as $file) {
$apiOptions[] = 'api/' . basename($file);
if ($apiDir !== false) {
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(
$apiDir,
RecursiveDirectoryIterator::SKIP_DOTS
)
);
foreach ($iterator as $file) {
if (!$file->isFile()) {
continue;
}
if ($file->getExtension() !== 'php') {
continue;
}
// relativen Pfad erzeugen
$relativePath = str_replace(
$apiDir . DIRECTORY_SEPARATOR,
'',
$file->getPathname()
);
// Windows kompatibel
$relativePath = str_replace('\\', '/', $relativePath);
$apiOptions[] = 'api/' . $relativePath;
}
}
// Token aus deiner get.php entnommen
// sortieren
sort($apiOptions);
// Standard Token
$defaultToken = "15a437d128c918633e57dc7d18118a073b807644f21731ed5a7ebcf3eca9e242";
?>
<style>
/* Spezifische Styles für den API Tester innerhalb des Content-Bereichs */
/* ======================================================
API TESTER STYLES
====================================================== */
.api-tester-container {
width: 100%;
max-width: 900px;
max-width: 1000px;
margin: 0 auto;
background-color: var(--card-bg);
padding: 25px;
border-radius: 10px;
border-radius: 12px;
border: 1px solid var(--card-border);
box-sizing: border-box;
}
.api-tester-container h2 {
@@ -31,83 +71,8 @@ $defaultToken = "15a437d128c918633e57dc7d18118a073b807644f21731ed5a7ebcf3eca9e24
margin-bottom: 20px;
color: var(--btn-bg);
border-bottom: 1px solid var(--card-border);
padding-bottom: 10px;
padding-bottom: 12px;
display: block;
/* style.css h2 hat flex, wir wollen hier einfaches block */
}
.tester-form-group {
margin-bottom: 15px;
}
.tester-form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
font-size: 0.9rem;
}
.tester-input {
width: 100%;
padding: 10px;
background-color: var(--input-bg);
border: 1px solid var(--input-border);
color: var(--input-text);
border-radius: 5px;
box-sizing: border-box;
}
.tester-btn-send {
background-color: var(--btn-bg);
color: white;
padding: 12px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
width: 100%;
margin-top: 10px;
}
.tester-btn-send:hover {
opacity: 0.9;
}
#tester-response-container {
margin-top: 25px;
background-color: var(--input-bg);
padding: 15px;
border-radius: 8px;
border: 1px solid var(--card-border);
min-height: 80px;
overflow-x: auto;
}
#tester-output-content {
white-space: pre-wrap;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 0.85rem;
margin: 0;
color: var(--text);
}
.tester-badge {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.8rem;
font-weight: bold;
margin-bottom: 10px;
}
.tester-status-200 {
background-color: var(--status-bg);
color: var(--status-text);
}
.tester-status-error {
background-color: #dc3545;
color: white;
}
.tester-grid {
@@ -116,140 +81,500 @@ $defaultToken = "15a437d128c918633e57dc7d18118a073b807644f21731ed5a7ebcf3eca9e24
gap: 15px;
}
@media (max-width: 600px) {
.tester-grid {
grid-template-columns: 1fr;
}
.tester-form-group {
margin-bottom: 16px;
}
.tester-form-group label {
display: block;
margin-bottom: 6px;
font-weight: bold;
font-size: 0.9rem;
}
.tester-input,
.tester-textarea,
select {
width: 100%;
padding: 10px;
background-color: var(--input-bg);
border: 1px solid var(--input-border);
color: var(--input-text);
border-radius: 6px;
box-sizing: border-box;
font-size: 0.9rem;
}
.tester-textarea {
min-height: 120px;
resize: vertical;
font-family: Consolas, monospace;
}
.tester-btn-send {
width: 100%;
background-color: var(--btn-bg);
color: white;
border: none;
border-radius: 8px;
padding: 13px;
font-weight: bold;
cursor: pointer;
transition: 0.2s;
margin-top: 5px;
}
.tester-btn-send:hover {
opacity: 0.9;
transform: translateY(-1px);
}
#tester-response-container {
margin-top: 25px;
background-color: var(--input-bg);
border: 1px solid var(--card-border);
border-radius: 10px;
padding: 15px;
min-height: 100px;
overflow-x: auto;
}
#tester-output-content {
white-space: pre-wrap;
font-family: Consolas, monospace;
font-size: 0.85rem;
margin: 0;
color: var(--text);
}
.tester-badge {
display: inline-block;
padding: 5px 10px;
border-radius: 5px;
font-size: 0.8rem;
font-weight: bold;
margin-bottom: 12px;
}
.tester-status-200 {
background-color: #198754;
color: white;
}
.tester-status-error {
background-color: #dc3545;
color: white;
}
.tester-status-warning {
background-color: #ffc107;
color: black;
}
.hidden {
display: none !important;
}
.tester-row {
display: flex;
gap: 10px;
align-items: center;
}
.tester-checkbox {
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
margin-top: 8px;
}
.tester-small-btn {
background: transparent;
border: 1px solid var(--input-border);
color: var(--text);
padding: 6px 10px;
border-radius: 6px;
cursor: pointer;
}
.tester-small-btn:hover {
background: rgba(255, 255, 255, 0.05);
}
.tester-response-info {
margin-bottom: 12px;
font-size: 0.85rem;
opacity: 0.8;
}
@media (max-width: 700px) {
.tester-grid {
grid-template-columns: 1fr;
}
.tester-row {
flex-direction: column;
align-items: stretch;
}
}
</style>
<div class="api-tester-container">
<h2>🛠️ API Tester</h2>
<!-- API Auswahl -->
<div class="tester-form-group">
<label for="endpoint">API Endpunkt</label>
<select id="endpoint" class="tester-input" onchange="checkEndpoint()">
<?php foreach ($apiOptions as $opt): ?>
<option value="<?= $opt ?>"><?= $opt ?></option>
<option value="<?= htmlspecialchars($opt) ?>">
<?= htmlspecialchars($opt) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<!-- Methode + Function -->
<div class="tester-grid">
<div class="tester-form-group">
<label for="method">HTTP Methode</label>
<select id="method" class="tester-input">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
<option value="PATCH">PATCH</option>
<option value="OPTIONS">OPTIONS</option>
</select>
</div>
<div class="tester-form-group" id="function-group">
<label for="function-header">Function Header</label>
<input type="text" id="function-header" class="tester-input" placeholder="z.B. IPs, Ueberstunden">
</div>
</div>
<!-- Token -->
<div class="tester-form-group">
<label for="token">Authorization Token</label>
<input type="text" id="token" class="tester-input" value="<?= $defaultToken ?>">
<div class="tester-row">
<input type="text" id="token" class="tester-input" value="<?= htmlspecialchars($defaultToken) ?>">
<label class="tester-checkbox">
<input type="checkbox" id="send-token" checked>
Mitsenden
</label>
</div>
</div>
<!-- Eigene Header -->
<div class="tester-form-group">
<label for="custom-headers">
Zusätzliche Header (JSON)
</label>
<textarea id="custom-headers" class="tester-textarea" placeholder='{
"X-Test": "123",
"Client": "Frontend"
}'></textarea>
</div>
<!-- JSON Body -->
<div class="tester-form-group" id="json-body-group">
<label for="json-body">
JSON Body
</label>
<textarea id="json-body" class="tester-textarea" placeholder='{
"test": true
}'></textarea>
</div>
<!-- Datei Upload -->
<div class="tester-form-group hidden" id="file-group">
<label for="files">Dateien (files)</label>
<label for="files">Dateien Upload</label>
<input type="file" id="files" class="tester-input" multiple>
</div>
<button class="tester-btn-send" onclick="sendApiRequest()">Anfrage senden 🚀</button>
<!-- Buttons -->
<div class="tester-grid">
<button class="tester-btn-send" onclick="sendApiRequest()">
Anfrage senden 🚀
</button>
<button class="tester-btn-send" onclick="clearResponse()">
Ausgabe leeren 🧹
</button>
</div>
<!-- Ausgabe -->
<div id="tester-response-container">
<div style="text-align: center; color: #888; padding: 10px;">Bereit für Anfragen.</div>
<div style="text-align:center; opacity:0.7; padding:15px;">
Bereit für Anfragen.
</div>
</div>
</div>
<script>
// ======================================================
// CHECK ENDPOINT
// ======================================================
function checkEndpoint() {
const endpoint = document.getElementById('endpoint').value;
const fileGroup = document.getElementById('file-group');
const functionGroup = document.getElementById('function-group');
const jsonBodyGroup = document.getElementById('json-body-group');
const methodSelect = document.getElementById('method');
// Standard: alles ausblenden
// reset
fileGroup.classList.add('hidden');
functionGroup.classList.remove('hidden');
jsonBodyGroup.classList.remove('hidden');
// upload api
if (endpoint.includes('upload.php')) {
if (endpoint === 'api/upload.php') {
// Nur bei exakt api/upload.php Dateien anzeigen
fileGroup.classList.remove('hidden');
functionGroup.classList.add('hidden');
jsonBodyGroup.classList.add('hidden');
methodSelect.value = 'POST';
}
if (endpoint === 'api/get.php') {
// get api
if (endpoint.includes('get.php')) {
methodSelect.value = 'GET';
}
}
// ======================================================
// CLEAR RESPONSE
// ======================================================
function clearResponse() {
document.getElementById('tester-response-container').innerHTML = `
<div style="text-align:center; opacity:0.7; padding:15px;">
Ausgabe geleert.
</div>
`;
}
// ======================================================
// SEND REQUEST
// ======================================================
async function sendApiRequest() {
const endpoint = document.getElementById('endpoint').value;
const method = document.getElementById('method').value;
const token = document.getElementById('token').value;
const functionHeader = document.getElementById('function-header').value;
const customHeadersInput = document.getElementById('custom-headers').value;
const jsonBodyInput = document.getElementById('json-body').value;
const fileInput = document.getElementById('files');
const sendToken = document.getElementById('send-token').checked;
const responseContainer = document.getElementById('tester-response-container');
responseContainer.innerHTML = '<div style="text-align: center; padding: 10px;">Lade...</div>';
responseContainer.innerHTML = `
<div style="text-align:center; padding:15px;">
Anfrage läuft...
</div>
`;
let headers = {
'Authorization': token
};
let headers = {};
if (endpoint.includes('get.php') && functionHeader) {
// Token
if (sendToken && token.trim() !== '') {
headers['Authorization'] = token;
}
// Function Header
if (functionHeader.trim() !== '') {
headers['Function'] = functionHeader;
}
// Custom Headers
if (customHeadersInput.trim() !== '') {
try {
const customHeaders = JSON.parse(customHeadersInput);
headers = {
...headers,
...customHeaders
};
} catch (e) {
responseContainer.innerHTML = `
<div style="color:#dc3545; padding:15px;">
Fehlerhafte Header JSON Struktur.
</div>
`;
return;
}
}
let options = {
method: method,
headers: headers
};
// Upload
if (endpoint.includes('upload.php') && fileInput.files.length > 0) {
const formData = new FormData();
for (let i = 0; i < fileInput.files.length; i++) {
formData.append('files[]', fileInput.files[i]);
}
options.body = formData;
} else {
// JSON Body
if (
['POST', 'PUT', 'PATCH', 'DELETE']
.includes(method) &&
jsonBodyInput.trim() !== ''
) {
try {
JSON.parse(jsonBodyInput);
headers['Content-Type'] = 'application/json';
options.body = jsonBodyInput;
} catch (e) {
responseContainer.innerHTML = `
<div style="color:#dc3545; padding:15px;">
Fehlerhafte JSON Body Struktur.
</div>
`;
return;
}
}
}
const startedAt = performance.now();
try {
const response = await fetch(endpoint, options);
const duration = (performance.now() - startedAt).toFixed(0);
const status = response.status;
const rawText = await response.text();
let resultText = rawText;
// JSON hübsch formatieren
try {
const json = JSON.parse(rawText);
resultText = JSON.stringify(json, null, 2);
} catch (e) {}
const badgeClass = status >= 200 && status < 300 ? 'tester-status-200' : 'tester-status-error';
let badgeClass = 'tester-status-error';
if (status >= 200 && status < 300) {
badgeClass = 'tester-status-200';
} else if (status >= 300 && status < 400) {
badgeClass = 'tester-status-warning';
}
responseContainer.innerHTML = `
<div class="tester-badge ${badgeClass}">Status: ${status}</div>
<pre id="tester-output-content"></pre>
`;
<div class="tester-badge ${badgeClass}">
Status: ${status}
</div>
<div class="tester-response-info">
⏱️ ${duration} ms
</div>
<button
class="tester-small-btn"
onclick="copyResponse()"
>
Antwort kopieren
</button>
<pre id="tester-output-content"></pre>
`;
document.getElementById('tester-output-content').textContent = resultText;
} catch (error) {
responseContainer.innerHTML = `<div style="color:#dc3545; padding: 10px;">Fehler: ${error.message}</div>`;
responseContainer.innerHTML = `
<div style="color:#dc3545; padding:15px;">
Fehler: ${error.message}
</div>
`;
}
}
// Initial check
// ======================================================
// COPY RESPONSE
// ======================================================
function copyResponse() {
const content = document.getElementById('tester-output-content');
if (!content) {
return;
}
navigator.clipboard.writeText(content.textContent);
alert('Antwort kopiert.');
}
// Initial
checkEndpoint();
</script>
+58 -58
View File
@@ -1,14 +1,14 @@
<?php
$conn = db_connect();
$sql = "SELECT DATE(FIRST_REQUEST) as tag, COUNT(*) as anzahl FROM logs WHERE WEBSITE = 'backend.blackpi.ipv64.net' GROUP BY DATE(FIRST_REQUEST) ORDER BY DATE(FIRST_REQUEST)";
$sql = "SELECT DATE(FIRST_REQUEST) as tag, COUNT(*) as anzahl FROM logs WHERE WEBSITE = 'backend.mrhossa.de' GROUP BY DATE(FIRST_REQUEST) ORDER BY DATE(FIRST_REQUEST)";
$result = $conn->query($sql);
$labels = [];
$data = [];
if ($result->num_rows > 0) {
while($row = $result->fetch_assoc()) {
while ($row = $result->fetch_assoc()) {
$labels[] = $row['tag'];
$data[] = (int)$row['anzahl'];
}
@@ -33,71 +33,71 @@ $conn->close();
<canvas id="logChart" width="600" height="400"></canvas>
<script>
const ctx = document.getElementById('logChart').getContext('2d');
const logChart = new Chart(ctx, {
type: 'line',
data: {
labels: <?php echo json_encode($labels); ?>,
datasets: [{
label: 'Logs pro Tag',
data: <?php echo json_encode($data); ?>,
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
fill: true,
tension: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Logs pro Tag'
},
legend: {
display: true,
position: 'top'
},
zoom: {
pan: {
enabled: true,
mode: 'x',
const ctx = document.getElementById('logChart').getContext('2d');
const logChart = new Chart(ctx, {
type: 'line',
data: {
labels: <?php echo json_encode($labels); ?>,
datasets: [{
label: 'Logs pro Tag',
data: <?php echo json_encode($data); ?>,
borderColor: 'rgba(75, 192, 192, 1)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
fill: true,
tension: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Logs pro Tag'
},
legend: {
display: true,
position: 'top'
},
zoom: {
wheel: {
pan: {
enabled: true,
mode: 'x',
},
mode: 'x',
}
}
},
interaction: {
mode: 'index',
intersect: false
},
scales: {
x: {
title: {
display: true,
text: 'Datum'
zoom: {
wheel: {
enabled: true,
},
mode: 'x',
}
}
},
y: {
min: 0,
max: 100,
title: {
display: true,
text: 'Anzahl Logs'
interaction: {
mode: 'index',
intersect: false
},
scales: {
x: {
title: {
display: true,
text: 'Datum'
}
},
ticks: {
stepSize: 20
},
beginAtZero: true
y: {
min: 0,
max: 100,
title: {
display: true,
text: 'Anzahl Logs'
},
ticks: {
stepSize: 20
},
beginAtZero: true
}
}
}
}
});
});
</script>
</body>
+24 -24
View File
@@ -40,7 +40,7 @@ if (!$useCustom) {
SELECT `CLIENT_IP`, `LAND`, `REQUEST_URL`, `COUNTER`, `FIRST_REQUEST`, `WEBSITE`
FROM logs
WHERE DATE(FIRST_REQUEST) = CURDATE()
AND (WEBSITE = 'backend.blackpi.ipv64.net')
AND (WEBSITE = 'backend.mrhossa.de')
AND CLIENT_IP NOT IN ($devips_sql)
ORDER BY FIRST_REQUEST DESC
";
@@ -50,7 +50,7 @@ if (!$useCustom) {
$sql = "
SELECT CLIENT_IP, LAND, USER_AGENT, SUM(counter) AS gesamt_counter
FROM logs
WHERE (WEBSITE = 'backend.blackpi.ipv64.net')
WHERE (WEBSITE = 'backend.mrhossa.de')
AND CLIENT_IP NOT IN ($devips_sql)
GROUP BY CLIENT_IP
ORDER BY gesamt_counter DESC
@@ -62,7 +62,7 @@ if (!$useCustom) {
SELECT `CLIENT_IP`, `LAND`, `REQUEST_URL`, `COUNTER`, `FIRST_REQUEST`, `WEBSITE`
FROM logs
WHERE `SERVER_IP` = 'node1'
AND (WEBSITE = 'backend.blackpi.ipv64.net')
AND (WEBSITE = 'backend.mrhossa.de')
ORDER BY `FIRST_REQUEST` DESC
";
break;
@@ -72,7 +72,7 @@ if (!$useCustom) {
SELECT `CLIENT_IP`, `LAND`, `REQUEST_URL`, `COUNTER`, `FIRST_REQUEST`, `WEBSITE`
FROM logs
WHERE `SERVER_IP` = 'node2'
AND (WEBSITE = 'backend.blackpi.ipv64.net')
AND (WEBSITE = 'backend.mrhossa.de')
ORDER BY `FIRST_REQUEST` DESC
";
break;
@@ -82,7 +82,7 @@ if (!$useCustom) {
SELECT `CLIENT_IP`, `LAND`, `REQUEST_URL`, `COUNTER`, `FIRST_REQUEST`, `WEBSITE`
FROM logs
WHERE `SERVER_IP` = 'node3'
AND (WEBSITE = 'backend.blackpi.ipv64.net')
AND (WEBSITE = 'backend.mrhossa.de')
ORDER BY `FIRST_REQUEST` DESC
";
break;
@@ -92,7 +92,7 @@ if (!$useCustom) {
SELECT `CLIENT_IP`, `LAND`, `REQUEST_URL`, `COUNTER`, `FIRST_REQUEST`, `WEBSITE`
FROM logs
WHERE `SERVER_IP` = 'node4'
AND (WEBSITE = 'backend.blackpi.ipv64.net')
AND (WEBSITE = 'backend.mrhossa.de')
ORDER BY `FIRST_REQUEST` DESC
";
break;
@@ -101,7 +101,7 @@ if (!$useCustom) {
$sql = "
SELECT `CLIENT_IP`, `LAND`, `REQUEST_URL`, `COUNTER`, `FIRST_REQUEST`, `WEBSITE`
FROM logs
WHERE (WEBSITE = 'backend.blackpi.ipv64.net')
WHERE (WEBSITE = 'backend.mrhossa.de')
AND CLIENT_IP NOT IN ($devips_sql)
ORDER BY FIRST_REQUEST DESC
";
@@ -116,7 +116,7 @@ if (!$useCustom) {
<select name="filter" onchange="this.form.submit()">
<?php
$options = ['alle','today','node1','node2','node3','node4','counterperip'];
$options = ['alle', 'today', 'node1', 'node2', 'node3', 'node4', 'counterperip'];
foreach ($options as $opt) {
$sel = ($filter === $opt) ? 'selected' : '';
echo "<option value='$opt' $sel>$opt</option>";
@@ -150,25 +150,25 @@ if ($erg = $conn->query($sql)) {
?>
<?php if (!$daten): ?>
<b>Keine Daten gefunden</b>
<b>Keine Daten gefunden</b>
<?php else: ?>
<div style="overflow-x:auto;">
<table border="1" cellpadding="5" cellspacing="0" style="border-collapse:collapse;width:100%;">
<tr>
<?php foreach ($daten[0] as $key => $_): ?>
<th><?= htmlspecialchars($key) ?></th>
<?php endforeach; ?>
</tr>
<div style="overflow-x:auto;">
<table border="1" cellpadding="5" cellspacing="0" style="border-collapse:collapse;width:100%;">
<tr>
<?php foreach ($daten[0] as $key => $_): ?>
<th><?= htmlspecialchars($key) ?></th>
<?php endforeach; ?>
</tr>
<?php foreach ($daten as $row): ?>
<tr>
<?php foreach ($row as $val): ?>
<td><?= htmlspecialchars($val ?? '') ?></td>
<?php foreach ($daten as $row): ?>
<tr>
<?php foreach ($row as $val): ?>
<td><?= htmlspecialchars($val ?? '') ?></td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</table>
</div>
</table>
</div>
<?php endif; ?>
@@ -0,0 +1,134 @@
<?php
include_once(__DIR__ . '/../controller/NotifyController.php');
?>
<div class="arbeitsberichte-wrapper">
<div class="vergabe-form">
<h3>Neue Nachricht anlegen</h3>
<form id="addNotificationForm" class="vergabe-grid">
<div class="form-group">
<label>Titel</label>
<input type="text" name="titel" id="in-titel" required>
</div>
<div class="form-group">
<label>Nachricht</label>
<input type="text" name="nachricht" id="in-nachricht" required>
</div>
<button type="submit" class="btn-submit-vergabe">💾 Speichern</button>
</form>
</div>
<div class="table-container" style="margin-top: 30px;">
<h3>Nachrichten Verwaltung</h3>
<div id="tableWrapper">
<p>Lade Daten...</p>
</div>
</div>
</div>
<script src="/js/AjaxMethods.js"></script>
<script src="/js/ajaxApi/BenachrichtigungenAjaxMethods.js"></script>
<script src="/js/AjaxRenderTables.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const tableWrapper = document.getElementById('tableWrapper');
function renderNotificationsTabelle(entries) {
if (!entries || entries.length === 0) {
tableWrapper.innerHTML = '<p>Keine Nachrichten vorhanden.</p>';
return;
}
let html = `
<table class="db-table mobile-responsive-table">
<thead>
<tr>
<th>Titel</th><th>Nachricht</th><th>Gesehen von</th>
<th style="width: 220px;">Aktion</th>
</tr>
</thead>
<tbody>
`;
for (const e of entries) {
html += `
<tr data-id="${escapeHtml(e.ID)}">
<td data-label="Titel">
<input type="text" class="edit-titel" value="${escapeHtml(e.TITEL)}">
</td>
<td data-label="Nachricht">
<input type="text" class="edit-nachricht" value="${escapeHtml(e.NACHRICHT)}">
</td>
<td data-label="Gesehen von">${escapeHtml(e.GESEHEN_ANZAHL)} Nutzer</td>
<td data-label="Aktion">
<div style="display:flex; flex-direction:column; gap:6px; align-items:stretch;">
<button type="button" class="btn-edit" data-id="${escapeHtml(e.ID)}">Speichern</button>
<button type="button" class="btn-delete" data-id="${escapeHtml(e.ID)}">Löschen</button>
</div>
</td>
</tr>`;
}
html += `</tbody></table>`;
tableWrapper.innerHTML = html;
tableWrapper.querySelectorAll('.btn-edit').forEach(btn => {
btn.addEventListener('click', async () => {
const row = tableWrapper.querySelector(`tr[data-id="${btn.dataset.id}"]`);
const titel = row.querySelector('.edit-titel').value.trim();
const nachricht = row.querySelector('.edit-nachricht').value.trim();
if (!titel || !nachricht) return;
try {
await editNotificationEntry(btn.dataset.id, titel, nachricht);
await ladeListe();
} catch (err) {
alert('Fehler beim Speichern: ' + err.message);
}
});
});
tableWrapper.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Möchtest du diese Nachricht wirklich löschen? Dies entfernt auch alle "Gesehen"-Markierungen dazu.')) return;
try {
await deleteNotificationEntry(btn.dataset.id);
await ladeListe();
} catch (err) {
alert('Fehler beim Löschen: ' + err.message);
}
});
});
}
async function ladeListe() {
try {
const data = await getNotificationEntries();
renderNotificationsTabelle(data.entries);
} catch (err) {
tableWrapper.innerHTML = `<p>❌ ${escapeHtml(err.message)}</p>`;
}
}
document.getElementById('addNotificationForm').addEventListener('submit', async (e) => {
e.preventDefault();
const titel = document.getElementById('in-titel').value.trim();
const nachricht = document.getElementById('in-nachricht').value.trim();
if (!titel || !nachricht) return;
try {
await addNotificationEntry(titel, nachricht);
e.target.reset();
await ladeListe();
} catch (err) {
alert('Fehler beim Speichern: ' + err.message);
}
});
ladeListe();
});
</script>
+1 -1
View File
@@ -120,7 +120,7 @@ $count = is_array($displayData) ? count($displayData) : 0;
<div class="main">
<div class="content">
<h2 style="display: flex; justify-content: space-between; align-items: center;">
<span>📊 Berichtte-Vorschau:</span>
<span>📊 Berichte-Vorschau:</span>
<div>
<a href="?service=<?= $selectedService ?>&mode=fk">
+212
View File
@@ -0,0 +1,212 @@
<?php
include_once(__DIR__ . '/../controller/DataController.php');
include_once(__DIR__ . '/../controller/DatabaseController.php');
$alleFaecher = DatabaseShow("Fach", "ID, FACH");
$alleRaeume = DatabaseShow("Raum", "ID, RAUM_NR");
$alleLehrer = DatabaseShow("Lehrer", "ID, NAME");
/*
1 => ["fach_id" => 1, "raum_id" => 5, "lehrer_id" => 8], // LF1
1 => ["fach_id" => 2, "raum_id" => 4, "lehrer_id" => 11], // LF2
1 => ["fach_id" => 3, "raum_id" => 1, "lehrer_id" => 2], // LF3
1 => ["fach_id" => 4, "raum_id" => 1, "lehrer_id" => 5], // LF4
6 => ["fach_id" => 5, "raum_id" => 3, "lehrer_id" => 1], // LF5
1 => ["fach_id" => 6, "raum_id" => 5, "lehrer_id" => 4], // Sozi
1 => ["fach_id" => 8, "raum_id" => 5, "lehrer_id" => 7], // VWL
1 => ["fach_id" => 9, "raum_id" => 5, "lehrer_id" => 3], // DE
1 => ["fach_id" => 10, "raum_id" => 5, "lehrer_id" => 3], // ENG
*/
$Stundenplan = [
"MO" => [
1 => ["fach_id" => 3, "raum_id" => 1, "lehrer_id" => 2], // LF3
2 => ["fach_id" => 3, "raum_id" => 1, "lehrer_id" => 2], // LF3
3 => ["fach_id" => 3, "raum_id" => 1, "lehrer_id" => 2], // LF3
4 => ["fach_id" => 6, "raum_id" => 5, "lehrer_id" => 4], // Sozi
5 => ["fach_id" => 10, "raum_id" => 5, "lehrer_id" => 3], // Eng
6 => ["fach_id" => 10, "raum_id" => 5, "lehrer_id" => 3], // Eng
7 => ["fach_id" => 2, "raum_id" => 3, "lehrer_id" => 11], // LF2
8 => ["fach_id" => 2, "raum_id" => 3, "lehrer_id" => 11], // LF2
],
"DI" => [
1 => ["fach_id" => 5, "raum_id" => 3, "lehrer_id" => 1], // LF5
2 => ["fach_id" => 5, "raum_id" => 3, "lehrer_id" => 1], // LF5
3 => ["fach_id" => 5, "raum_id" => 1, "lehrer_id" => 5], // LF5
4 => ["fach_id" => 2, "raum_id" => 3, "lehrer_id" => 6], // LF2
5 => ["fach_id" => 2, "raum_id" => 3, "lehrer_id" => 6], // LF2
],
"MI" => [
1 => ["fach_id" => 8, "raum_id" => 5, "lehrer_id" => 7], // VWL
2 => ["fach_id" => 8, "raum_id" => 5, "lehrer_id" => 7], // VWL
3 => ["fach_id" => 9, "raum_id" => 5, "lehrer_id" => 3], // DE
4 => ["fach_id" => 9, "raum_id" => 5, "lehrer_id" => 3], // DE
5 => ["fach_id" => 2, "raum_id" => 4, "lehrer_id" => 6], // LF2
6 => ["fach_id" => 2, "raum_id" => 4, "lehrer_id" => 6], // LF2
7 => ["fach_id" => 8, "raum_id" => 5, "lehrer_id" => 7], // VWL
],
"DO" => [
1 => ["fach_id" => 3, "raum_id" => 1, "lehrer_id" => 2], // LF3
2 => ["fach_id" => 3, "raum_id" => 1, "lehrer_id" => 2], // LF3
3 => ["fach_id" => 3, "raum_id" => 1, "lehrer_id" => 2], // LF3
4 => ["fach_id" => 6, "raum_id" => 5, "lehrer_id" => 4], // Sozi
5 => ["fach_id" => 6, "raum_id" => 5, "lehrer_id" => 4], // Sozi
6 => ["fach_id" => 4, "raum_id" => 1, "lehrer_id" => 5], // LF4
7 => ["fach_id" => 4, "raum_id" => 1, "lehrer_id" => 5], // LF4
8 => ["fach_id" => 4, "raum_id" => 1, "lehrer_id" => 5], // LF4
],
"FR" => [
1 => ["fach_id" => 1, "raum_id" => 5, "lehrer_id" => 8], // LF1
2 => ["fach_id" => 1, "raum_id" => 5, "lehrer_id" => 8], // LF1
3 => ["fach_id" => 1, "raum_id" => 5, "lehrer_id" => 8], // LF1
4 => ["fach_id" => 5, "raum_id" => 3, "lehrer_id" => 1], // LF5
5 => ["fach_id" => 5, "raum_id" => 3, "lehrer_id" => 1], // LF5
6 => ["fach_id" => 5, "raum_id" => 3, "lehrer_id" => 1], // LF5
]
];
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Berufsschulbericht</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="main">
<div class="content" style="max-width: 1100px; margin: 0 auto; padding: 20px;">
<div class="form-container">
<h3 style="margin-top: 0;">Berufsschulbericht erfassen</h3>
<form method="post">
<div class="form-group" style="max-width: 220px; margin-bottom: 25px;">
<label for="datum">Datum auswählen</label>
<input type="date" name="datum" id="datum" required>
</div>
<div class="vergabe-grid"
style="font-weight: bold; padding-bottom: 8px; margin-bottom: 12px; border-bottom: 1px solid var(--card-border);">
<div>Stunde</div>
<div>Fach</div>
<div>Raum</div>
<div>Lehrer</div>
<div>Inhalt / Thema</div>
</div>
<?php for ($i = 1; $i <= 8; $i++): ?>
<div class="vergabe-grid" style="margin-bottom: 10px; align-items: center;">
<div>
<span class="badge"
style="display: inline-block; min-width: 90px; text-align: center; padding: 8px 12px; font-weight: bold;">Stunde
<?= $i ?></span>
</div>
<div class="form-group">
<select name="stunden[<?= $i ?>][fach_id]" data-stunde="<?= $i ?>" data-type="fach_id">
<option value="">-- Fach --</option>
<?php foreach ($alleFaecher as $row): ?>
<option value="<?= $row['ID'] ?>"><?= htmlspecialchars($row['FACH']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<select name="stunden[<?= $i ?>][raum_id]" data-stunde="<?= $i ?>" data-type="raum_id">
<option value="">-- Raum --</option>
<?php foreach ($alleRaeume as $row): ?>
<option value="<?= $row['ID'] ?>"><?= htmlspecialchars($row['RAUM_NR']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<select name="stunden[<?= $i ?>][lehrer_id]" data-stunde="<?= $i ?>" data-type="lehrer_id">
<option value="">-- Lehrer --</option>
<?php foreach ($alleLehrer as $row): ?>
<option value="<?= $row['ID'] ?>"><?= htmlspecialchars($row['NAME']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<input type="text" name="stunden[<?= $i ?>][inhalt_text]" placeholder="Inhalt der Stunde..."
style="margin: 0; padding: 10px;">
</div>
</div>
<?php endfor; ?>
<div style="margin-top: 25px; text-align: right;">
<button type="submit" class="btn-submit-vergabe"
style="width: 220px; display: inline-flex; font-weight: bold;">
Bericht speichern
</button>
</div>
</form>
</div>
</div>
</div>
<script>
const stundenplan = <?= json_encode($Stundenplan) ?>;
const wochentage = ["SO", "MO", "DI", "MI", "DO", "FR", "SA"];
document.getElementById('datum').addEventListener('change', function() {
const gewaehltesDatum = new Date(this.value);
if (isNaN(gewaehltesDatum)) return;
let tagKey = wochentage[gewaehltesDatum.getDay()];
if (tagKey === "SO" || tagKey === "SA") {
tagKey = "MO";
}
const tagesPlan = stundenplan[tagKey] || {};
document.querySelectorAll('.vergabe-grid select').forEach(select => {
const stunde = select.getAttribute('data-stunde');
const type = select.getAttribute('data-type');
const vordefinierteId = (tagesPlan[stunde] && tagesPlan[stunde][type]) ? tagesPlan[stunde][
type
] : "";
select.value = vordefinierteId;
});
});
</script>
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$datum = $_POST['datum'] ?? null;
$werte = [$datum];
for ($i = 1; $i <= 8; $i++) {
$stunde = $_POST['stunden'][$i] ?? [];
$werte[] = !empty($stunde['fach_id']) ? $stunde['fach_id'] : null;
$werte[] = !empty($stunde['lehrer_id']) ? $stunde['lehrer_id'] : null;
$werte[] = !empty($stunde['raum_id']) ? $stunde['raum_id'] : null;
$werte[] = !empty($stunde['inhalt_text']) ? $stunde['inhalt_text'] : null;
}
DatabaseInsert(
"Berufsschulberichte",
"DATUM, FACH_S1, LEHRER_S1, RAUM_S1, INHALT_S1, FACH_S2, LEHRER_S2, RAUM_S2, INHALT_S2, FACH_S3, LEHRER_S3, RAUM_S3, INHALT_S3, FACH_S4, LEHRER_S4, RAUM_S4, INHALT_S4, FACH_S5, LEHRER_S5, RAUM_S5, INHALT_S5, FACH_S6, LEHRER_S6, RAUM_S6, INHALT_S6, FACH_S7, LEHRER_S7, RAUM_S7, INHALT_S7, FACH_S8, LEHRER_S8, RAUM_S8, INHALT_S8",
...$werte
);
echo "<div class='status-msg' style='position: fixed; bottom: 20px; right: 20px; z-index: 10000;'>Bericht gespeichert!</div>";
}
?>
</body>
</html>
+122
View File
@@ -0,0 +1,122 @@
<div class="arbeitsberichte-wrapper">
<div class="vergabe-form">
<h3>Datei hochladen</h3>
<form id="uploadForm" enctype="multipart/form-data">
<div class="form-group">
<label>Datei auswählen</label>
<input type="file" name="datei" id="in-datei" required>
</div>
<button type="submit" class="btn-submit-vergabe">⬆️ Hochladen</button>
</form>
<p id="uploadStatus" style="margin-top: 10px;"></p>
</div>
<div class="table-container" style="margin-top: 30px;">
<h3>Meine Dateien</h3>
<div id="tableWrapper">
<p>Lade Daten...</p>
</div>
</div>
</div>
<script src="/js/AjaxMethods.js"></script>
<script src="/js/ajaxApi/DateienAjaxMethods.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const tableWrapper = document.getElementById('tableWrapper');
const uploadStatus = document.getElementById('uploadStatus');
function formatBytes(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
function renderDateienTabelle(entries) {
if (!entries || entries.length === 0) {
tableWrapper.innerHTML = '<p>Keine Dateien vorhanden.</p>';
return;
}
let html = `
<table class="db-table mobile-responsive-table">
<thead>
<tr>
<th>Dateiname</th><th>Größe</th><th>Hochgeladen am</th>
<th style="width: 180px;">Aktion</th>
</tr>
</thead>
<tbody>
`;
for (const e of entries) {
html += `
<tr data-id="${escapeHtml(e.ID)}">
<td data-label="Dateiname">${escapeHtml(e.ORIGINALNAME)}</td>
<td data-label="Größe">${formatBytes(e.GROESSE)}</td>
<td data-label="Hochgeladen am">${escapeHtml(e.HOCHGELADEN_AM)}</td>
<td data-label="Aktion">
<div style="display:flex; gap:6px;">
<a href="/api/js/handler/DateienDownloadHandler.php?id=${escapeHtml(e.ID)}" class="btn-submit-vergabe" style="text-decoration:none; padding:6px 12px;">⬇️ Download</a>
<button type="button" class="btn-delete" data-id="${escapeHtml(e.ID)}">🗑️</button>
</div>
</td>
</tr>`;
}
html += `</tbody></table>`;
tableWrapper.innerHTML = html;
tableWrapper.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Diese Datei wirklich löschen?')) return;
try {
await deleteDateiEntry(btn.dataset.id);
await ladeListe();
} catch (err) {
alert('Fehler beim Löschen: ' + err.message);
}
});
});
}
async function ladeListe() {
try {
const data = await getDateienEntries();
renderDateienTabelle(data.entries);
} catch (err) {
tableWrapper.innerHTML = `<p>❌ ${escapeHtml(err.message)}</p>`;
}
}
document.getElementById('uploadForm').addEventListener('submit', async (e) => {
e.preventDefault();
const fileInput = document.getElementById('in-datei');
if (!fileInput.files.length) return;
const formData = new FormData();
formData.append('datei', fileInput.files[0]);
uploadStatus.textContent = 'Lade hoch...';
try {
const response = await fetch('/api/js/handler/DateienUploadHandler.php', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
});
const data = await parseJsonResponse(response);
if (data.error) throw new Error(data.error);
uploadStatus.textContent = '✅ Erfolgreich hochgeladen';
fileInput.value = '';
await ladeListe();
} catch (err) {
uploadStatus.textContent = '❌ ' + err.message;
}
});
ladeListe();
});
</script>
+17 -17
View File
@@ -57,9 +57,9 @@ $domains = DatabaseShow("Domains", "*");
</h2>
<?php if ($statusMsg): ?>
<div class="status-msg">
<?= htmlspecialchars($statusMsg) ?>
</div>
<div class="status-msg">
<?= htmlspecialchars($statusMsg) ?>
</div>
<?php endif; ?>
@@ -67,27 +67,27 @@ $domains = DatabaseShow("Domains", "*");
<?php foreach ($domains as $domain): ?>
<form method="post" class="admin-card">
<form method="post" class="admin-card">
<input type="hidden" name="id" value="<?= $domain["ID"] ?>">
<input type="hidden" name="id" value="<?= $domain["ID"] ?>">
<label>Subdomain</label>
<label>Subdomain</label>
<input type="text" name="subdomain" value="<?= htmlspecialchars($domain["SUBDOMAIN"]) ?>">
<input type="text" name="subdomain" value="<?= htmlspecialchars($domain["SUBDOMAIN"]) ?>">
<div style="margin-top:10px; display:flex; gap:10px;">
<div style="margin-top:10px; display:flex; gap:10px;">
<button type="submit" name="save_domain" class="btn-save">
Speichern
</button>
<button type="submit" name="save_domain" class="btn-save">
Speichern
</button>
<button type="submit" name="delete_domain" class="btn-delete">
Löschen
</button>
<button type="submit" name="delete_domain" class="btn-delete">
Löschen
</button>
</div>
</div>
</form>
</form>
<?php endforeach; ?>
@@ -100,7 +100,7 @@ $domains = DatabaseShow("Domains", "*");
<label>Subdomain</label>
<input type="text" name="subdomain" placeholder="z.B. kleinerpenis.blackpi.ipv64.net">
<input type="text" name="subdomain" placeholder="z.B. kleinerpenis.mrhossa.de">
<button type="submit" name="add_domain" class="btn-add">
Domain hinzufügen
+178
View File
@@ -0,0 +1,178 @@
<div>
<div class="vergabe-form">
<form id="addForm" class="vergabe-grid">
<div class="form-group">
<label>Artikel</label>
<input type="text" name="artikel" id="artikel" required>
</div>
<div class="form-group">
<label>Menge</label>
<input type="text" name="menge" id="menge" required>
</div>
<div class="form-group">
<label>Typ</label>
<select name="typ" id="typ">
<option value="einmalig">Einmalig</option>
<option value="vorlage">Vorlage (wiederkehrend)</option>
</select>
</div>
<button type="submit" class="btn-submit-vergabe">
Speichern
</button>
</form>
</div>
<div class="table-container" style="margin-top: 30px;">
<h3>Einkaufsliste</h3>
<div id="tableWrapper">
<p>Lade Daten...</p>
</div>
</div>
</div>
<script src="/js/AjaxMethods.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const tableWrapper = document.getElementById('tableWrapper');
const table = 'Einkaufsliste';
const spaltenKonfig = [{
key: 'PRODUKT',
label: 'Artikel'
},
{
key: 'MENGE',
label: 'Menge'
},
{
key: 'TYP',
label: 'Typ'
},
];
// Eigene Render-Funktion, da hier je nach TYP unterschiedlich
// gehandelt werden muss (Löschen vs. Erledigt-Markieren).
function renderEinkaufsliste(entries) {
if (entries && !Array.isArray(entries)) {
entries = Object.values(entries);
}
if (!entries || entries.length === 0) {
tableWrapper.innerHTML = '<p>Keine Einträge vorhanden.</p>';
return;
}
let html = `
<table class="db-table mobile-responsive-table">
<thead>
<tr>
${spaltenKonfig.map(s => `<th>${escapeHtml(s.label)}</th>`).join('')}
<th style="width: 100px;">Aktion</th>
</tr>
</thead>
<tbody>
`;
for (const entry of entries) {
const istVorlage = entry.TYP === 'vorlage';
const label = istVorlage ? 'Erledigt' : 'Löschen';
html += `<tr>`;
for (const s of spaltenKonfig) {
let wert = entry[s.key];
if (s.key === 'TYP') {
wert = istVorlage ? 'Vorlage' : 'Einmalig';
}
html += `<td data-label="${escapeHtml(s.label)}">${escapeHtml(wert)}</td>`;
}
html += `
<td data-label="Aktion">
<button type="button" class="btn-delete" data-id="${escapeHtml(entry.ID)}" data-typ="${escapeHtml(entry.TYP)}">
${label}
</button>
</td>
</tr>`;
}
html += `</tbody></table>`;
tableWrapper.innerHTML = html;
tableWrapper.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', async () => {
const id = btn.dataset.id;
const istVorlage = btn.dataset.typ === 'vorlage';
const frage = istVorlage ?
'Diesen Vorlagen-Artikel als erledigt markieren?' :
'Diesen Artikel wirklich endgültig löschen?';
if (!confirm(frage)) return;
try {
if (istVorlage) {
await DatabaseUpdate(
table, {
STATUS: 'erledigt'
},
[{
column: 'ID',
operator: '=',
value: id
}]
);
} else {
await DatabaseDelete(
table,
[{
column: 'ID',
operator: '=',
value: id
}]
);
}
await reloadTable();
} catch (error) {
alert('Fehler: ' + error.message);
}
});
});
}
async function reloadTable() {
try {
const data = await DatabaseShow(table, [], [{
column: 'STATUS',
operator: '=',
value: 'aktiv'
}]);
renderEinkaufsliste(data.entries);
} catch (error) {
tableWrapper.innerHTML = `<p>${escapeHtml(error.message)}</p>`;
}
}
document.getElementById('addForm').addEventListener('submit', async (e) => {
e.preventDefault();
const artikel = document.getElementById('artikel').value.trim();
const menge = document.getElementById('menge').value.trim();
const typ = document.getElementById('typ').value;
if (!artikel || !menge) return;
try {
await DatabaseInsert(
table,
['PRODUKT', 'MENGE', 'TYP', 'STATUS'],
[artikel, menge, typ, 'aktiv']
);
e.target.reset();
await reloadTable();
} catch (error) {
alert('Fehler beim Speichern: ' + error.message);
}
});
reloadTable();
});
</script>
+97
View File
@@ -0,0 +1,97 @@
<?php
include_once(__DIR__ . '/../controller/DataController.php');
include_once(__DIR__ . '/../controller/DatabaseController.php');
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_bericht'])) {
$prozess1 = $_POST['prozess1'];
$prozess2 = $_POST['prozess2'];
$prozess3 = $_POST['prozess3'];
$prozess4 = $_POST['prozess4'];
$startzeit = $_POST['startzeit'];
$endzeit = $_POST['endzeit'];
$datum = $_POST['datum'];
// IHK konforme zeiten berechnen und Überstunden berechnen
$soll = new DateTime($startzeit);
$soll->modify("+8 hours 30 minutes");
$randomMin = rand(1, 15);
$endePublicDt = clone $soll;
$endePublicDt->modify("+{$randomMin} minutes");
$endePublic = $endePublicDt->format("H:i");
$real = DateTime::createFromFormat("H:i", $endzeit);
$beginnDt = new DateTime($startzeit);
$arbeitsDiff = $beginnDt->diff($real);
$arbeitsMinuten = ($arbeitsDiff->h * 60) + $arbeitsDiff->i;
$sollMinuten = 8 * 60 + 30;
if ($arbeitsMinuten <= $sollMinuten) {
$overtime = 0;
} else {
$diff = $soll->diff($real);
$overtime = ($diff->h * 60) + $diff->i;
}
DatabaseInsert(
"Firmenberichte",
"DATUM, PROZESS_1, PROZESS_2, PROZESS_3, PROZESS_4, BEGINN, ENDE_REAL, ENDE_PUBLIC, OVERTIME",
$datum,
$prozess1,
$prozess2,
$prozess3,
$prozess4,
$startzeit,
$endzeit,
$endePublic,
$overtime
);
redirectToWebsite((new getSpecificData())->getReferer());
exit;
}
?>
<div>
<div class="berichte-container">
<form method="POST" class="berichte-form">
<div class="form-group">
<label>Prozess 1</label>
<input type="text" name="prozess1" required>
</div>
<div class="form-group">
<label>Prozess 2</label>
<input type="text" name="prozess2" required>
</div>
<div class="form-group">
<label>Prozess 3</label>
<input type="text" name="prozess3" required>
</div>
<div class="form-group">
<label>Prozess 4</label>
<input type="text" name="prozess4" required>
</div>
<div class="form-group">
<label>Startzeit</label>
<input type="time" name="startzeit" required>
</div>
<div class="form-group">
<label>Endzeit</label>
<input type="time" name="endzeit" required>
</div>
<div class="form-group">
<label>Datum</label>
<input type="date" name="datum" required>
</div>
<button type="submit" name="add_bericht" class="btn-submit-bericht">
Bericht Speichern
</button>
</form>
</div>
</div>
+98 -57
View File
@@ -1,4 +1,6 @@
<?php
include_once(__DIR__ . '/../controller/NotifyController.php');
$username = $_SESSION['username'] ?? 'Gast';
$is_superuser = $_SESSION['is_superuser'] ?? false;
$is_admin = $_SESSION['is_admin'] ?? false;
@@ -10,8 +12,24 @@ $db_res = DatabaseShow(
$username
);
// --- Ungesehene Benachrichtigung ermitteln ---
$notificationCtl = new Notification();
$offeneNachrichten = $notificationCtl->getUnseenMessages();
$aktuelleNachricht = $offeneNachrichten[0] ?? null; // immer nur die erste anzeigen
?>
<?php if ($aktuelleNachricht): ?>
<div class="notification-overlay open" id="notificationModal" data-nachricht-id="<?= htmlspecialchars($aktuelleNachricht['ID']) ?>">
<div class="notification-box">
<div class="notification-icon">📢</div>
<h3><?= htmlspecialchars($aktuelleNachricht['TITEL']) ?></h3>
<p><?= nl2br(htmlspecialchars($aktuelleNachricht['NACHRICHT'])) ?></p>
<button type="button" id="notificationConfirm">Verstanden</button>
</div>
</div>
<?php endif; ?>
<div class="home-dashboard">
<div class="links-container">
@@ -20,18 +38,18 @@ $db_res = DatabaseShow(
<div class="group-links" style="padding: 10px; color: inherit;">
<p style="margin: 5px 0;"><strong>Name:</strong> <code><?= htmlspecialchars($username) ?></code></p>
<?php if ($username != 'gast'): ?>
<p style="margin: 5px 0;"><strong>Nachname:</strong>
<code><?= htmlspecialchars($db_res[0]['NACHNAME']) ?></code>
</p>
<p style="margin: 5px 0;"><strong>E-Mail:</strong>
<code><?= htmlspecialchars($db_res[0]['EMAIL']) ?></code>
</p>
<p style="margin: 5px 0;"><strong>Benutzerrolle:</strong>
<code><?= htmlspecialchars($db_res[0]['ROLLE']) ?></code>
</p>
<p style="margin: 5px 0;"><strong>Home IP:</strong>
<code><?= htmlspecialchars($db_res[0]['HOME_IP']) ?></code>
</p>
<p style="margin: 5px 0;"><strong>Nachname:</strong>
<code><?= htmlspecialchars($db_res[0]['NACHNAME']) ?></code>
</p>
<p style="margin: 5px 0;"><strong>E-Mail:</strong>
<code><?= htmlspecialchars($db_res[0]['EMAIL']) ?></code>
</p>
<p style="margin: 5px 0;"><strong>Benutzerrolle:</strong>
<code><?= htmlspecialchars($db_res[0]['ROLLE']) ?></code>
</p>
<p style="margin: 5px 0;"><strong>Home IP:</strong>
<code><?= htmlspecialchars($db_res[0]['HOME_IP']) ?></code>
</p>
<?php endif ?>
</div>
</div>
@@ -41,18 +59,18 @@ $db_res = DatabaseShow(
<div class="group-links"
style="padding: 10px; display: flex; flex-direction: row; gap: 10px; flex-wrap: wrap;">
<?php if ((new getSpecificData())->getIsAdmin()): ?>
<span
style="background: #dc3545; color: white; padding: 4px 10px; border-radius: 15px; font-size: 0.8rem; font-weight: bold;">Admin</span>
<span
style="background: #dc3545; color: white; padding: 4px 10px; border-radius: 15px; font-size: 0.8rem; font-weight: bold;">Admin</span>
<?php endif; ?>
<?php if ((new getSpecificData())->getIsSuperuser()): ?>
<span
style="background: #ffc107; color: #000; padding: 4px 10px; border-radius: 15px; font-size: 0.8rem; font-weight: bold;">Superuser</span>
<span
style="background: #ffc107; color: #000; padding: 4px 10px; border-radius: 15px; font-size: 0.8rem; font-weight: bold;">Superuser</span>
<?php endif; ?>
<?php if (!(new getSpecificData())->getIsSuperuser() && !(new getSpecificData())->getIsAdmin()): ?>
<span
style="background: #6c757d; color: white; padding: 4px 10px; border-radius: 15px; font-size: 0.8rem; font-weight: bold;">Standard-Nutzer</span>
<span
style="background: #6c757d; color: white; padding: 4px 10px; border-radius: 15px; font-size: 0.8rem; font-weight: bold;">Standard-Nutzer</span>
<?php endif; ?>
</div>
</div>
@@ -67,21 +85,21 @@ $db_res = DatabaseShow(
<code>Aktiv</code>
</p>
<?php if ($is_admin): ?>
<p style="margin: 5px 0;"><strong>Client IP:</strong>
<code><?= htmlspecialchars((new getSpecificData())->getClientIP()) ?></code>
</p>
<p style="margin: 5px 0;"><strong>Server IP:</strong>
<code><?= htmlspecialchars((new getSpecificData())->getServerIP()) ?></code>
</p>
<p style="margin: 5px 0;"><strong>Geo IP:</strong>
<code><?= htmlspecialchars((new getSpecificData())->getGeoIP((new getSpecificData())->getClientIP())) ?></code>
</p>
<p style="margin: 5px 0;"><strong>Database IP:</strong>
<code><?= htmlspecialchars((new getSpecificData())->getDatabase()) ?></code>
</p>
<p style="margin: 5px 0;"><strong>Wetter:</strong>
<code><?= htmlspecialchars((new getSpecificData())->getWeather()['aktuell']) ?></code>
</p>
<p style="margin: 5px 0;"><strong>Client IP:</strong>
<code><?= htmlspecialchars((new getSpecificData())->getClientIP()) ?></code>
</p>
<p style="margin: 5px 0;"><strong>Server IP:</strong>
<code><?= htmlspecialchars((new getSpecificData())->getServerIP()) ?></code>
</p>
<p style="margin: 5px 0;"><strong>Geo IP:</strong>
<code><?= htmlspecialchars((new getSpecificData())->getGeoIP((new getSpecificData())->getClientIP())) ?></code>
</p>
<p style="margin: 5px 0;"><strong>Database IP:</strong>
<code><?= htmlspecialchars((new getSpecificData())->getDatabase()) ?></code>
</p>
<p style="margin: 5px 0;"><strong>Wetter:</strong>
<code><?= htmlspecialchars((new getSpecificData())->getWeather()['aktuell']) ?></code>
</p>
<?php endif ?>
</div>
</div>
@@ -89,32 +107,55 @@ $db_res = DatabaseShow(
</div>
</div>
<script src="/js/ajaxApi/NotificationAjaxMethods.js"></script>
<script src="/js/AjaxMethods.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const modal = document.getElementById('notificationModal');
const confirmBtn = document.getElementById('notificationConfirm');
if (!modal || !confirmBtn) return; // keine offene Nachricht vorhanden
confirmBtn.addEventListener('click', async () => {
const nachrichtId = modal.dataset.nachrichtId;
try {
await markNotificationAsSeen(nachrichtId);
modal.classList.remove('open');
setTimeout(() => modal.remove(), 200); // nach dem Fade-out entfernen
} catch (err) {
alert('Fehler beim Bestätigen: ' + err.message);
}
});
});
</script>
<style>
/* Zusätzliche Inline-Styles für die Feinabstimmung in der home_info */
.home-dashboard {
animation: fadeIn 0.5s ease-in-out;
}
code {
background: rgba(0, 0, 0, 0.1);
padding: 2px 5px;
border-radius: 4px;
font-family: monospace;
}
body.dark-mode code {
background: rgba(255, 255, 255, 0.1);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
/* Zusätzliche Inline-Styles für die Feinabstimmung in der home_info */
.home-dashboard {
animation: fadeIn 0.5s ease-in-out;
}
to {
opacity: 1;
transform: translateY(0);
code {
background: rgba(0, 0, 0, 0.1);
padding: 2px 5px;
border-radius: 4px;
font-family: monospace;
}
body.dark-mode code {
background: rgba(255, 255, 255, 0.1);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
}
</style>
+45 -45
View File
@@ -35,77 +35,77 @@ if ($customQuery) {
// Standard Queries
if (!$useCustom) {
switch ($filter) {
case 'today':
$sql = "
case 'today':
$sql = "
SELECT `CLIENT_IP`, `LAND`, `REQUEST_URL`, `COUNTER`, `FIRST_REQUEST`, `WEBSITE`
FROM logs
WHERE CLIENT_IP NOT IN ($devips_sql)
AND (WEBSITE != 'backend.blackpi.ipv64.net' OR WEBSITE IS NULL)
AND (WEBSITE != 'backend.mrhossa.de' OR WEBSITE IS NULL)
AND DATE(FIRST_REQUEST) = CURDATE()
ORDER BY FIRST_REQUEST DESC
";
break;
break;
case 'counterperip':
$sql = "
case 'counterperip':
$sql = "
SELECT CLIENT_IP, LAND, USER_AGENT, SUM(counter) AS gesamt_counter
FROM logs
WHERE CLIENT_IP NOT IN ($devips_sql)
AND (WEBSITE != 'backend.blackpi.ipv64.net' OR WEBSITE IS NULL)
AND (WEBSITE != 'backend.mrhossa.de' OR WEBSITE IS NULL)
GROUP BY CLIENT_IP
ORDER BY gesamt_counter DESC
";
break;
break;
case 'node1':
$sql = "
case 'node1':
$sql = "
SELECT `CLIENT_IP`, `LAND`, `REQUEST_URL`, `COUNTER`, `FIRST_REQUEST`, `WEBSITE`
FROM logs
WHERE `SERVER_IP` = 'node1'
AND (WEBSITE != 'backend.blackpi.ipv64.net' OR WEBSITE IS NULL)
AND (WEBSITE != 'backend.mrhossa.de' OR WEBSITE IS NULL)
ORDER BY `FIRST_REQUEST` DESC
";
break;
break;
case 'node2':
$sql = "
case 'node2':
$sql = "
SELECT `CLIENT_IP`, `LAND`, `REQUEST_URL`, `COUNTER`, `FIRST_REQUEST`, `WEBSITE`
FROM logs
WHERE `SERVER_IP` = 'node2'
AND (WEBSITE != 'backend.blackpi.ipv64.net' OR WEBSITE IS NULL)
AND (WEBSITE != 'backend.mrhossa.de' OR WEBSITE IS NULL)
ORDER BY `FIRST_REQUEST` DESC
";
break;
break;
case 'node3':
$sql = "
case 'node3':
$sql = "
SELECT `CLIENT_IP`, `LAND`, `REQUEST_URL`, `COUNTER`, `FIRST_REQUEST`, `WEBSITE`
FROM logs
WHERE `SERVER_IP` = 'node3'
AND (WEBSITE != 'backend.blackpi.ipv64.net' OR WEBSITE IS NULL)
AND (WEBSITE != 'backend.mrhossa.de' OR WEBSITE IS NULL)
ORDER BY `FIRST_REQUEST` DESC
";
break;
break;
case 'node4':
$sql = "
case 'node4':
$sql = "
SELECT `CLIENT_IP`, `LAND`, `REQUEST_URL`, `COUNTER`, `FIRST_REQUEST`, `WEBSITE`
FROM logs
WHERE `SERVER_IP` = 'node4'
AND (WEBSITE != 'backend.blackpi.ipv64.net' OR WEBSITE IS NULL)
AND (WEBSITE != 'backend.mrhossa.de' OR WEBSITE IS NULL)
ORDER BY `FIRST_REQUEST` DESC
";
break;
break;
default:
$sql = "
default:
$sql = "
SELECT `CLIENT_IP`, `LAND`, `REQUEST_URL`, `COUNTER`, `FIRST_REQUEST`, `WEBSITE`
FROM logs
WHERE CLIENT_IP NOT IN ($devips_sql)
AND (WEBSITE != 'backend.blackpi.ipv64.net' OR WEBSITE IS NULL)
AND (WEBSITE != 'backend.mrhossa.de' OR WEBSITE IS NULL)
ORDER BY FIRST_REQUEST DESC
";
}
}
}
?>
@@ -116,7 +116,7 @@ if (!$useCustom) {
<select name="filter" onchange="this.form.submit()">
<?php
$options = ['alle','today','node1','node2','node3','node4','counterperip'];
$options = ['alle', 'today', 'node1', 'node2', 'node3', 'node4', 'counterperip'];
foreach ($options as $opt) {
$sel = ($filter === $opt) ? 'selected' : '';
echo "<option value='$opt' $sel>$opt</option>";
@@ -150,25 +150,25 @@ if ($erg = $conn->query($sql)) {
?>
<?php if (!$daten): ?>
<b>Keine Daten gefunden</b>
<b>Keine Daten gefunden</b>
<?php else: ?>
<div style="overflow-x:auto;">
<table border="1" cellpadding="5" cellspacing="0" style="border-collapse:collapse;width:100%;">
<tr>
<?php foreach ($daten[0] as $key => $_): ?>
<th><?= htmlspecialchars($key) ?></th>
<?php endforeach; ?>
</tr>
<div style="overflow-x:auto;">
<table border="1" cellpadding="5" cellspacing="0" style="border-collapse:collapse;width:100%;">
<tr>
<?php foreach ($daten[0] as $key => $_): ?>
<th><?= htmlspecialchars($key) ?></th>
<?php endforeach; ?>
</tr>
<?php foreach ($daten as $row): ?>
<tr>
<?php foreach ($row as $val): ?>
<td><?= htmlspecialchars($val ?? '') ?></td>
<?php foreach ($daten as $row): ?>
<tr>
<?php foreach ($row as $val): ?>
<td><?= htmlspecialchars($val ?? '') ?></td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</table>
</div>
</table>
</div>
<?php endif; ?>
+4
View File
@@ -1,5 +1,8 @@
<?php
include_once(__DIR__ . '/../controller/RightsController.php');
include_once(__DIR__ . '/../config.php');
$configFile = 'config.php';
$backupDir = 'config_backups';
@@ -47,6 +50,7 @@ if (isset($_POST['save']) && ($_SESSION['logged_in'] ?? false)) {
$serviceToGroupMapping[$s['targetGroup']][] = $finalId;
}
}
(new RightsController())->RightsHandler($s['name']);
}
$newServiceGroups = [];
+207
View File
@@ -0,0 +1,207 @@
<div id="statusMsg" class="status-msg" style="display:none;"></div>
<div class="form-container">
<h3>📅 Neuen Termin vergeben</h3>
<form id="addTerminForm" class="vergabe-form">
<div class="vergabe-grid">
<div class="form-group">
<label for="datum">Start-Datum *</label>
<input type="date" id="datum" name="datum" required>
</div>
<div class="form-group">
<label for="zeit">Uhrzeit *</label>
<input type="time" id="zeit" name="zeit" required
style="width:100%; padding:10px; border-radius:5px; border:1px solid var(--input-border); background:var(--input-bg); color:var(--input-text);">
</div>
<div class="form-group">
<label for="name">Name / Zweck *</label>
<input type="text" id="name" name="name" placeholder="z.B. Kundentermin" required>
</div>
<div class="form-group">
<label for="notiz">Notiz (Optional)</label>
<input type="text" id="notiz" name="notiz" placeholder="Details zum Termin...">
</div>
<div class="form-group">
<label for="wiederholung">Wiederholung</label>
<select id="wiederholung" name="wiederholung"
style="width:100%; padding:10px; border-radius:5px; border:1px solid var(--input-border); background:var(--input-bg); color:var(--input-text);">
<option value="none">Keine Wiederholung</option>
<option value="daily">Täglich</option>
<option value="weekly">Wöchentlich</option>
<option value="monthly">Monatlich</option>
</select>
</div>
<div class="form-group" id="enddatum-group" style="display: none;">
<label for="enddatum">Wiederholen bis *</label>
<input type="date" id="enddatum" name="enddatum">
</div>
<button type="submit" class="btn-submit-vergabe">Speichern</button>
</div>
</form>
</div>
<div class="form-container">
<h3>📋 Aktuelle Terminbelegungen</h3>
<div class="table-container">
<div id="termineTableWrapper">Lade Daten...</div>
</div>
</div>
<script src="/js/AjaxMethods.js"></script>
<script src="/js/AjaxRenderTables.js"></script>
<script src="/js/ajaxApi/TermineAjaxMethods.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const statusMsg = document.getElementById('statusMsg');
const tableWrapper = document.getElementById('termineTableWrapper');
const intervallNamen = {
daily: 'Täglich',
weekly: 'Wöchentlich',
monthly: 'Monatlich'
};
function zeigeStatus(text, istFehler = false) {
statusMsg.textContent = text;
statusMsg.style.display = 'block';
statusMsg.style.background = istFehler ? '#f8d7da' : '';
statusMsg.style.color = istFehler ? '#721c24' : '';
setTimeout(() => {
statusMsg.style.display = 'none';
}, 4000);
}
function formatDatum(iso) {
const d = new Date(iso);
return d.toLocaleDateString('de-DE');
}
function formatZeit(zeit) {
return zeit?.slice(0, 5) ?? '';
}
function renderTermine(termine) {
if (!termine || termine.length === 0) {
tableWrapper.innerHTML = `
<table class="db-table mobile-responsive-table">
<thead><tr><th>Datum / Turnus</th><th>Uhrzeit</th><th>Termin / Name</th><th>Notiz</th><th>Eingetragen von</th><th>Aktion</th></tr></thead>
<tbody><tr><td colspan="6" style="text-align:center; padding:20px;">Keine Termine eingetragen.</td></tr></tbody>
</table>`;
return;
}
let html = `
<table class="db-table mobile-responsive-table">
<thead>
<tr><th>Datum / Turnus</th><th>Uhrzeit</th><th>Termin / Name</th><th>Notiz</th><th>Eingetragen von</th><th>Aktion</th></tr>
</thead>
<tbody>
`;
for (const t of termine) {
const istSerie = !!t.SERIEN_ID;
html += `
<tr>
<td data-label="Datum:">
<strong>${escapeHtml(formatDatum(t.DATUM))}</strong>
${istSerie ? `<br><small style="color:#007bff; font-style:italic;">(${escapeHtml(intervallNamen[t.INTERVALL] ?? '')} bis ${escapeHtml(formatDatum(t.SERIEN_ENDE))})</small>` : ''}
</td>
<td data-label="Uhrzeit:">${escapeHtml(formatZeit(t.ZEIT))}</td>
<td data-label="Termin / Name:">
<strong>${escapeHtml(t.NAME)}</strong>
${istSerie ? `<span style="font-size:0.8em; background:#e1f5fe; color:#0288d1; padding:2px 6px; border-radius:10px; margin-left:5px;">Wiederholend</span>` : ''}
</td>
<td data-label="Notiz:">${escapeHtml(t.NOTIZ || '-')}</td>
<td data-label="Von:"><span class="badge">${escapeHtml(t.ERSTELLT_VON)}</span></td>
<td data-label="Aktion:">
${istSerie
? `<button type="button" class="btn-delete btn-delete-serie" data-serien-id="${escapeHtml(t.SERIEN_ID)}" style="background-color:#d32f2f;">löschen</button>`
: `<button type="button" class="btn-delete btn-delete-einzeln" data-id="${escapeHtml(t.ID)}">Löschen</button>`
}
</td>
</tr>`;
}
html += `</tbody></table>`;
tableWrapper.innerHTML = html;
tableWrapper.querySelectorAll('.btn-delete-einzeln').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Möchtest du diesen Termin wirklich löschen?')) return;
try {
const data = await deleteTermin(btn.dataset.id);
zeigeStatus(data.message);
await ladeTermine();
} catch (err) {
zeigeStatus('Fehler: ' + err.message, true);
}
});
});
tableWrapper.querySelectorAll('.btn-delete-serie').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Möchtest du die GESAMTE Terminserie löschen?')) return;
try {
const data = await deleteTerminSerie(btn.dataset.serienId);
zeigeStatus(data.message);
await ladeTermine();
} catch (err) {
zeigeStatus('Fehler: ' + err.message, true);
}
});
});
}
async function ladeTermine() {
try {
const data = await getTermine();
renderTermine(data.entries);
} catch (err) {
tableWrapper.innerHTML = `<p>❌ ${escapeHtml(err.message)}</p>`;
}
}
// -----------------------------------------------------------
// Wiederholung: Enddatum-Feld ein-/ausblenden
// -----------------------------------------------------------
document.getElementById('wiederholung').addEventListener('change', function() {
const enddatumGroup = document.getElementById('enddatum-group');
const enddatumInput = document.getElementById('enddatum');
if (this.value !== 'none') {
enddatumGroup.style.display = 'block';
enddatumInput.required = true;
} else {
enddatumGroup.style.display = 'none';
enddatumInput.required = false;
enddatumInput.value = '';
}
});
// -----------------------------------------------------------
// Formular absenden
// -----------------------------------------------------------
document.getElementById('addTerminForm').addEventListener('submit', async (e) => {
e.preventDefault();
const datum = document.getElementById('datum').value;
const zeit = document.getElementById('zeit').value;
const name = document.getElementById('name').value.trim();
const notiz = document.getElementById('notiz').value.trim();
const wiederholung = document.getElementById('wiederholung').value;
const enddatum = document.getElementById('enddatum').value;
try {
const data = await addTermin(datum, zeit, name, notiz, wiederholung, enddatum);
zeigeStatus(data.message);
e.target.reset();
document.getElementById('enddatum-group').style.display = 'none';
await ladeTermine();
} catch (err) {
zeigeStatus('Fehler: ' + err.message, true);
}
});
ladeTermine();
});
</script>
+144
View File
@@ -0,0 +1,144 @@
<div class="arbeitsberichte-wrapper">
<div class="vergabe-form">
<h3>Neues Ticket anlegen</h3>
<form id="addTicketForm" class="vergabe-grid">
<div class="form-group">
<label>Titel</label>
<input type="text" name="titel" id="in-titel" required>
</div>
<div class="form-group">
<label>Beschreibung</label>
<input type="text" name="beschreibung" id="in-beschreibung" required>
</div>
<button type="submit" class="btn-submit-vergabe">💾 Speichern</button>
</form>
</div>
<div class="table-container" style="margin-top: 30px;">
<h3>Tickets</h3>
<div id="tableWrapper">
<p>Lade Daten...</p>
</div>
</div>
</div>
<script src="/js/AjaxMethods.js"></script>
<script src="/js/ajaxApi/TicketBoardAjaxMethods.js"></script>
<script src="/js/AjaxRenderTables.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const tableWrapper = document.getElementById('tableWrapper');
const POLL_INTERVAL_MS = 5000;
let lastStatusById = {};
// -----------------------------------------------------------
// Tabelle rendern (eigene Render-Funktion wg. Status-Highlight)
// -----------------------------------------------------------
function renderTicketsTabelle(entries) {
if (!entries || entries.length === 0) {
tableWrapper.innerHTML = '<p>Keine Tickets vorhanden.</p>';
lastStatusById = {};
return;
}
let html = `
<table class="db-table mobile-responsive-table">
<thead>
<tr>
<th>Titel</th><th>Beschreibung</th><th>Status</th>
<th style="width: 100px;">Aktion</th>
</tr>
</thead>
<tbody>
`;
for (const e of entries) {
const changed = lastStatusById[e.ID] !== undefined &&
lastStatusById[e.ID] !== e.STATUS;
html += `
<tr data-id="${escapeHtml(e.ID)}" class="${changed ? 'status-row-changed' : ''}">
<td data-label="Titel">${escapeHtml(e.TITEL)}</td>
<td data-label="Beschreibung">${escapeHtml(e.BESCHREIBUNG)}</td>
<td data-label="Status"><span class="status-badge">${escapeHtml(e.STATUS)}</span></td>
<td data-label="Aktion">
<button type="button" class="btn-delete" data-id="${escapeHtml(e.ID)}">Löschen</button>
</td>
</tr>`;
}
html += `</tbody></table>`;
tableWrapper.innerHTML = html;
const neueStatus = {};
entries.forEach(e => {
neueStatus[e.ID] = e.STATUS;
});
lastStatusById = neueStatus;
tableWrapper.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Möchtest du dieses Ticket wirklich löschen?')) return;
try {
await deleteTicketEntry(btn.dataset.id);
await ladeListe();
} catch (err) {
alert('Fehler beim Löschen: ' + err.message);
}
});
});
}
async function ladeListe() {
try {
const data = await getTicketEntries();
renderTicketsTabelle(data.entries);
} catch (err) {
tableWrapper.innerHTML = `<p>❌ ${escapeHtml(err.message)}</p>`;
}
}
// -----------------------------------------------------------
// Ticket anlegen
// -----------------------------------------------------------
document.getElementById('addTicketForm').addEventListener('submit', async (e) => {
e.preventDefault();
const titel = document.getElementById('in-titel').value.trim();
const beschreibung = document.getElementById('in-beschreibung').value.trim();
if (!titel || !beschreibung) return;
try {
await addTicketEntry(titel, beschreibung);
e.target.reset();
await ladeListe();
} catch (err) {
alert('Fehler beim Speichern: ' + err.message);
}
});
// -----------------------------------------------------------
// Initialisierung + zyklische Aktualisierung
// -----------------------------------------------------------
ladeListe();
setInterval(ladeListe, POLL_INTERVAL_MS);
});
</script>
<style>
@keyframes statusChanged {
0% {
background-color: #fff3cd;
}
100% {
background-color: transparent;
}
}
.status-row-changed {
animation: statusChanged 2.5s ease-out;
}
</style>
+97
View File
@@ -0,0 +1,97 @@
<div class="arbeitsberichte-wrapper">
<div class="table-container" style="margin-top: 30px;">
<h3>Ticket Verwaltung</h3>
<div id="tableWrapper">
<p>Lade Daten...</p>
</div>
</div>
</div>
<script src="/js/AjaxMethods.js"></script>
<script src="/js/AjaxRenderTables.js"></script>
<script src="/js/ajaxApi/TicketControlAjaxMethods.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const tableWrapper = document.getElementById('tableWrapper');
const allowedStatuses = ['OFFEN', 'DEPLOYED', 'ABGELEHNT', 'ANGENOMMEN'];
function renderTicketControTabelle(entries) {
if (!entries || entries.length === 0) {
tableWrapper.innerHTML = '<p>Keine Tickets vorhanden.</p>';
return;
}
let html = `
<table class="db-table mobile-responsive-table">
<thead>
<tr>
<th>Titel</th><th>Beschreibung</th><th>Status</th><th>Ersteller</th>
<th style="width: 220px;">Aktion</th>
</tr>
</thead>
<tbody>
`;
for (const e of entries) {
const options = allowedStatuses.map(s =>
`<option value="${escapeHtml(s)}" ${e.STATUS === s ? 'selected' : ''}>${escapeHtml(s)}</option>`
).join('');
html += `
<tr data-id="${escapeHtml(e.ID)}">
<td data-label="Titel">${escapeHtml(e.TITEL)}</td>
<td data-label="Beschreibung">${escapeHtml(e.BESCHREIBUNG)}</td>
<td data-label="Status">${escapeHtml(e.STATUS)}</td>
<td data-label="Ersteller">${escapeHtml(e.ERSTELLER)}</td>
<td data-label="Aktion">
<div style="display:flex; flex-direction:column; gap:6px; align-items:stretch;">
<select class="status-select" data-id="${escapeHtml(e.ID)}">
${options}
</select>
<button type="button" class="btn-edit" data-id="${escapeHtml(e.ID)}">Speichern</button>
<button type="button" class="btn-delete" data-id="${escapeHtml(e.ID)}" style="margin-top:3pt;">Löschen</button>
</div>
</td>
</tr>`;
}
html += `</tbody></table>`;
tableWrapper.innerHTML = html;
tableWrapper.querySelectorAll('.btn-edit').forEach(btn => {
btn.addEventListener('click', async () => {
const id = btn.dataset.id;
const select = tableWrapper.querySelector(
`.status-select[data-id="${id}"]`);
try {
await editTicketControEntry(id, select.value);
await ladeListe();
} catch (err) {
alert('Fehler beim Speichern: ' + err.message);
}
});
});
tableWrapper.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Möchtest du dieses Ticket wirklich löschen?')) return;
try {
await deleteTicketControEntry(btn.dataset.id);
await ladeListe();
} catch (err) {
alert('Fehler beim Löschen: ' + err.message);
}
});
});
}
async function ladeListe() {
try {
const data = await getTicketControEntries();
renderTicketControTabelle(data.entries);
} catch (err) {
tableWrapper.innerHTML = `<p>❌ ${escapeHtml(err.message)}</p>`;
}
}
ladeListe();
});
</script>
+284
View File
@@ -0,0 +1,284 @@
<div class="arbeitsberichte-wrapper">
<div class="vergabe-form">
<form id="addForm" class="vergabe-grid">
<div class="form-group">
<label>Wann ausgeliehen</label>
<input type="date" name="wann" id="in-wann" required>
</div>
<div class="form-group">
<label>Rückgabe</label>
<input type="date" name="rueckgabe" id="in-rueckgabe" required>
</div>
<div class="form-group autocomplete-wrapper">
<label>An wen</label>
<input type="text" id="wer-input" placeholder="Name eingeben" required autocomplete="off">
<div class="autocomplete-suggestions" id="wer-suggestions"></div>
</div>
<div class="form-group autocomplete-wrapper">
<label>Was</label>
<input type="text" id="was-input" placeholder="Gegenstand eingeben" required autocomplete="off">
<div class="autocomplete-suggestions" id="was-suggestions"></div>
</div>
<button type="submit" class="btn-submit-vergabe">Speichern</button>
</form>
</div>
<div class="vergabe-form" style="margin-top: 30px;">
<form id="searchForm" class="vergabe-grid">
<div class="form-group autocomplete-wrapper">
<label for="search-wer-input">Nach Person filtern</label>
<input type="text" id="search-wer-input" placeholder="Name eingeben..." autocomplete="off">
<div class="autocomplete-suggestions" id="search-wer-suggestions"></div>
</div>
<div class="form-group autocomplete-wrapper">
<label for="search-was-input">Nach Gegenstand filtern</label>
<input type="text" id="search-was-input" placeholder="Gegenstand eingeben..." autocomplete="off">
<div class="autocomplete-suggestions" id="search-was-suggestions"></div>
</div>
<div style="display: flex; gap: 10px; align-items: flex-end;">
<button type="submit" class="btn-submit-vergabe" style="margin: 0; flex-grow: 1;">Suchen</button>
<button type="button" id="resetSearch" class="btn-delete" style="margin: 0;">Zurücksetzen</button>
</div>
</form>
</div>
<div class="table-container" style="margin-top: 30px;">
<h3 id="tableHeading">Aktuelle Vergaben</h3>
<div id="tableWrapper">
<p>Lade Daten...</p>
</div>
</div>
</div>
<script src="/js/AjaxMethods.js"></script>
<script src="/js/ajaxApi/VergabeAjaxMethods.js"></script>
<script src="/js/AjaxRenderTables.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const tableWrapper = document.getElementById('tableWrapper');
const tableHeading = document.getElementById('tableHeading');
let aktuellSearchWer = '';
let aktuellSearchWas = '';
// -----------------------------------------------------------
// Tabelle rendern (eigene Render-Funktion, da "Zurückgeben"
// eine andere Aktion ist als das generische Löschen)
// -----------------------------------------------------------
function renderVergabeTabelle(entries) {
if (!entries || entries.length === 0) {
tableWrapper.innerHTML = '<p>Keine Einträge vorhanden.</p>';
return;
}
let html = `
<table class="db-table mobile-responsive-table">
<thead>
<tr>
<th>Was</th><th>Wer</th><th>Wann ausgeliehen</th><th>Rückgabe</th>
<th style="width: 100px;">Aktion</th>
</tr>
</thead>
<tbody>
`;
for (const e of entries) {
html += `
<tr data-id="${escapeHtml(e.ID)}">
<td data-label="Was">${escapeHtml(e.WAS)}</td>
<td data-label="Wer">${escapeHtml(e.WER)}</td>
<td data-label="Wann">${escapeHtml(e.WANN)}</td>
<td data-label="Rückgabe">${escapeHtml(e.RUECKGABE)}</td>
<td data-label="Aktion">
<button type="button" class="btn-delete" data-id="${escapeHtml(e.ID)}">Löschen</button>
</td>
</tr>`;
}
html += `</tbody></table>`;
tableWrapper.innerHTML = html;
tableWrapper.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Möchtest du diesen Eintrag wirklich löschen?')) return;
try {
await returnVergabeEntry(btn.dataset.id);
await ladeListe();
} catch (err) {
alert('Fehler beim Löschen: ' + err.message);
}
});
});
}
function aktualisiereUeberschrift() {
if (aktuellSearchWer && aktuellSearchWas) {
tableHeading.textContent = `Gefiltert nach: ${aktuellSearchWer} ➔ ${aktuellSearchWas}`;
} else if (aktuellSearchWer) {
tableHeading.textContent = `Vergaben für: ${aktuellSearchWer}`;
} else if (aktuellSearchWas) {
tableHeading.textContent = `Ausgeliehene Gegenstände: ${aktuellSearchWas}`;
} else {
tableHeading.textContent = 'Aktuelle Vergaben';
}
}
async function ladeListe() {
try {
const data = await getVergabeEntries(aktuellSearchWer, aktuellSearchWas);
aktualisiereUeberschrift();
renderVergabeTabelle(data.entries);
} catch (err) {
tableWrapper.innerHTML = `<p>❌ ${escapeHtml(err.message)}</p>`;
}
}
// -----------------------------------------------------------
// Eintrag hinzufügen
// -----------------------------------------------------------
document.getElementById('addForm').addEventListener('submit', async (e) => {
e.preventDefault();
const wann = document.getElementById('in-wann').value;
const rueckgabe = document.getElementById('in-rueckgabe').value;
const wer = document.getElementById('wer-input').value.trim();
const was = document.getElementById('was-input').value.trim();
if (!wann || !rueckgabe || !wer || !was) return;
try {
await addVergabeEntry(was, wer, wann, rueckgabe);
e.target.reset();
document.getElementById('in-wann').value = new Date().toISOString().slice(0, 10);
document.getElementById('in-rueckgabe').value = new Date().toISOString().slice(0, 10);
await ladeVorschlaege(); // neue WAS/WER-Werte könnten dazugekommen sein
await ladeListe();
} catch (err) {
alert('Fehler beim Speichern: ' + err.message);
}
});
// -----------------------------------------------------------
// Suche
// -----------------------------------------------------------
document.getElementById('searchForm').addEventListener('submit', async (e) => {
e.preventDefault();
aktuellSearchWer = document.getElementById('search-wer-input').value.trim();
aktuellSearchWas = document.getElementById('search-was-input').value.trim();
await ladeListe();
});
document.getElementById('resetSearch').addEventListener('click', async () => {
aktuellSearchWer = '';
aktuellSearchWas = '';
document.getElementById('search-wer-input').value = '';
document.getElementById('search-was-input').value = '';
await ladeListe();
});
// -----------------------------------------------------------
// Autocomplete (unverändert zu deiner Logik, nur Datenquelle
// kommt jetzt per AJAX statt server-seitig eingebettet)
// -----------------------------------------------------------
function setupAutocomplete(inputId, suggestionsId, data, submitOnSelect = false) {
const input = document.getElementById(inputId);
const box = document.getElementById(suggestionsId);
if (!input || !box) return;
let items = [];
let activeIndex = -1;
function render(list) {
box.innerHTML = '';
items = list;
activeIndex = -1;
if (list.length === 0) {
box.style.display = 'none';
return;
}
list.forEach((val, i) => {
const div = document.createElement('div');
div.textContent = val;
div.addEventListener('mousedown', (e) => {
e.preventDefault();
selectItem(i);
});
box.appendChild(div);
});
box.style.display = 'block';
}
function updateActive() {
[...box.children].forEach((el, i) => el.classList.toggle('active', i === activeIndex));
if (activeIndex >= 0) box.children[activeIndex].scrollIntoView({
block: 'nearest'
});
}
function selectItem(i) {
if (items[i] !== undefined) input.value = items[i];
box.style.display = 'none';
activeIndex = -1;
if (submitOnSelect) {
input.closest('form').dispatchEvent(new Event('submit', {
cancelable: true
}));
}
}
input.addEventListener('input', () => {
const val = input.value.toLowerCase();
if (val.length === 0) {
box.style.display = 'none';
return;
}
render(data.filter(d => d.toLowerCase().includes(val)));
});
input.addEventListener('keydown', (e) => {
if (box.style.display !== 'block') return;
if (e.key === 'ArrowDown') {
e.preventDefault();
activeIndex = Math.min(activeIndex + 1, items.length - 1);
updateActive();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
activeIndex = Math.max(activeIndex - 1, 0);
updateActive();
} else if (e.key === 'Tab' || e.key === 'Enter') {
if (activeIndex >= 0) {
e.preventDefault();
selectItem(activeIndex);
} else box.style.display = 'none';
} else if (e.key === 'Escape') box.style.display = 'none';
});
document.addEventListener('click', (e) => {
if (e.target !== input) box.style.display = 'none';
});
}
async function ladeVorschlaege() {
const data = await getVergabeSuggestions();
setupAutocomplete('was-input', 'was-suggestions', data.was);
setupAutocomplete('wer-input', 'wer-suggestions', data.wer);
setupAutocomplete('search-wer-input', 'search-wer-suggestions', data.wer, true);
setupAutocomplete('search-was-input', 'search-was-suggestions', data.was, true);
}
// -----------------------------------------------------------
// Initialisierung
// -----------------------------------------------------------
document.getElementById('in-wann').value = new Date().toISOString().slice(0, 10);
document.getElementById('in-rueckgabe').value = new Date().toISOString().slice(0, 10);
ladeVorschlaege();
ladeListe();
});
</script>
+61 -57
View File
@@ -1,73 +1,27 @@
<?php
include_once __DIR__ . '/../controller/DatabaseController.php';
include_once __DIR__ . '/../controller/RoutesController.php';
$conn = db_connect();
$view = $_GET['view'] ?? 'show';
$selectedService = $_GET['service'];
// --- LOGIK: Überstunden-Eintrag (Abbau) speichern ---
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_overtime'])) {
$datum = $_POST['ot_date'];
$minuten = intval($_POST['ot_minutes']);
$grund = $conn->real_escape_string($_POST['ot_reason']);
DatabaseInsert(
"Ueberstunden",
"DATUM, MINUTEN, GRUND",
$datum,
$minuten,
$grund
);
redirectToWebsite((new getSpecificData())->getReferer());
exit;
}
// --- Gesamtsumme berechnen ---
$minuten_berichte = 0;
$res1 = $conn->query("SELECT SUM(OVERTIME) AS summe FROM Firmenberichte");
if ($row = $res1->fetch_object()) { $minuten_berichte = intval($row->summe); }
$minuten_abbau = 0;
$res2 = $conn->query("SELECT SUM(MINUTEN) AS summe FROM Ueberstunden WHERE GRUND = 'Abbau'");
if ($row = $res2->fetch_object()) { $minuten_abbau = intval($row->summe); }
$minuten_aufbau = 0;
$res2 = $conn->query("SELECT SUM(MINUTEN) AS summe FROM Ueberstunden WHERE GRUND = 'Aufbau'");
if ($row = $res2->fetch_object()) { $minuten_aufbau = intval($row->summe); }
$gesamt_minuten = $minuten_berichte - $minuten_abbau + $minuten_aufbau;
?>
<div class="arbeitsberichte-wrapper">
<div
<div id="overtimeCard"
style="background: rgba(150,150,150,0.05); padding: 25px; border-radius: 10px; margin-bottom: 25px; border: 1px solid rgba(150,150,150,0.2); border-left: 6px solid #007bff;">
<div style="margin-bottom: 20px;">
<span style="color: #888; font-size: 0.8rem; font-weight: bold; text-transform: uppercase;">Aktueller
Stand</span>
<h2 style="margin: 5px 0 0 0; font-size: 2rem; color: <?= $gesamt_minuten >= 0 ? '#28a745' : '#dc3545' ?>;">
<?= intdiv($gesamt_minuten, 60) ?>h <?= abs($gesamt_minuten % 60) ?>min
</h2>
<h2 id="standAnzeige" style="margin: 5px 0 0 0; font-size: 2rem;">Lädt...</h2>
</div>
<?php if ($_SESSION['username'] == "bruno" || $_SESSION['username'] == "Bruno"): ?>
<form method="POST" style="display: flex; gap: 15px; flex-wrap: wrap; align-items: flex-end;">
<input type="hidden" name="add_overtime" value="1">
<form id="overtimeForm" style="display: none; gap: 15px; flex-wrap: wrap; align-items: flex-end;">
<div style="display: flex; flex-direction: column; gap: 5px;">
<label style="font-size: 0.85rem; font-weight: 600; color: inherit;">Datum</label>
<input type="date" name="ot_date" value="<?= date('Y-m-d') ?>" required
<input type="date" id="ot-date" required
style="padding: 8px; border: 1px solid rgba(150,150,150,0.3); border-radius: 4px; background: rgba(150,150,150,0.1); color: inherit;">
</div>
<div style="display: flex; flex-direction: column; gap: 5px;">
<label style="font-size: 0.85rem; font-weight: 600; color: inherit;">Minuten Abbau</label>
<input type="number" name="ot_minutes" placeholder="480" required
<label style="font-size: 0.85rem; font-weight: 600; color: inherit;">Minuten</label>
<input type="number" id="ot-minutes" placeholder="480" required
style="width: 100px; padding: 8px; border: 1px solid rgba(150,150,150,0.3); border-radius: 4px; background: rgba(150,150,150,0.1); color: inherit;">
</div>
<div class="form-group">
<label style="font-size: 0.85rem; font-weight: 600; color: inherit;">Grund</label>
<select name="ot_reason" required>
<select id="ot-reason" required>
<option value="">Bitte wählen</option>
<option value="Abbau">Abbau</option>
<option value="Aufbau">Aufbau</option>
@@ -79,6 +33,56 @@ $gesamt_minuten = $minuten_berichte - $minuten_abbau + $minuten_aufbau;
Eintrag speichern
</button>
</form>
<?php endif ?>
</div>
</div>
</div>
<script src="/js/AjaxMethods.js"></script>
<script src="/js/AjaxRenderTables.js"></script>
<script src="/js/ajaxApi/ZeiterfassungAjaxMethods.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const standAnzeige = document.getElementById('standAnzeige');
const overtimeForm = document.getElementById('overtimeForm');
function formatStand(gesamtMinuten) {
const stunden = Math.trunc(gesamtMinuten / 60);
const minuten = Math.abs(gesamtMinuten % 60);
return `${stunden}h ${minuten}min`;
}
async function ladeStand() {
try {
const data = await getOvertimeStats();
standAnzeige.textContent = formatStand(data.gesamtMinuten);
standAnzeige.style.color = data.gesamtMinuten >= 0 ? '#28a745' : '#dc3545';
overtimeForm.style.display = data.istBerechtigt ? 'flex' : 'none';
} catch (err) {
standAnzeige.textContent = 'Fehler beim Laden';
console.error(err.message);
}
}
overtimeForm.addEventListener('submit', async (e) => {
e.preventDefault();
const datum = document.getElementById('ot-date').value;
const minuten = document.getElementById('ot-minutes').value;
const grund = document.getElementById('ot-reason').value;
try {
await addOvertimeEntry(datum, minuten, grund);
e.target.reset();
document.getElementById('ot-date').value = new Date().toISOString().slice(0, 10);
await ladeStand();
} catch (err) {
alert('Fehler beim Speichern: ' + err.message);
}
});
document.getElementById('ot-date').value = new Date().toISOString().slice(0, 10);
ladeStand();
});
</script>
+479 -5
View File
@@ -880,6 +880,17 @@ body.dark-mode .user-table .btn-primary:hover {
font-size: 0.8rem;
}
.btn-edit {
background-color: #32b844;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
}
/* ===== Toggle Switch ===== */
.toggle-card {
@@ -982,7 +993,6 @@ body.dark-mode .info-value {
border-collapse: collapse;
margin-top: 20px;
table-layout: fixed;
/* sorgt für feste Spaltenbreite */
}
.db-table thead {
@@ -996,10 +1006,13 @@ body.dark-mode .info-value {
text-align: left;
border-bottom: 1px solid var(--card-border);
/* Wichtig für gleiche Größe */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-wrap: break-word;
overflow-wrap: break-word;
white-space: normal;
}
.db-table td:nth-child(2) {
max-width: 300px;
}
.db-table tr:hover {
@@ -1070,4 +1083,465 @@ select {
/* Dark Mode Pfeil */
body.dark-mode select {
background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg fill='%23ccc' height='20' viewBox='0 0 20 20' width='20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5 7l5 5 5-5z'/%3E%3C/svg%3E");
}
/* ===== Vergabe Formular ===== */
.vergabe-form {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-left: 5px solid var(--btn-bg);
border-radius: 10px;
padding: 20px;
margin-bottom: 25px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
}
/* Desktop-First Grid Layout */
.vergabe-grid {
display: grid;
/* Nutzt minmax, damit die Datumsfelder auf kleineren Desktop-Monitoren nicht zerquetschen */
grid-template-columns: minmax(140px, 1fr) minmax(140px, 1fr) 1.3fr 2fr auto;
gap: 15px;
align-items: end;
}
.vergabe-grid .form-group {
margin: 0;
}
.vergabe-grid input,
.btn-submit-vergabe {
width: 100%;
/* Wichtig für Responsiveness */
padding: 10px 16px;
font-size: 15px;
border-radius: 5px;
box-sizing: border-box;
}
body.dark-mode input[type="date"] {
color-scheme: dark;
}
/* Dedizierter Speicher-Button Style */
.btn-submit-vergabe {
background: var(--btn-add-bg);
color: white;
border: none;
font-weight: 600;
cursor: pointer;
height: 43px;
/* Gleicht die Höhe der Inputs inklusive Padding/Border perfekt aus */
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease, transform 0.1s ease;
}
.btn-submit-vergabe:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.btn-submit-vergabe:active {
transform: scale(0.98);
}
/* ===== Mobile Optimierung (Tablets & Smartphones) ===== */
@media (max-width: 1024px) {
.vergabe-grid {
/* Schaltet auf Mobilgeräten in eine saubere 1-spaltige Liste um */
grid-template-columns: 1fr;
gap: 12px;
/* Schafft vertikalen Abstand zwischen den Feldern */
}
.btn-submit-vergabe {
margin-top: 5px;
height: 48px;
/* Größere Touch-Fläche für Smartphones */
font-size: 1rem;
}
}
/* ===== Globale Mobile Anpassungen ===== */
@media (max-width: 768px) {
/* 1. Haupt-Layout untereinander brechen lassen */
.main {
flex-direction: column;
height: calc(100vh - 60px);
/* Behält die feste Gesamthöhe bei */
overflow-y: auto;
/* Erlaubt das Scrollen der gesamten Seite falls nötig */
}
/* 2. Sidebar nimmt volle Breite ein und passt sich dem Inhalt an */
.sidebar {
width: 100%;
height: auto;
/* Keine feste Höhe mehr auf Mobile */
border-right: none;
border-bottom: 1px solid #333;
/* Trennlinie nach unten statt nach rechts */
overflow-y: visible;
/* Scrollbar hier deaktivieren */
}
/* 3. Inhaltsbereich nimmt den Rest des Platzes ein */
.content {
width: 100%;
height: auto;
overflow-y: visible;
/* Verhindert doppelte Scrollbalken */
padding: 15px 10px;
}
/* 4. Das Vergabe-Formular bricht sauber in eine Spalte um */
.vergabe-grid {
grid-template-columns: 1fr !important;
/* Erzwingt eine Spalte */
gap: 12px;
}
/* 5. Inputs und Buttons nehmen die volle Breite der Box ein */
.vergabe-grid input,
.btn-submit-vergabe,
.vergabe-grid button {
width: 100% !important;
box-sizing: border-box;
}
/* Optional: Header-Buttons (Logout / Light Mode) für Mobile optimieren, falls sie überlappen */
#darkModeToggle,
#logoutButton {
position: static;
/* Entfernt absolute Positionierung im mobilen Header */
margin: 5px;
padding: 6px 12px;
font-size: 0.9rem;
}
header {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 10px;
height: auto;
}
}
/* ===== Mobile Optimierung für die Vergabe-Tabelle ===== */
@media (max-width: 768px) {
/* Verstecke die normale Tabellen-Überschrift */
.mobile-responsive-table thead {
display: none;
}
/* Mache aus der Tabelle ein Block-Layout */
.mobile-responsive-table,
.mobile-responsive-table tbody,
.mobile-responsive-table tr,
.mobile-responsive-table td {
display: block;
width: 100% !important;
}
/* Jede Zeile wird zu einer schicken Card */
.mobile-responsive-table tr {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 8px;
margin-bottom: 15px;
padding: 10px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}
/* Einzelne Zellen nebeneinander/untereinander stylen */
.mobile-responsive-table td {
text-align: right !important;
padding: 8px 10px !important;
border-bottom: 1px solid var(--card-border);
position: relative;
white-space: normal !important;
/* Erlaubt Textumbruch auf Handys */
overflow: visible !important;
}
.mobile-responsive-table td:last-child {
border-bottom: none;
padding-top: 12px !important;
}
/* Die Beschriftung (z.B. "Was:") links einblenden */
.mobile-responsive-table td::before {
content: attr(data-label);
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
font-weight: bold;
font-size: 0.9rem;
color: var(--text);
}
/* Lösch-Button auf Mobile über die gesamte Breite strecken für leichtere Bedienung */
.mobile-responsive-table .btn-delete {
width: 100%;
padding: 10px;
font-size: 1rem;
}
}
/* ===== Optimiertes Firmenberichte Erfassungsformular ===== */
.berichte-container {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-top: 4px solid #007bff;
/* Eleganter blauer Akzentstreifen oben statt links */
border-radius: 12px;
padding: 30px;
max-width: 500px;
/* Kompakt und perfekt lesbar auf Monitoren */
margin: 40px auto;
/* Schön zentriert mit Luft nach oben/unten */
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
}
/* Tiefenwirkung für den Dark Mode */
body.dark-mode .berichte-container {
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
background: #161616;
/* Passt sich perfekt deiner CSS-Variable an */
}
.berichte-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.berichte-form .form-group {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.berichte-form .form-group label {
font-size: 0.85rem;
font-weight: 700;
color: var(--text);
text-transform: uppercase;
letter-spacing: 0.8px;
opacity: 0.85;
}
/* Einheitlicher, moderner Look für ALLE Input-Typen */
.berichte-form input[type="text"],
.berichte-form input[type="time"],
.berichte-form input[type="date"] {
width: 100%;
padding: 12px 16px;
background: var(--input-bg);
border: 1px solid var(--input-border);
color: var(--input-text);
border-radius: 8px;
font-size: 0.95rem;
box-sizing: border-box;
transition: border-color 0.2s, box-shadow 0.2s;
}
/* Behebt das Problem mit den grellweißen Zeit/Datumsfeldern im Dark Mode */
body.dark-mode .berichte-form input[type="time"],
body.dark-mode .berichte-form input[type="date"] {
color-scheme: dark;
/* Zwingt den Browser, das native Dropdown dunkel zu machen */
background: #222;
border-color: #444;
color: #fff;
}
/* Fokus-Effekt (weicher blauer Schein) */
.berichte-form input:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 4px rgba(0, 123, 255, 0.15);
}
body.dark-mode .berichte-form input:focus {
box-shadow: 0 0 0 4px rgba(0, 123, 255, 0.3);
}
/* Vollbreiter, moderner Absende-Button */
.btn-submit-bericht {
width: 100%;
background: #28a745;
/* Kräftiges Success-Grün */
color: white;
border: none;
padding: 14px;
font-size: 1rem;
font-weight: 600;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-top: 10px;
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.2);
transition: all 0.2s ease;
}
.btn-submit-bericht:hover {
background: #218838;
transform: translateY(-1px);
box-shadow: 0 6px 15px rgba(40, 167, 69, 0.3);
}
.btn-submit-bericht:active {
transform: scale(0.98);
}
/* Kleine Bildschirme (Smartphones) */
@media (max-width: 600px) {
.berichte-container {
margin: 15px;
padding: 20px;
}
}
.autocomplete-wrapper {
position: relative;
}
.autocomplete-suggestions {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--card-bg);
color: var(--text);
border: 1px solid var(--input-border);
border-top: none;
max-height: 180px;
overflow-y: auto;
z-index: 1000;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.autocomplete-suggestions div {
padding: 8px 10px;
cursor: pointer;
}
.autocomplete-suggestions div:hover,
.autocomplete-suggestions div.active {
background: rgba(0, 123, 255, 0.15);
}
body.dark-mode .autocomplete-suggestions div:hover,
body.dark-mode .autocomplete-suggestions div.active {
background: rgba(0, 123, 255, 0.25);
}
.register-form {
display: flex;
flex-direction: column;
gap: 12px;
}
.register-form input {
margin-bottom: 0;
/* gap übernimmt jetzt den Abstand, kein doppeltes Spacing mehr */
}
/* ===== Benachrichtigungs-Modal ===== */
.notification-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
}
.notification-overlay.open {
opacity: 1;
pointer-events: auto;
}
.notification-box {
background-color: white;
border-radius: 10px;
border-top: 4px solid #007bff;
width: 420px;
max-width: 90%;
padding: 28px 32px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
text-align: center;
transform: translateY(-10px);
transition: transform 0.2s ease;
}
.notification-overlay.open .notification-box {
transform: translateY(0);
}
body.dark-mode .notification-box {
background-color: #1a1a1a;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
}
.notification-box .notification-icon {
font-size: 2.2rem;
margin-bottom: 12px;
}
.notification-box h3 {
margin-top: 0;
margin-bottom: 12px;
font-size: 1.3rem;
}
body.dark-mode .notification-box h3 {
color: #f0f0f0;
}
.notification-box p {
margin-bottom: 24px;
line-height: 1.5;
color: #555;
white-space: pre-line;
}
body.dark-mode .notification-box p {
color: #ccc;
}
.notification-box button {
padding: 10px 28px;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
background-color: #007bff;
color: white;
transition: background-color 0.2s ease;
}
.notification-box button:hover {
background-color: #0056b3;
}
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
set -e
echo "Starte automatischen Merge- und Tag-Prozess..."
git config core.filemode false
echo "Aktualisiere lokale Branches vom Remote..."
git fetch --all --quiet
echo "Wechsle zu main..."
git checkout main --quiet
git pull origin main --quiet
echo "Merge: main -> dev..."
git checkout dev --quiet
git pull origin dev --quiet || true
git merge main --no-ff -m "Automated merge from main to dev" --quiet
git push origin dev --quiet
echo "Merge: dev -> live..."
git checkout live --quiet
git pull origin live --quiet || true
git merge dev --no-ff -m "Automated merge from dev to live" --quiet
git push origin live --quiet
TAG_NAME="v$(date +'%Y.%m.%d-%H%M')"
echo " Erstelle Tag: $TAG_NAME..."
git tag -a "$TAG_NAME" -m "Automated release tag $TAG_NAME"
git push origin "$TAG_NAME" --quiet
git checkout main --quiet
echo ""
echo "========================================================"
echo "✅ FERTIG! Alles sauber durchgelaufen."
echo "📦 Neuer Release-Tag gepusht: $TAG_NAME"
echo "========================================================"