add settings locations
Some checks failed
/ build (push) Has been cancelled

This commit is contained in:
Carsten Graf
2025-09-08 22:30:15 +02:00
parent 55eb062d2c
commit 173b13fcfc
13 changed files with 940 additions and 247 deletions

View File

@@ -1,4 +1,4 @@
<!doctype html>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
@@ -124,7 +124,7 @@
// Globale Variablen
let rfidData = [];
let isLoading = false;
let DBUrl = "db.reptilfpv.de:3000";
let DBUrl = "ninja.reptilfpv.de:3000";
var APIKey;
// Maximales Datum auf heute setzen
@@ -166,7 +166,7 @@
ageDisplay.style.display = "none";
if (age < 0) {
showErrorMessage(
"Das Geburtsdatum kann nicht in der Zukunft liegen!",
"Das Geburtsdatum kann nicht in der Zukunft liegen!"
);
e.target.value = "";
} else {
@@ -203,7 +203,7 @@
const alter = calculateAge(geburtsdatum);
if (alter < 0) {
showErrorMessage(
"Das Geburtsdatum kann nicht in der Zukunft liegen!",
"Das Geburtsdatum kann nicht in der Zukunft liegen!"
);
return;
}
@@ -217,6 +217,7 @@
method: "POST",
headers: {
"Content-Type": "application/json",
...(APIKey && { Authorization: `Bearer ${APIKey}` }),
},
body: JSON.stringify({
uid: uid,
@@ -243,13 +244,13 @@
} else {
// Fehler anzeigen
showErrorMessage(
result.error || "Fehler beim Speichern der Daten",
result.error || "Fehler beim Speichern der Daten"
);
}
} catch (error) {
console.error("Fehler beim Speichern:", error);
showErrorMessage(
"Verbindungsfehler zum Server. Bitte versuchen Sie es später erneut.",
"Verbindungsfehler zum Server. Bitte versuchen Sie es später erneut."
);
} finally {
setLoadingState(false);
@@ -355,6 +356,7 @@
method: "GET",
headers: {
"Content-Type": "application/json",
...(APIKey && { Authorization: `Bearer ${APIKey}` }),
},
});
@@ -394,7 +396,7 @@
} catch (error) {
console.error("Fehler beim Lesen der UID:", error);
showErrorMessage(
"Verbindungsfehler zum RFID Reader. Bitte prüfen Sie die Verbindung.",
"Verbindungsfehler zum RFID Reader. Bitte prüfen Sie die Verbindung."
);
// UID Feld rot markieren
@@ -412,12 +414,16 @@
async function checkServerStatus() {
try {
const response = await fetch("/api/health");
const response = await fetch("/api/health", {
headers: {
...(APIKey && { Authorization: `Bearer ${APIKey}` }),
},
});
const data = await response.json();
if (!data.status || data.status !== "connected") {
showErrorMessage(
"Server nicht verbunden. Einige Funktionen könnten eingeschränkt sein.",
"Server nicht verbunden. Einige Funktionen könnten eingeschränkt sein."
);
return false;
}
@@ -437,7 +443,7 @@
APIKey = data.licence || "";
})
.catch((error) =>
showMessage("Fehler beim Laden der Lizenz", "error"),
showMessage("Fehler beim Laden der Lizenz", "error")
);
}
</script>

View File

@@ -279,33 +279,138 @@
text-align: center;
}
.status-message {
margin-top: 20px;
padding: 15px;
border-radius: 10px;
font-weight: 600;
text-align: center;
/* Modern Notification Toast */
.notification-toast {
position: fixed;
top: 24px;
right: 24px;
min-width: 320px;
max-width: 400px;
background: rgba(255, 255, 255, 0.98);
border-radius: 16px;
box-shadow:
0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04),
0 0 0 1px rgba(0, 0, 0, 0.05);
backdrop-filter: blur(20px);
z-index: 99999;
display: none;
align-items: flex-start;
gap: 12px;
padding: 16px;
transform: translateX(100%);
opacity: 0;
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
pointer-events: auto;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.status-success {
background: #d4edda;
color: #155724;
border: 2px solid #c3e6cb;
.notification-toast.show {
transform: translateX(0);
opacity: 1;
}
.status-error {
background: #f8d7da;
color: #721c24;
border: 2px solid #f5c6cb;
.notification-icon {
flex-shrink: 0;
width: 40px;
height: 40px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 600;
color: white;
background: linear-gradient(135deg, #10b981, #059669);
}
.status-info {
background: #cce7ff;
color: #004085;
border: 2px solid #b3d9ff;
.notification-body {
flex: 1;
min-width: 0;
}
.notification-title {
font-size: 14px;
font-weight: 600;
color: #111827;
margin-bottom: 4px;
line-height: 1.2;
}
.notification-message {
font-size: 13px;
color: #6b7280;
line-height: 1.4;
word-wrap: break-word;
}
.notification-close {
flex-shrink: 0;
width: 32px;
height: 32px;
border: none;
background: none;
color: #9ca3af;
cursor: pointer;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
margin-top: -4px;
margin-right: -4px;
}
.notification-close:hover {
background: rgba(0, 0, 0, 0.05);
color: #374151;
}
.notification-close:active {
transform: scale(0.95);
}
/* Toast Types */
.notification-toast.success .notification-icon {
background: linear-gradient(135deg, #10b981, #059669);
}
.notification-toast.error .notification-icon {
background: linear-gradient(135deg, #ef4444, #dc2626);
}
.notification-toast.info .notification-icon {
background: linear-gradient(135deg, #3b82f6, #2563eb);
}
.notification-toast.warning .notification-icon {
background: linear-gradient(135deg, #f59e0b, #d97706);
}
/* Animation */
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOutRight {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
.learning-mode {
display: none;
text-align: center;
@@ -417,4 +522,14 @@
border-right: none;
border-bottom: 1px solid #e9ecef;
}
/* Mobile notification bubble adjustments */
.notification-bubble {
top: 10px;
right: 10px;
left: 10px;
max-width: none;
font-size: 14px;
padding: 12px 16px;
}
}

View File

@@ -11,6 +11,22 @@
<title>Ninjacross Timer - Einstellungen</title>
</head>
<body>
<!-- Modern Notification Toast -->
<div id="notificationBubble" class="notification-toast" style="display: none;">
<div class="notification-icon">
<span id="notificationIcon"></span>
</div>
<div class="notification-body">
<div class="notification-title" id="notificationTitle">Erfolg</div>
<div class="notification-message" id="notificationText">Bereit</div>
</div>
<button class="notification-close" onclick="hideNotification()">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 8.707l3.646 3.647.708-.707L8.707 8l3.647-3.646-.707-.708L8 7.293 4.354 3.646l-.707.708L7.293 8l-3.646 3.646.707.708L8 8.707z"/>
</svg>
</button>
</div>
<div class="container">
<!-- Header Section -->
<div class="header">
@@ -25,9 +41,6 @@
<a href="/rfid" class="nav-button">📡 RFID</a>
</div>
<!-- Status Message Container -->
<div id="statusMessage" class="status-message"></div>
<!-- Date & Time Section -->
<div class="section">
<h2>🕐 Datum & Uhrzeit</h2>
@@ -595,7 +608,7 @@
.then((response) => response.json())
.then((data) => {
document.getElementById("licencekey").value = data.licence || "";
loadLocations();
loadLocationsFromBackend();
})
.catch((error) =>
showMessage("Fehler beim Laden der Lizenz", "error")
@@ -1004,44 +1017,84 @@
//location functions
// Locations laden und Dropdown befüllen
function loadLocations() {
const licence = document.getElementById("licencekey").value; // Get the licence key from the input field
fetch("http://db.reptilfpv.de:3000/api/location/", {
method: "GET",
headers: {
Authorization: `Bearer ${licence}`, // Add Bearer token using licenkey
},
})
.then((response) => response.json())
.then((data) => {
const select = document.getElementById("locationSelect");
function loadLocationsFromBackend() {
const select = document.getElementById("locationSelect");
// Vorhandene Optionen löschen (außer der ersten "Bitte wählen...")
while (select.children.length > 1) {
select.removeChild(select.lastChild);
}
// Vorhandene Optionen löschen (außer der ersten "Bitte wählen...")
while (select.children.length > 1) {
select.removeChild(select.lastChild);
}
// Neue Optionen aus Backend-Response hinzufügen
// Neue Optionen aus Backend-Response hinzufügen
data.forEach((location) => {
const option = document.createElement("option");
option.value = location.id;
option.textContent = location.name;
select.appendChild(option);
});
// Aktuell gespeicherten Standort laden
loadCurrentLocation();
// Fallback: Statische Standorte falls API nicht verfügbar ist
const staticLocations = [
{ id: "1", name: "Hauptstandort" },
{ id: "2", name: "Standort A" },
{ id: "3", name: "Standort B" },
{ id: "4", name: "Teststandort" }
];
// Versuche zuerst die echte API zu verwenden
const licence = document.getElementById("licencekey").value;
if (licence && licence.trim() !== "") {
fetch("https://ninja.reptilfpv.de/api/v1/private/locations", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${licence}`,
},
})
.catch((error) => {
console.log("Locations konnten nicht geladen werden:", error);
showMessage("Fehler beim Laden der Locations", "error");
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
})
.then((data) => {
if (data.success && data.data) {
// API erfolgreich - verwende echte Daten
data.data.forEach((location) => {
const option = document.createElement("option");
option.value = location.id;
option.textContent = location.name;
select.appendChild(option);
});
showMessage("Standorte erfolgreich von API geladen", "success");
} else {
throw new Error("Ungültige API-Response");
}
// Aktuell gespeicherten Standort laden
loadSavedLocation();
})
.catch((error) => {
console.log("API nicht verfügbar, verwende statische Daten:", error);
// API fehlgeschlagen - verwende statische Daten als Fallback
staticLocations.forEach((location) => {
const option = document.createElement("option");
option.value = location.id;
option.textContent = location.name;
select.appendChild(option);
});
showMessage("Standorte geladen (statische Daten - API nicht verfügbar)", "warning");
// Aktuell gespeicherten Standort laden
loadSavedLocation();
});
} else {
// Kein Lizenz-Key - verwende statische Daten
staticLocations.forEach((location) => {
const option = document.createElement("option");
option.value = location.id;
option.textContent = location.name;
select.appendChild(option);
});
showMessage("Standorte geladen (statische Daten - kein Lizenz-Key)", "warning");
// Aktuell gespeicherten Standort laden
loadSavedLocation();
}
}
// Aktuell gespeicherten Standort laden
function loadCurrentLocation() {
fetch("/api/get-location")
function loadSavedLocation() {
fetch("/api/get-local-location")
.then((response) => response.json())
.then((data) => {
if (data.locationId) {
@@ -1114,7 +1167,7 @@
}
// Standort an Backend senden
fetch("/api/set-location", {
fetch("/api/set-local-location", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
@@ -1132,18 +1185,90 @@
.catch((error) => showMessage("Verbindungsfehler", "error"));
});
// Status-Nachricht anzeigen
function showMessage(message, type) {
const statusDiv = document.getElementById("statusMessage");
statusDiv.textContent = message;
statusDiv.className = `status-message status-${type}`;
statusDiv.style.display = "block";
// Moderne Notification anzeigen
function showMessage(message, type = 'info') {
console.log("showMessage called:", message, type);
const toast = document.getElementById("notificationBubble");
const icon = document.getElementById("notificationIcon");
const title = document.getElementById("notificationTitle");
const text = document.getElementById("notificationText");
if (!toast || !icon || !title || !text) {
console.error("Notification elements not found!");
return;
}
// Clear any existing timeout
if (window.notificationTimeout) {
clearTimeout(window.notificationTimeout);
}
// Set content
text.textContent = message;
// Set type-specific styling and content
toast.className = "notification-toast";
switch(type) {
case 'success':
toast.classList.add('success');
icon.textContent = '✓';
title.textContent = 'Erfolg';
break;
case 'error':
toast.classList.add('error');
icon.textContent = '✕';
title.textContent = 'Fehler';
break;
case 'info':
toast.classList.add('info');
icon.textContent = '';
title.textContent = 'Information';
break;
case 'warning':
toast.classList.add('warning');
icon.textContent = '⚠';
title.textContent = 'Warnung';
break;
default:
toast.classList.add('info');
icon.textContent = '';
title.textContent = 'Information';
}
// Show toast with animation
toast.style.display = "flex";
// Force reflow
toast.offsetHeight;
// Add show class after a small delay to ensure display is set
setTimeout(() => {
statusDiv.style.display = "none";
toast.classList.add('show');
}, 10);
// Auto-hide after 5 seconds
window.notificationTimeout = setTimeout(() => {
hideNotification();
}, 5000);
}
// Notification verstecken mit Animation
function hideNotification() {
const toast = document.getElementById("notificationBubble");
if (!toast) return;
// Clear timeout if exists
if (window.notificationTimeout) {
clearTimeout(window.notificationTimeout);
}
// Remove show class for animation
toast.classList.remove('show');
// Hide after animation completes
setTimeout(() => {
toast.style.display = "none";
}, 400); // Match CSS transition duration
}
// System-Info alle 30 Sekunden aktualisieren
setInterval(loadSystemInfo, 30000);
</script>