This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user