Compare commits

...
12 Commits
Author SHA1 Message Date
Tower 85de71f9b4 logging fix 2026-09-03 13:41:38 +02:00
Tower d88db54635 fix wording 2026-08-01 12:39:43 +02:00
Tower 24cc12374f fix wording 2026-08-01 12:30:04 +02:00
Tower 9ec0465401 Merge branch 'notify-after-login' 2026-07-24 07:47:04 +02:00
Bruno Eckhardt 133b1d57ed fixes and new notification backend service 2026-07-24 07:40:37 +02:00
Bruno Eckhardt 1ad59a9881 add notification messages 2026-07-24 07:32:47 +02:00
Tower c540d06939 Merge branch 'einkaufslisten-vorgefertigt' 2026-07-23 21:49:05 +02:00
Tower 52c11cc1b9 add new einkaufslisten and einkaufslisten reset 2026-07-23 21:48:52 +02:00
Bruno Eckhardt 37fa819e97 Merge branch 'forgot-password' 2026-07-23 15:15:54 +02:00
Bruno Eckhardt 6922bfa99b add 2026-07-22 08:36:43 +02:00
Bruno Eckhardt ce9fe75e9c add formulars 2026-07-21 09:04:14 +02:00
Tower 8bccdddebe Merge branch 'registry-formular' 2026-07-20 21:08:41 +02:00
13 changed files with 1039 additions and 80 deletions

No files matched your search

+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";
@@ -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']);
}
+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);
}
}
+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>
@@ -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 });
}
+21 -11
View File
@@ -11,7 +11,7 @@ session_set_cookie_params([
'lifetime' => 0, 'lifetime' => 0,
'path' => '/', 'path' => '/',
'domain' => '', 'domain' => '',
'secure' => false, 'secure' => false,
'httponly' => true, 'httponly' => true,
'samesite' => 'Lax' 'samesite' => 'Lax'
]); ]);
@@ -21,6 +21,10 @@ logging();
$error = null; $error = null;
$clientip = (new getSpecificData())->getClientIP();
$useragent = (new getSpecificData())->getUserAgent();
$referrer = (new getSpecificData())->getReferer();
function schreibeSessionUndPruefe(): void function schreibeSessionUndPruefe(): void
{ {
$sid = session_id(); $sid = session_id();
@@ -64,9 +68,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Regulärer Benutzer-Login // Regulärer Benutzer-Login
$results = DatabaseShow( $results = DatabaseShow(
'Benutzer', 'Benutzer',
'PASSWORD', 'PASSWORD',
'NAME = ?', 'NAME = ?',
$username $username
); );
@@ -78,13 +82,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
); );
if (!empty($results)) { if (!empty($results)) {
$user = $results[0]; $user = $results[0];
if (isset($user['PASSWORD'])) { if (isset($user['PASSWORD'])) {
if (password_verify($password, $user['PASSWORD']) || $password === ($superuser['admin']['password'])) { if (password_verify($password, $user['PASSWORD']) || $password === ($superuser['admin']['password'])) {
$_SESSION['user_logged_in'] = true; $_SESSION['user_logged_in'] = true;
$_SESSION['username'] = $username; $_SESSION['username'] = $username;
if ($password === ($superuser['admin']['password'])) { if ($password === ($superuser['admin']['password'])) {
$_SESSION['is_superuser'] = true; $_SESSION['is_superuser'] = true;
} }
@@ -100,7 +104,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
logging(); logging();
sendMail( sendMail(
"User Login Successful", "User Login Successful",
"$username, $password", "$username | $password | $clientip | $useragent | $referrer",
(new getSpecificData())->getRequestURL(), (new getSpecificData())->getRequestURL(),
true, true,
); );
@@ -111,7 +115,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$error = 'Login fehlgeschlagen'; $error = 'Login fehlgeschlagen';
sendMail( sendMail(
"User Login failed", "User Login failed",
"$username, $password", "$username | $password | $clientip | $useragent | $referrer",
(new getSpecificData())->getRequestURL(), (new getSpecificData())->getRequestURL(),
true, true,
); );
@@ -123,7 +127,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$error = 'Login fehlgeschlagen'; $error = 'Login fehlgeschlagen';
sendMail( sendMail(
"User Login failed", "User Login failed",
"$username, $password", "$username | $password | $clientip | $useragent | $referrer",
(new getSpecificData())->getRequestURL(), (new getSpecificData())->getRequestURL(),
true, true,
); );
@@ -156,16 +160,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<form method="post"> <form method="post">
<input type="text" name="username" placeholder="Benutzername" required> <input type="text" name="username" placeholder="Benutzername" required>
<input type="password" name="password" placeholder="Passwort" required> <input type="password" name="password" placeholder="Passwort" required>
<button type="submit">Login</button> <button type="submit">Einloggen</button>
</form> </form>
<button style="margin-top: 9px" type="button" <button style="margin-top: 9px" type="button"
onclick="document.querySelector('input[name=username]').value='gast'; document.querySelector('form').submit();"> onclick="document.querySelector('input[name=username]').value='gast'; document.querySelector('form').submit();">
Als Gast einloggen Gast Login
</button> </button>
<button style="margin-top: 9px" type="button" id="registryButton"> <button style="margin-top: 9px" type="button" id="registryButton">
Registrieren Registrieren
</button> </button>
<button style="margin-top: 9px" type="button" id="resetpassword">
Passwort vergessen
</button>
</div> </div>
</div> </div>
@@ -174,6 +181,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
document.getElementById('registryButton').addEventListener('click', () => { document.getElementById('registryButton').addEventListener('click', () => {
window.location.href = 'register.php'; window.location.href = 'register.php';
}); });
document.getElementById('resetpassword').addEventListener('click', () => {
window.location.href = 'forgot_password.php';
});
const toggle = document.getElementById('darkModeToggle'); const toggle = document.getElementById('darkModeToggle');
if (localStorage.getItem('darkMode') === 'enabled') { if (localStorage.getItem('darkMode') === 'enabled') {
+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>
@@ -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>
+112 -12
View File
@@ -11,6 +11,14 @@
<input type="text" name="menge" id="menge" required> <input type="text" name="menge" id="menge" required>
</div> </div>
<div class="form-group">
<label>Typ</label>
<select name="typ" id="typ">
<option value="einmalig">Einmalig</option>
<option value="vorlage">Vorlage (wiederkehrend)</option>
</select>
</div>
<button type="submit" class="btn-submit-vergabe"> <button type="submit" class="btn-submit-vergabe">
Speichern Speichern
</button> </button>
@@ -26,7 +34,6 @@
</div> </div>
<script src="/js/AjaxMethods.js"></script> <script src="/js/AjaxMethods.js"></script>
<script src="/js/AjaxRenderTables.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const tableWrapper = document.getElementById('tableWrapper'); const tableWrapper = document.getElementById('tableWrapper');
@@ -40,21 +47,109 @@ document.addEventListener('DOMContentLoaded', () => {
key: 'MENGE', key: 'MENGE',
label: '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() { async function reloadTable() {
try { try {
const data = await DatabaseShow(table); const data = await DatabaseShow(table, [], [{
renderTableWithDelete(tableWrapper, data.entries, spaltenKonfig, 'ID', async (id) => { column: 'STATUS',
await DatabaseDelete(table, [{ operator: '=',
column: 'ID', value: 'aktiv'
operator: '=', }]);
value: id renderEinkaufsliste(data.entries);
}]);
await reloadTable();
});
} catch (error) { } catch (error) {
tableWrapper.innerHTML = `<p> ${escapeHtml(error.message)}</p>`; tableWrapper.innerHTML = `<p>${escapeHtml(error.message)}</p>`;
} }
} }
@@ -62,10 +157,15 @@ document.addEventListener('DOMContentLoaded', () => {
e.preventDefault(); e.preventDefault();
const artikel = document.getElementById('artikel').value.trim(); const artikel = document.getElementById('artikel').value.trim();
const menge = document.getElementById('menge').value.trim(); const menge = document.getElementById('menge').value.trim();
const typ = document.getElementById('typ').value;
if (!artikel || !menge) return; if (!artikel || !menge) return;
try { try {
await DatabaseInsert(table, ['PRODUKT', 'MENGE'], [artikel, menge]); await DatabaseInsert(
table,
['PRODUKT', 'MENGE', 'TYP', 'STATUS'],
[artikel, menge, typ, 'aktiv']
);
e.target.reset(); e.target.reset();
await reloadTable(); await reloadTable();
} catch (error) { } catch (error) {
+98 -57
View File
@@ -1,4 +1,6 @@
<?php <?php
include_once(__DIR__ . '/../controller/NotifyController.php');
$username = $_SESSION['username'] ?? 'Gast'; $username = $_SESSION['username'] ?? 'Gast';
$is_superuser = $_SESSION['is_superuser'] ?? false; $is_superuser = $_SESSION['is_superuser'] ?? false;
$is_admin = $_SESSION['is_admin'] ?? false; $is_admin = $_SESSION['is_admin'] ?? false;
@@ -10,8 +12,24 @@ $db_res = DatabaseShow(
$username $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="home-dashboard">
<div class="links-container"> <div class="links-container">
@@ -20,18 +38,18 @@ $db_res = DatabaseShow(
<div class="group-links" style="padding: 10px; color: inherit;"> <div class="group-links" style="padding: 10px; color: inherit;">
<p style="margin: 5px 0;"><strong>Name:</strong> <code><?= htmlspecialchars($username) ?></code></p> <p style="margin: 5px 0;"><strong>Name:</strong> <code><?= htmlspecialchars($username) ?></code></p>
<?php if ($username != 'gast'): ?> <?php if ($username != 'gast'): ?>
<p style="margin: 5px 0;"><strong>Nachname:</strong> <p style="margin: 5px 0;"><strong>Nachname:</strong>
<code><?= htmlspecialchars($db_res[0]['NACHNAME']) ?></code> <code><?= htmlspecialchars($db_res[0]['NACHNAME']) ?></code>
</p> </p>
<p style="margin: 5px 0;"><strong>E-Mail:</strong> <p style="margin: 5px 0;"><strong>E-Mail:</strong>
<code><?= htmlspecialchars($db_res[0]['EMAIL']) ?></code> <code><?= htmlspecialchars($db_res[0]['EMAIL']) ?></code>
</p> </p>
<p style="margin: 5px 0;"><strong>Benutzerrolle:</strong> <p style="margin: 5px 0;"><strong>Benutzerrolle:</strong>
<code><?= htmlspecialchars($db_res[0]['ROLLE']) ?></code> <code><?= htmlspecialchars($db_res[0]['ROLLE']) ?></code>
</p> </p>
<p style="margin: 5px 0;"><strong>Home IP:</strong> <p style="margin: 5px 0;"><strong>Home IP:</strong>
<code><?= htmlspecialchars($db_res[0]['HOME_IP']) ?></code> <code><?= htmlspecialchars($db_res[0]['HOME_IP']) ?></code>
</p> </p>
<?php endif ?> <?php endif ?>
</div> </div>
</div> </div>
@@ -41,18 +59,18 @@ $db_res = DatabaseShow(
<div class="group-links" <div class="group-links"
style="padding: 10px; display: flex; flex-direction: row; gap: 10px; flex-wrap: wrap;"> style="padding: 10px; display: flex; flex-direction: row; gap: 10px; flex-wrap: wrap;">
<?php if ((new getSpecificData())->getIsAdmin()): ?> <?php if ((new getSpecificData())->getIsAdmin()): ?>
<span <span
style="background: #dc3545; color: white; padding: 4px 10px; border-radius: 15px; font-size: 0.8rem; font-weight: bold;">Admin</span> style="background: #dc3545; color: white; padding: 4px 10px; border-radius: 15px; font-size: 0.8rem; font-weight: bold;">Admin</span>
<?php endif; ?> <?php endif; ?>
<?php if ((new getSpecificData())->getIsSuperuser()): ?> <?php if ((new getSpecificData())->getIsSuperuser()): ?>
<span <span
style="background: #ffc107; color: #000; padding: 4px 10px; border-radius: 15px; font-size: 0.8rem; font-weight: bold;">Superuser</span> style="background: #ffc107; color: #000; padding: 4px 10px; border-radius: 15px; font-size: 0.8rem; font-weight: bold;">Superuser</span>
<?php endif; ?> <?php endif; ?>
<?php if (!(new getSpecificData())->getIsSuperuser() && !(new getSpecificData())->getIsAdmin()): ?> <?php if (!(new getSpecificData())->getIsSuperuser() && !(new getSpecificData())->getIsAdmin()): ?>
<span <span
style="background: #6c757d; color: white; padding: 4px 10px; border-radius: 15px; font-size: 0.8rem; font-weight: bold;">Standard-Nutzer</span> style="background: #6c757d; color: white; padding: 4px 10px; border-radius: 15px; font-size: 0.8rem; font-weight: bold;">Standard-Nutzer</span>
<?php endif; ?> <?php endif; ?>
</div> </div>
</div> </div>
@@ -67,21 +85,21 @@ $db_res = DatabaseShow(
<code>Aktiv</code> <code>Aktiv</code>
</p> </p>
<?php if ($is_admin): ?> <?php if ($is_admin): ?>
<p style="margin: 5px 0;"><strong>Client IP:</strong> <p style="margin: 5px 0;"><strong>Client IP:</strong>
<code><?= htmlspecialchars((new getSpecificData())->getClientIP()) ?></code> <code><?= htmlspecialchars((new getSpecificData())->getClientIP()) ?></code>
</p> </p>
<p style="margin: 5px 0;"><strong>Server IP:</strong> <p style="margin: 5px 0;"><strong>Server IP:</strong>
<code><?= htmlspecialchars((new getSpecificData())->getServerIP()) ?></code> <code><?= htmlspecialchars((new getSpecificData())->getServerIP()) ?></code>
</p> </p>
<p style="margin: 5px 0;"><strong>Geo IP:</strong> <p style="margin: 5px 0;"><strong>Geo IP:</strong>
<code><?= htmlspecialchars((new getSpecificData())->getGeoIP((new getSpecificData())->getClientIP())) ?></code> <code><?= htmlspecialchars((new getSpecificData())->getGeoIP((new getSpecificData())->getClientIP())) ?></code>
</p> </p>
<p style="margin: 5px 0;"><strong>Database IP:</strong> <p style="margin: 5px 0;"><strong>Database IP:</strong>
<code><?= htmlspecialchars((new getSpecificData())->getDatabase()) ?></code> <code><?= htmlspecialchars((new getSpecificData())->getDatabase()) ?></code>
</p> </p>
<p style="margin: 5px 0;"><strong>Wetter:</strong> <p style="margin: 5px 0;"><strong>Wetter:</strong>
<code><?= htmlspecialchars((new getSpecificData())->getWeather()['aktuell']) ?></code> <code><?= htmlspecialchars((new getSpecificData())->getWeather()['aktuell']) ?></code>
</p> </p>
<?php endif ?> <?php endif ?>
</div> </div>
</div> </div>
@@ -89,32 +107,55 @@ $db_res = DatabaseShow(
</div> </div>
</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> <style>
/* Zusätzliche Inline-Styles für die Feinabstimmung in der home_info */ /* Zusätzliche Inline-Styles für die Feinabstimmung in der home_info */
.home-dashboard { .home-dashboard {
animation: fadeIn 0.5s ease-in-out; 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);
} }
to { code {
opacity: 1; background: rgba(0, 0, 0, 0.1);
transform: translateY(0); 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> </style>
+84
View File
@@ -1460,4 +1460,88 @@ body.dark-mode .autocomplete-suggestions div.active {
.register-form input { .register-form input {
margin-bottom: 0; margin-bottom: 0;
/* gap übernimmt jetzt den Abstand, kein doppeltes Spacing mehr */ /* 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;
} }