Compare commits

..
Author SHA1 Message Date
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 10472843a7 Merge branch 'termin-service' 2026-07-07 09:07:27 +02:00
52 changed files with 4129 additions and 1147 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",
+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";
+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,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']);
}
+1 -391
View File
@@ -6,394 +6,4 @@ $superuser = array (
'password' => 'bBHofurUJWKzPE8Z:',
'active' => true,
),
);
$serviceGroups = array (
'Info' =>
array (
'label' => 'Home',
'defaultOpen' => true,
'services' =>
array (
0 => 'home_info',
1 => 'vergabe',
2 => 'links',
3 => 'einkaufsliste',
4 => 'termine',
),
),
'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,
),
'vergabe' =>
array (
'name' => 'Vergabe',
'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,
),
'einkaufsliste' =>
array (
'name' => 'Einkaufsliste',
'active' => true,
'adminOnly' => false,
),
'termine' =>
array (
'name' => 'Termine',
'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',
),
);
);
+22 -14
View File
@@ -1,21 +1,23 @@
<?php
require(__DIR__ . '/DatabaseController.php');
include_once(__DIR__ . '/DatabaseController.php');
class ApiController
{
class ApiController {
/**
* method for pretty jsonsuccess output
*
* @param array $data
* @return string
*/
public function jsonSuccess($data) {
public function jsonSuccess($data)
{
return json_encode(
[
'status' => 'success',
'data' => $data,
],
],
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE
);
}
@@ -26,13 +28,14 @@ class ApiController {
* @param array $error
* @return string
*/
public function jsonError ($error) {
public function jsonError($error)
{
echo json_encode(
[
'status' => 'failed',
'data' => 'none',
'error' => $error
],
],
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE
);
}
@@ -42,7 +45,8 @@ class ApiController {
*
* @return array
*/
private function getAuthKey () {
private function getAuthKey()
{
$validTokens = DatabaseShow(
"API_Credentials",
"TOKEN"
@@ -58,7 +62,8 @@ class ApiController {
* @param string $function
* @return void
*/
public function urlAuth ($token, $function = '') {
public function urlAuth($token, $function = '')
{
$validTokens = $this->getAuthKey();
if (!in_array($token, $validTokens)) {
@@ -90,7 +95,8 @@ class ApiController {
return true;
}
public function requestSave($function = '') {
public function requestSave($function = '')
{
DatabaseInsert(
"API_LOGS",
"CLIENT_IP, TOKEN, API, FUNKTION, JSON",
@@ -106,7 +112,8 @@ class ApiController {
* create new api token
*
*/
private function createToken() {
private function createToken()
{
return bin2hex(random_bytes(32));
}
@@ -115,8 +122,9 @@ class ApiController {
*
* @return bool
*/
public function saveToken() {
try {
public function saveToken()
{
try {
DatabaseInsert(
"API_Credentials",
"TOKEN, BENUTZER",
@@ -128,4 +136,4 @@ class ApiController {
}
return true;
}
}
}
+2 -28
View File
@@ -108,13 +108,7 @@ 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
@@ -166,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;
}
}
?>
}
+23
View File
@@ -222,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);
}
}
-2
View File
@@ -61,8 +61,6 @@ class RightsController {
*/
public function RightsHandler($sitename, $name = 'Bruno', $lastname = 'Eckhardt') {
try {
$sitename = strtolower($sitename);
$this->saveSite($sitename);
DatabaseInsert(
"Benutzer_Berechtigungen",
-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 });
}
@@ -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 });
}
+71 -43
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,56 +142,61 @@ 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): ?>
<?php if ($error): ?>
<p class="login-error"><?= htmlspecialchars($error) ?></p>
<?php endif; ?>
<?php endif; ?>
<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>
<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();">
Als Gast einloggen
</button>
<button style="margin-top: 9px" type="button"
onclick="document.querySelector('input[name=username]').value='gast'; document.querySelector('form').submit();">
Als Gast einloggen
</button>
<button style="margin-top: 9px" type="button" id="registryButton">
Registrieren
</button>
<button style="margin-top: 9px" type="button" id="resetpassword">
Passwort vergessen
</button>
</div>
</div>
</body>
</div>
<script>
const toggle = document.getElementById('darkModeToggle');
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');
}
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');
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>
+9 -2
View File
@@ -1,10 +1,17 @@
<?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();
@@ -22,4 +29,4 @@ try {
true,
);
exit();
}
}
+7 -4
View File
@@ -3,7 +3,7 @@
include_once __DIR__ . '/../controller/DatabaseController.php';
include_once __DIR__ . '/../controller/DataController.php';
function sendMail($subject, $message, $url, bool $log, $to = "bruno.eckhardt07 qmail.com")
function sendMail($subject, $message, $url, bool $log, $to = "bruno.eckhardt07@gmail.com")
{
$headers = "From: bruno.eckhardt07@gmail.com\r\n";
mail(
@@ -29,9 +29,12 @@ function sendMail($subject, $message, $url, bool $log, $to = "bruno.eckhardt07 q
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,
);
@@ -40,9 +43,9 @@ 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>
+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>
+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>
+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
+162 -75
View File
@@ -1,91 +1,178 @@
<?php
include_once __DIR__ . '/../controller/DatabaseController.php';
include_once __DIR__ . '/../controller/RoutesController.php';
$view = $_GET['view'] ?? 'show';
$selectedService = $_GET['service'] ?? null;
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_entry'])) {
$artikel = $_POST['artikel'];
$menge = $_POST['menge'];
DatabaseInsert(
"Einkaufsliste",
"PRODUKT, MENGE",
$artikel,
$menge
);
redirectToWebsite((new getSpecificData())->getReferer());
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_entry'])) {
$id = $_POST['id'];
DatabaseDelete("Einkaufsliste", "ID = ?", $id);
redirectToWebsite((new getSpecificData())->getReferer());
exit;
}
$entries = DatabaseShow(
"Einkaufsliste"
);
?>
<div>
<div class="vergabe-form">
<form method="POST" class="vergabe-grid">
<form id="addForm" class="vergabe-grid">
<div class="form-group">
<label>Artikel</label>
<input type="text" name="artikel" required>
<input type="text" name="artikel" id="artikel" required>
</div>
<div class="form-group">
<label>Menge</label>
<input type="text" name="menge" required>
<input type="text" name="menge" id="menge" required>
</div>
<button type="submit" name="add_entry" class="btn-submit-vergabe">
💾 Speichern
</button>
<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>
<?php if (!empty($entries)): ?>
<table class="db-table mobile-responsive-table">
<thead>
<tr>
<th>Artikel</th>
<th>Menge</th>
<th style="width: 100px;">Aktion</th>
</tr>
</thead>
<tbody>
<?php foreach ($entries as $entry): ?>
<tr>
<td data-label="Artikel"><?= htmlspecialchars($entry['PRODUKT'] ?? '') ?></td>
<td data-label="Menge"><?= htmlspecialchars($entry['MENGE'] ?? '') ?></td>
<td data-label="Aktion">
<form method="POST" onsubmit="return confirm('Möchtest du diesen Eintrag wirklich löschen?');" style="margin: 0;">
<input type="hidden" name="id" value="<?= $entry['ID'] ?? '' ?>">
<button type="submit" name="delete_entry" class="btn-delete">
🗑️ Löschen
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p>Keine Einträge vorhanden.</p>
<?php endif; ?>
<div id="tableWrapper">
<p>Lade Daten...</p>
</div>
</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; ?>
+159 -179
View File
@@ -1,108 +1,8 @@
<?php
include_once(__DIR__ . '/../controller/DatabaseController.php');
include_once(__DIR__ . '/../controller/DataController.php');
$msg = "";
$username = $_SESSION['username'] ?? 'Gast';
if (isset($_GET['action']) && $_GET['action'] === 'delete') {
if (isset($_GET['serien_id']) && !empty($_GET['serien_id'])) {
$serienId = $_GET['serien_id'];
DatabaseDelete("Termine", "SERIEN_ID = ?", $serienId);
$msg = "Komplette Terminserie erfolgreich gelöscht!";
} elseif (isset($_GET['id'])) {
$deleteId = (int)$_GET['id'];
DatabaseDelete("Termine", "ID = ?", $deleteId);
$msg = "Termin erfolgreich gelöscht!";
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_termin'])) {
$datum = $_POST['datum'] ?? '';
$zeit = $_POST['zeit'] ?? '';
$name = $_POST['name'] ?? '';
$notiz = $_POST['notiz'] ?? '';
$wiederholung = $_POST['wiederholung'] ?? 'none';
$enddatum = $_POST['enddatum'] ?? '';
if (!empty($datum) && !empty($zeit) && !empty($name)) {
if ($wiederholung !== 'none' && !empty($enddatum)) {
$currentDate = new DateTime($datum);
$endDate = new DateTime($enddatum);
if ($endDate < $currentDate) {
$msg = "Das Enddatum darf nicht vor dem Startdatum liegen.";
} else {
$insertedCount = 0;
$serienId = uniqid('serie_', true);
$intervalMapping = [
'daily' => '+1 day',
'weekly' => '+1 week',
'monthly' => '+1 month'
];
$modifier = $intervalMapping[$wiederholung] ?? null;
if ($modifier) {
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);
}
$msg = "Serientermin ($insertedCount Termine) erfolgreich eingetragen!";
}
}
} else {
DatabaseInsert(
"Termine",
"DATUM, ZEIT, NAME, NOTIZ, ERSTELLT_VON",
$datum,
$zeit,
$name,
$notiz,
$username
);
$msg = "Termin erfolgreich eingetragen!";
}
} else {
$msg = "Bitte alle Pflichtfelder ausfüllen.";
}
}
$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
);
$intervallNamen = [
'daily' => 'Täglich',
'weekly' => 'Wöchentlich',
'monthly' => 'Monatlich'
];
?>
<?php if (!empty($msg)): ?>
<div class="status-msg"><?= htmlspecialchars($msg) ?></div>
<?php endif; ?>
<div id="statusMsg" class="status-msg" style="display:none;"></div>
<div class="form-container">
<h3>📅 Neuen Termin vergeben</h3>
<form method="POST" class="vergabe-form">
<form id="addTerminForm" class="vergabe-form">
<div class="vergabe-grid">
<div class="form-group">
<label for="datum">Start-Datum *</label>
@@ -123,7 +23,7 @@ $intervallNamen = [
</div>
<div class="form-group">
<label for="wiederholung">Wiederholung</label>
<select id="wiederholung" name="wiederholung" onchange="toggleEnddatum()"
<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>
@@ -135,7 +35,7 @@ $intervallNamen = [
<label for="enddatum">Wiederholen bis *</label>
<input type="date" id="enddatum" name="enddatum">
</div>
<button type="submit" name="add_termin" class="btn-submit-vergabe">Speichern</button>
<button type="submit" class="btn-submit-vergabe">Speichern</button>
</div>
</form>
</div>
@@ -143,85 +43,165 @@ $intervallNamen = [
<div class="form-container">
<h3>📋 Aktuelle Terminbelegungen</h3>
<div class="table-container">
<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>
<?php if (empty($termine)): ?>
<tr>
<td colspan="6" style="text-align: center; padding: 20px;">Keine Termine eingetragen.</td>
</tr>
<?php else: ?>
<?php foreach ($termine as $termin): ?>
<?php
$dateFormatted = date('d.m.Y', strtotime($termin['DATUM']));
$timeFormatted = date('H:i', strtotime($termin['ZEIT']));
$istSerie = !empty($termin['SERIEN_ID']);
?>
<tr>
<td data-label="Datum:">
<strong><?= htmlspecialchars($dateFormatted) ?></strong>
<?php if ($istSerie): ?>
<br>
<small style="color: var(--primary-color, #007bff); font-style: italic;">
(<?= $intervallNamen[$termin['INTERVALL']] ?? '' ?>
bis <?= date('d.m.Y', strtotime($termin['SERIEN_ENDE'])) ?>)
</small>
<?php endif; ?>
</td>
<td data-label="Uhrzeit:"><?= htmlspecialchars($timeFormatted) ?></td>
<td data-label="Termin / Name:">
<strong><?= htmlspecialchars($termin['NAME']) ?></strong>
<?php if ($istSerie): ?>
<span
style="font-size: 0.8em; background: #e1f5fe; color: #0288d1; padding: 2px 6px; border-radius: 10px; margin-left: 5px;">Wiederholend</span>
<?php endif; ?>
</td>
<td data-label="Notiz:"><?= htmlspecialchars($termin['NOTIZ'] ?: '-') ?></td>
<td data-label="Von:"><span class="badge"><?= htmlspecialchars($termin['ERSTELLT_VON']) ?></span>
</td>
<td data-label="Aktion:">
<?php if ($istSerie): ?>
<a href="?service=termine&action=delete&serien_id=<?= urlencode($termin['SERIEN_ID']) ?>"
onclick="return confirm('Möchtest du die GESAMTE Terminserie löschen?');">
<button class="btn-delete" style="background-color: #d32f2f;">löschen</button>
</a>
<?php else: ?>
<a href="?service=termine&action=delete&id=<?= $termin['ID'] ?>"
onclick="return confirm('Möchtest du diesen Termin wirklich löschen?');">
<button class="btn-delete">Löschen</button>
</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<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>
function toggleEnddatum() {
var wiederholung = document.getElementById('wiederholung').value;
var enddatumGroup = document.getElementById('enddatum-group');
var enddatumInput = document.getElementById('enddatum');
document.addEventListener('DOMContentLoaded', () => {
const statusMsg = document.getElementById('statusMsg');
const tableWrapper = document.getElementById('termineTableWrapper');
if (wiederholung !== 'none') {
enddatumGroup.style.display = 'block';
enddatumInput.required = true;
} else {
enddatumGroup.style.display = 'none';
enddatumInput.required = false;
enddatumInput.value = '';
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>
+256 -83
View File
@@ -1,111 +1,284 @@
<?php
include_once __DIR__ . '/../controller/DatabaseController.php';
include_once __DIR__ . '/../controller/RoutesController.php';
$conn = db_connect();
$view = $_GET['view'] ?? 'show';
$selectedService = $_GET['service'] ?? null;
// --- LOGIK: Überstunden-Eintrag (Abbau) speichern ---
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_entry'])) {
$was = $_POST['was'];
$wer = $_POST['wer'];
$wann = $_POST['wann'];
$rueckgabe = $_POST['rueckgabe'];
DatabaseInsert(
"Vergabe",
"WAS, WER, WANN, RUECKGABE",
$was,
$wer,
$wann,
$rueckgabe
);
redirectToWebsite((new getSpecificData())->getReferer());
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['delete_entry'])) {
$id = $_POST['id'];
DatabaseDelete("Vergabe", "ID = ?", $id);
redirectToWebsite((new getSpecificData())->getReferer());
exit;
}
$entries = DatabaseShow(
"Vergabe"
);
?>
<div class="arbeitsberichte-wrapper">
<div class="vergabe-form">
<form method="POST" class="vergabe-grid">
<form id="addForm" class="vergabe-grid">
<div class="form-group">
<label>Wann ausgeliehen</label>
<input type="date" name="wann" value="<?= date('Y-m-d') ?>" required>
<input type="date" name="wann" id="in-wann" required>
</div>
<div class="form-group">
<label>Rückgabe</label>
<input type="date" name="rueckgabe" value="<?= date('Y-m-d') ?>" required>
<input type="date" name="rueckgabe" id="in-rueckgabe" required>
</div>
<div class="form-group">
<div class="form-group autocomplete-wrapper">
<label>An wen</label>
<input type="text" name="wer" placeholder="Name eingeben" required>
<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">
<div class="form-group autocomplete-wrapper">
<label>Was</label>
<input type="text" name="was" placeholder="Gegenstand eingeben" required>
<input type="text" id="was-input" placeholder="Gegenstand eingeben" required autocomplete="off">
<div class="autocomplete-suggestions" id="was-suggestions"></div>
</div>
<button type="submit" name="add_entry" class="btn-submit-vergabe">
💾 Speichern
</button>
<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>Aktuelle Vergaben</h3>
<?php if (!empty($entries)): ?>
<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>Was</th><th>Wer</th><th>Wann ausgeliehen</th><th>Rückgabe</th>
<th style="width: 100px;">Aktion</th>
</tr>
</thead>
<tbody>
<?php foreach ($entries as $entry): ?>
<tr>
<td data-label="Was"><?= htmlspecialchars($entry['WAS'] ?? '') ?></td>
<td data-label="Wer"><?= htmlspecialchars($entry['WER'] ?? '') ?></td>
<td data-label="Wann"><?= htmlspecialchars($entry['WANN'] ?? '') ?></td>
<td data-label="Rückgabe"><?= htmlspecialchars($entry['RUECKGABE'] ?? '') ?></td>
<td data-label="Aktion">
<form method="POST" onsubmit="return confirm('Möchtest du diesen Eintrag wirklich löschen?');" style="margin: 0;">
<input type="hidden" name="id" value="<?= $entry['ID'] ?? '' ?>">
<button type="submit" name="delete_entry" class="btn-delete">
🗑️ Löschen
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p>Keine Einträge vorhanden.</p>
<?php endif; ?>
</div>
</div>
`;
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>
+270 -6
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 {
@@ -981,8 +992,7 @@ body.dark-mode .info-value {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
/* table-layout: fixed; */
/* sorgt für feste Spaltenbreite */
table-layout: fixed;
}
.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 {
@@ -1280,4 +1293,255 @@ body.dark-mode input[type="date"] {
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 "========================================================"