Initial commit for Python Version
This commit is contained in:
+149
@@ -0,0 +1,149 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"timestamp": "2026-05-28T13:06:19.854940",
|
||||||
|
"username": "beta",
|
||||||
|
"endpoint": "/",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-05-28T13:06:58.223722",
|
||||||
|
"username": "beta",
|
||||||
|
"endpoint": "/",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-05-28T13:07:05.902636",
|
||||||
|
"username": "beta",
|
||||||
|
"endpoint": "/",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-05-28T13:07:51.345366",
|
||||||
|
"username": "beta",
|
||||||
|
"endpoint": "/",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-05-28T13:09:37.481260",
|
||||||
|
"username": "beta",
|
||||||
|
"endpoint": "/",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-05-28T13:14:02.867188",
|
||||||
|
"username": "beta",
|
||||||
|
"endpoint": "/",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-05-28T13:28:08.386403",
|
||||||
|
"username": "beta",
|
||||||
|
"endpoint": "/",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-05-28T13:29:06.541184",
|
||||||
|
"username": "beta",
|
||||||
|
"endpoint": "/",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-05-28T13:30:47.979071",
|
||||||
|
"username": "mtazl",
|
||||||
|
"endpoint": "/",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-05-28T13:46:05.447815",
|
||||||
|
"username": "beta",
|
||||||
|
"endpoint": "/",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-05-28T13:46:19.317688",
|
||||||
|
"username": "beta",
|
||||||
|
"endpoint": "/admin",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 403
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-05-28T14:42:29.477028",
|
||||||
|
"username": "mtazl",
|
||||||
|
"endpoint": "/",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-05-28T14:43:31.374462",
|
||||||
|
"username": "admin",
|
||||||
|
"endpoint": "/admin",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-05-28T14:43:31.411860",
|
||||||
|
"username": "admin",
|
||||||
|
"endpoint": "/api/admin/stats",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-05-28T14:43:31.562482",
|
||||||
|
"username": "admin",
|
||||||
|
"endpoint": "/api/admin/logs",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-05-28T14:44:01.420524",
|
||||||
|
"username": "admin",
|
||||||
|
"endpoint": "/api/admin/stats",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-05-28T14:44:01.438789",
|
||||||
|
"username": "admin",
|
||||||
|
"endpoint": "/api/admin/logs",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-05-28T14:44:13.773466",
|
||||||
|
"username": "admin",
|
||||||
|
"endpoint": "/admin",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-05-28T14:44:13.817782",
|
||||||
|
"username": "admin",
|
||||||
|
"endpoint": "/api/admin/stats",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-05-28T14:44:13.894128",
|
||||||
|
"username": "admin",
|
||||||
|
"endpoint": "/api/admin/logs",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"timestamp": "2026-06-03T05:20:48.808775",
|
||||||
|
"username": "mtazl",
|
||||||
|
"endpoint": "/",
|
||||||
|
"method": "GET",
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
]
|
||||||
+849
@@ -0,0 +1,849 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Roll Calculator Admin</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Arial, sans-serif;
|
||||||
|
background: #f4f6f9;
|
||||||
|
color: #333;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #003366 0%, #0066cc 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px 30px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.header h1 { font-size: 20px; }
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 30px auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
background: #fffbea;
|
||||||
|
border-left: 4px solid #f0b429;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 2px solid #e0e8f0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.tab-btn {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.tab-btn.active {
|
||||||
|
color: #0066cc;
|
||||||
|
border-bottom-color: #0066cc;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content { display: none; }
|
||||||
|
.tab-content.active { display: block; }
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #003366;
|
||||||
|
}
|
||||||
|
.card > p {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.form-group label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
}
|
||||||
|
.form-group input, .form-group textarea, .form-group select {
|
||||||
|
padding: 9px 11px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: 'Segoe UI', Arial, sans-serif;
|
||||||
|
transition: border 0.2s;
|
||||||
|
}
|
||||||
|
.form-group textarea {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
.form-group input:focus, .form-group textarea:focus, .form-group select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #0066cc;
|
||||||
|
box-shadow: 0 0 0 3px rgba(0,102,204,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: #0066cc;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-primary:hover { background: #0052a3; box-shadow: 0 2px 6px rgba(0,102,204,0.3); }
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #e0e0e0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.btn-secondary:hover { background: #d0d0d0; }
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-danger:hover { background: #c82333; }
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-success:hover { background: #218838; }
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
table th {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 2px solid #0066cc;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
table td {
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
table tr:hover { background: #f9f9f9; }
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.alert.show { display: block; }
|
||||||
|
.alert-success {
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
.alert-error {
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
.alert-info {
|
||||||
|
background: #d1ecf1;
|
||||||
|
border: 1px solid #bee5eb;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.action-buttons button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.edit-btn {
|
||||||
|
background: #0066cc;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.edit-btn:hover { background: #0052a3; }
|
||||||
|
.delete-btn {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.delete-btn:hover { background: #c82333; }
|
||||||
|
|
||||||
|
.password-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
.password-box {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 32px;
|
||||||
|
max-width: 400px;
|
||||||
|
box-shadow: 0 8px 40px rgba(0,0,0,0.3);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.password-box h2 { margin-bottom: 20px; color: #003366; }
|
||||||
|
.password-box input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 11px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.password-box .btn { width: 100%; margin-bottom: 10px; }
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.stat {
|
||||||
|
background: linear-gradient(135deg, #e8f4fd 0%, #d0e8fa 100%);
|
||||||
|
border: 1px solid #0066cc;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #003366;
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 5000;
|
||||||
|
}
|
||||||
|
.modal-overlay.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.modal {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 8px 40px rgba(0,0,0,0.3);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.modal h2 {
|
||||||
|
color: #003366;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.modal-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 30px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- PASSWORD OVERLAY -->
|
||||||
|
<div id="passwordOverlay" class="password-overlay">
|
||||||
|
<div class="password-box">
|
||||||
|
<h2>🔐 Roll Calculator Admin</h2>
|
||||||
|
<p style="font-size:13px;color:#666;margin-bottom:20px;">Geben Sie das Admin-Passwort ein</p>
|
||||||
|
<input type="password" id="passwordInput" placeholder="Passwort" autofocus>
|
||||||
|
<button class="btn btn-primary" onclick="checkPassword()">🔓 Anmelden</button>
|
||||||
|
<p style="font-size:11px;color:#999;margin-top:10px;">Standard: admin123</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- EDIT MODAL -->
|
||||||
|
<div id="editModal" class="modal-overlay">
|
||||||
|
<div class="modal">
|
||||||
|
<h2>📝 Artikel bearbeiten</h2>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Artikel-Nr. (nicht änderbar)</label>
|
||||||
|
<input type="text" id="editNr" disabled style="background:#f5f5f5;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Name</label>
|
||||||
|
<input type="text" id="editName">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Produktdicke (mm)</label>
|
||||||
|
<input type="number" id="editThickness" step="0.001" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Area Weight (g/m²)</label>
|
||||||
|
<input type="number" id="editAreaWeight" step="1" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Gewicht pro Rolle (kg)</label>
|
||||||
|
<input type="number" id="editWeightPerRoll" step="0.1" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Breite (m)</label>
|
||||||
|
<input type="number" id="editWidth" step="0.01" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Preis (€)</label>
|
||||||
|
<input type="number" id="editPrice" step="0.01" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Typ</label>
|
||||||
|
<input type="text" id="editType" placeholder="z.B. Geotextil, Kunststoff">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Farbe</label>
|
||||||
|
<input type="text" id="editColor" placeholder="z.B. Schwarz, Weiß">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Beschreibung</label>
|
||||||
|
<textarea id="editDescription" placeholder="Zusätzliche Informationen..."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-buttons">
|
||||||
|
<button class="btn btn-secondary" onclick="closeEditModal()">Abbrechen</button>
|
||||||
|
<button class="btn btn-primary" onclick="saveEditedArticle()">💾 Speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MAIN APP -->
|
||||||
|
<div id="mainApp" style="display:none;">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🛠️ Roll Calculator Admin</h1>
|
||||||
|
<button class="btn btn-secondary" onclick="logout()">Abmelden</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="info-box">
|
||||||
|
💡 Tipp: Alle Änderungen werden im Browser gespeichert. Mit "Download" kannst du die neuen JSON-Dateien herunterladen und auf deinen Server kopieren.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab-btn active" onclick="switchTab('articles')">📄 Artikel-Daten</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('config')">⚙️ Konfiguration</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('tools')">🔧 Tools</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB: ARTICLES -->
|
||||||
|
<div id="articles" class="tab-content active">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Artikel verwalten</h2>
|
||||||
|
<p>Artikel hinzufügen, bearbeiten oder löschen</p>
|
||||||
|
|
||||||
|
<div id="articleAlert" class="alert"></div>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value" id="articleCount">0</div>
|
||||||
|
<div class="stat-label">Artikel</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="font-size:14px;margin-bottom:12px;margin-top:20px;">Neuer Artikel</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Artikel-Nr. *</label>
|
||||||
|
<input type="text" id="artNr" placeholder="z.B. 180305">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Name *</label>
|
||||||
|
<input type="text" id="artName" placeholder="z.B. Bfix NSP 5300">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Produktdicke (mm) *</label>
|
||||||
|
<input type="number" id="artThickness" placeholder="z.B. 6.918" step="0.001" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Area Weight (g/m²)</label>
|
||||||
|
<input type="number" id="artAreaWeight" placeholder="z.B. 5300" step="1" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Gewicht pro Rolle (kg)</label>
|
||||||
|
<input type="number" id="artWeightPerRoll" placeholder="z.B. 25" step="0.1" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Breite (m)</label>
|
||||||
|
<input type="number" id="artWidth" placeholder="z.B. 5" step="0.01" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Preis (€)</label>
|
||||||
|
<input type="number" id="artPrice" placeholder="z.B. 250.00" step="0.01" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Typ</label>
|
||||||
|
<input type="text" id="artType" placeholder="z.B. Geotextil">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Farbe</label>
|
||||||
|
<input type="text" id="artColor" placeholder="z.B. Schwarz">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Beschreibung</label>
|
||||||
|
<textarea id="artDescription" placeholder="Zusätzliche Informationen..."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="btn btn-primary" onclick="addArticle()">➕ Hinzufügen</button>
|
||||||
|
<button class="btn btn-secondary" onclick="clearArticleForm()">Zurücksetzen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="font-size:14px;margin-bottom:12px;margin-top:30px;">Artikel-Liste</h3>
|
||||||
|
<div style="overflow-x:auto;">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Nr.</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Dicke (mm)</th>
|
||||||
|
<th>Breite (m)</th>
|
||||||
|
<th>Preis (€)</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="articleTableBody">
|
||||||
|
<tr><td colspan="6" style="text-align:center;color:#999;">Keine Artikel</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB: CONFIG -->
|
||||||
|
<div id="config" class="tab-content">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Konfiguration (config.json)</h2>
|
||||||
|
<p>Gabelstapler-Regeln und Schwellenwerte - JSON direkt bearbeiten</p>
|
||||||
|
|
||||||
|
<div id="configAlert" class="alert"></div>
|
||||||
|
|
||||||
|
<textarea id="configJson" placeholder="config.json wird hier angezeigt..."></textarea>
|
||||||
|
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="btn btn-primary" onclick="saveConfigJson()">💾 Aktualisieren</button>
|
||||||
|
<button class="btn btn-secondary" onclick="resetConfigJson()">🔄 Zurücksetzen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="font-size:11px;color:#999;margin-top:16px;">
|
||||||
|
💡 JSON wird automatisch validiert. Im Fehlerfall siehst du eine Fehlermeldung.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB: TOOLS -->
|
||||||
|
<div id="tools" class="tab-content">
|
||||||
|
<div class="card">
|
||||||
|
<h2>📥 Dateien verwalten</h2>
|
||||||
|
<p>Herunterladen, hochladen oder zurücksetzen</p>
|
||||||
|
|
||||||
|
<div id="toolsAlert" class="alert"></div>
|
||||||
|
|
||||||
|
<h3 style="font-size:13px;margin-bottom:12px;">Download</h3>
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="btn btn-success" onclick="downloadArticleData()">📥 article-data.json</button>
|
||||||
|
<button class="btn btn-success" onclick="downloadConfig()">📥 config.json</button>
|
||||||
|
<button class="btn btn-success" onclick="downloadAll()">📦 Alle Dateien (ZIP)</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="font-size:13px;margin-bottom:12px;margin-top:20px;">Upload</h3>
|
||||||
|
<p style="font-size:12px;color:#666;margin-bottom:10px;">Dateien hochladen und überschreiben:</p>
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="btn btn-secondary" onclick="document.getElementById('uploadFile').click()">📤 JSON-Datei hochladen</button>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="uploadFile" class="file-input" accept=".json" onchange="handleFileUpload(event)">
|
||||||
|
|
||||||
|
<h3 style="font-size:13px;margin-bottom:12px;margin-top:20px;">Daten</h3>
|
||||||
|
<p style="font-size:12px;color:#666;margin-bottom:10px;">Alle bearbeiteten Daten als ein JSON exportieren:</p>
|
||||||
|
<button class="btn btn-primary" onclick="exportAllAsJson()">💾 Alles exportieren</button>
|
||||||
|
|
||||||
|
<h3 style="font-size:13px;margin-bottom:12px;margin-top:20px;">⚠️ Vorsicht</h3>
|
||||||
|
<p style="font-size:12px;color:#666;margin-bottom:10px;">Bearbeitete Daten zurücksetzen (Achtung: nicht rückgängig!):</p>
|
||||||
|
<button class="btn btn-danger" onclick="if(confirm('Wirklich auf Original zurücksetzen?')) resetAll()">🔄 Auf Standardwerte zurücksetzen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const ADMIN_PASSWORD = 'admin123';
|
||||||
|
|
||||||
|
let articlesData = [];
|
||||||
|
let configData = {};
|
||||||
|
let currentEditingArticle = null;
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadDataFromServer();
|
||||||
|
document.getElementById('passwordInput').addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') checkPassword();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadDataFromServer() {
|
||||||
|
Promise.all([
|
||||||
|
fetch('article-data.json').then(r => r.json()).catch(() => []),
|
||||||
|
fetch('config.json').then(r => r.json()).catch(() => ({}))
|
||||||
|
]).then(([articles, config]) => {
|
||||||
|
articlesData = articles || [];
|
||||||
|
configData = config || {};
|
||||||
|
console.log('✅ Daten geladen:', articlesData.length, 'Artikel');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPassword() {
|
||||||
|
const pwd = document.getElementById('passwordInput').value;
|
||||||
|
if (pwd === ADMIN_PASSWORD) {
|
||||||
|
document.getElementById('passwordOverlay').style.display = 'none';
|
||||||
|
document.getElementById('mainApp').style.display = 'block';
|
||||||
|
refreshUI();
|
||||||
|
} else {
|
||||||
|
alert('❌ Falsches Passwort!');
|
||||||
|
document.getElementById('passwordInput').value = '';
|
||||||
|
document.getElementById('passwordInput').focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
document.getElementById('mainApp').style.display = 'none';
|
||||||
|
document.getElementById('passwordOverlay').style.display = 'flex';
|
||||||
|
document.getElementById('passwordInput').value = '';
|
||||||
|
document.getElementById('passwordInput').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(tabName) {
|
||||||
|
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
document.getElementById(tabName).classList.add('active');
|
||||||
|
event.target.classList.add('active');
|
||||||
|
|
||||||
|
if (tabName === 'config') loadConfigJson();
|
||||||
|
if (tabName === 'articles') refreshArticleTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAlert(containerId, message, type, duration = 4000) {
|
||||||
|
const alert = document.getElementById(containerId);
|
||||||
|
alert.className = `alert show alert-${type}`;
|
||||||
|
alert.textContent = message;
|
||||||
|
if (duration) setTimeout(() => { alert.classList.remove('show'); }, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshUI() {
|
||||||
|
refreshArticleTable();
|
||||||
|
loadConfigJson();
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStats() {
|
||||||
|
document.getElementById('articleCount').textContent = articlesData.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditModal(nr) {
|
||||||
|
const article = articlesData.find(a => a.nr === nr);
|
||||||
|
if (!article) return;
|
||||||
|
|
||||||
|
currentEditingArticle = article;
|
||||||
|
|
||||||
|
document.getElementById('editNr').value = article.nr || '';
|
||||||
|
document.getElementById('editName').value = article.name || '';
|
||||||
|
document.getElementById('editThickness').value = article.thickness || '';
|
||||||
|
document.getElementById('editAreaWeight').value = article.area_weight || '';
|
||||||
|
document.getElementById('editWeightPerRoll').value = article.weight_per_roll || '';
|
||||||
|
document.getElementById('editWidth').value = article.width || '';
|
||||||
|
document.getElementById('editPrice').value = article.price || '';
|
||||||
|
document.getElementById('editType').value = article.type || '';
|
||||||
|
document.getElementById('editColor').value = article.color || '';
|
||||||
|
document.getElementById('editDescription').value = article.description || '';
|
||||||
|
|
||||||
|
document.getElementById('editModal').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeEditModal() {
|
||||||
|
document.getElementById('editModal').classList.remove('active');
|
||||||
|
currentEditingArticle = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveEditedArticle() {
|
||||||
|
if (!currentEditingArticle) return;
|
||||||
|
|
||||||
|
currentEditingArticle.name = document.getElementById('editName').value.trim();
|
||||||
|
currentEditingArticle.thickness = parseFloat(document.getElementById('editThickness').value) || 0;
|
||||||
|
currentEditingArticle.area_weight = parseFloat(document.getElementById('editAreaWeight').value) || 0;
|
||||||
|
currentEditingArticle.weight_per_roll = parseFloat(document.getElementById('editWeightPerRoll').value) || 0;
|
||||||
|
currentEditingArticle.width = parseFloat(document.getElementById('editWidth').value) || 0;
|
||||||
|
currentEditingArticle.price = parseFloat(document.getElementById('editPrice').value) || 0;
|
||||||
|
currentEditingArticle.type = document.getElementById('editType').value.trim();
|
||||||
|
currentEditingArticle.color = document.getElementById('editColor').value.trim();
|
||||||
|
currentEditingArticle.description = document.getElementById('editDescription').value.trim();
|
||||||
|
|
||||||
|
closeEditModal();
|
||||||
|
showAlert('articleAlert', '✅ Artikel aktualisiert!', 'success');
|
||||||
|
refreshArticleTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addArticle() {
|
||||||
|
const nr = document.getElementById('artNr').value.trim();
|
||||||
|
const name = document.getElementById('artName').value.trim();
|
||||||
|
const thickness = parseFloat(document.getElementById('artThickness').value) || 0;
|
||||||
|
|
||||||
|
if (!nr || !name || thickness === 0) {
|
||||||
|
showAlert('articleAlert', '❌ Artikel-Nr., Name und Dicke sind erforderlich!', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (articlesData.some(a => a.nr === nr)) {
|
||||||
|
showAlert('articleAlert', '❌ Artikel-Nr. existiert bereits!', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newArticle = {
|
||||||
|
nr,
|
||||||
|
name,
|
||||||
|
thickness,
|
||||||
|
area_weight: parseFloat(document.getElementById('artAreaWeight').value) || 0,
|
||||||
|
weight_per_roll: parseFloat(document.getElementById('artWeightPerRoll').value) || 0,
|
||||||
|
width: parseFloat(document.getElementById('artWidth').value) || 0,
|
||||||
|
price: parseFloat(document.getElementById('artPrice').value) || 0,
|
||||||
|
type: document.getElementById('artType').value.trim(),
|
||||||
|
color: document.getElementById('artColor').value.trim(),
|
||||||
|
description: document.getElementById('artDescription').value.trim()
|
||||||
|
};
|
||||||
|
|
||||||
|
articlesData.push(newArticle);
|
||||||
|
showAlert('articleAlert', '✅ Artikel hinzugefügt!', 'success');
|
||||||
|
clearArticleForm();
|
||||||
|
refreshArticleTable();
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteArticle(nr) {
|
||||||
|
if (confirm('Artikel ' + nr + ' wirklich löschen?')) {
|
||||||
|
articlesData = articlesData.filter(a => a.nr !== nr);
|
||||||
|
showAlert('articleAlert', '✅ Artikel gelöscht!', 'success');
|
||||||
|
refreshArticleTable();
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshArticleTable() {
|
||||||
|
const tbody = document.getElementById('articleTableBody');
|
||||||
|
if (articlesData.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:#999;">Keine Artikel</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = articlesData.map(a => `
|
||||||
|
<tr>
|
||||||
|
<td><strong>${a.nr}</strong></td>
|
||||||
|
<td>${a.name}</td>
|
||||||
|
<td>${a.thickness.toFixed(3)}</td>
|
||||||
|
<td>${a.width ? a.width.toFixed(2) : '—'}</td>
|
||||||
|
<td>${a.price ? a.price.toFixed(2) + ' €' : '—'}</td>
|
||||||
|
<td>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button class="edit-btn" onclick="openEditModal('${a.nr}')">✎ Bearbeiten</button>
|
||||||
|
<button class="delete-btn" onclick="deleteArticle('${a.nr}')">✕ Löschen</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearArticleForm() {
|
||||||
|
document.getElementById('artNr').value = '';
|
||||||
|
document.getElementById('artName').value = '';
|
||||||
|
document.getElementById('artThickness').value = '';
|
||||||
|
document.getElementById('artAreaWeight').value = '';
|
||||||
|
document.getElementById('artWeightPerRoll').value = '';
|
||||||
|
document.getElementById('artWidth').value = '';
|
||||||
|
document.getElementById('artPrice').value = '';
|
||||||
|
document.getElementById('artType').value = '';
|
||||||
|
document.getElementById('artColor').value = '';
|
||||||
|
document.getElementById('artDescription').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadConfigJson() {
|
||||||
|
document.getElementById('configJson').value = JSON.stringify(configData, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveConfigJson() {
|
||||||
|
try {
|
||||||
|
configData = JSON.parse(document.getElementById('configJson').value);
|
||||||
|
showAlert('configAlert', '✅ Konfiguration aktualisiert!', 'success');
|
||||||
|
} catch (e) {
|
||||||
|
showAlert('configAlert', '❌ Ungültiges JSON: ' + e.message, 'error', 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetConfigJson() {
|
||||||
|
loadConfigJson();
|
||||||
|
showAlert('configAlert', '🔄 Zurückgesetzt', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadFile(data, filename) {
|
||||||
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadArticleData() {
|
||||||
|
downloadFile(articlesData, 'article-data.json');
|
||||||
|
showAlert('toolsAlert', '✅ article-data.json heruntergeladen', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadConfig() {
|
||||||
|
downloadFile(configData, 'config.json');
|
||||||
|
showAlert('toolsAlert', '✅ config.json heruntergeladen', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadAll() {
|
||||||
|
const allData = { articles: articlesData, config: configData, exported_at: new Date().toISOString() };
|
||||||
|
downloadFile(allData, 'roll-calculator-backup-' + new Date().toISOString().slice(0,10) + '.json');
|
||||||
|
showAlert('toolsAlert', '✅ Alle Daten heruntergeladen', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportAllAsJson() {
|
||||||
|
downloadAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileUpload(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.target.result);
|
||||||
|
if (data.articles) articlesData = data.articles;
|
||||||
|
if (data.config) configData = data.config;
|
||||||
|
if (data.articleData) articlesData = data.articleData;
|
||||||
|
|
||||||
|
showAlert('toolsAlert', '✅ Datei hochgeladen und geladen!', 'success');
|
||||||
|
refreshUI();
|
||||||
|
} catch (err) {
|
||||||
|
showAlert('toolsAlert', '❌ Fehler beim Laden der Datei: ' + err.message, 'error', 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
event.target.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetAll() {
|
||||||
|
articlesData = [];
|
||||||
|
configData = {};
|
||||||
|
showAlert('toolsAlert', '🔄 Auf Standardwerte zurückgesetzt', 'success');
|
||||||
|
refreshUI();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,694 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Roll Calculator Admin - Einfach</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Arial, sans-serif;
|
||||||
|
background: #f4f6f9;
|
||||||
|
color: #333;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #003366 0%, #0066cc 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px 30px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.header h1 { font-size: 20px; }
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 30px auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
background: #fffbea;
|
||||||
|
border-left: 4px solid #f0b429;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 2px solid #e0e8f0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.tab-btn {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.tab-btn.active {
|
||||||
|
color: #0066cc;
|
||||||
|
border-bottom-color: #0066cc;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content { display: none; }
|
||||||
|
.tab-content.active { display: block; }
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #003366;
|
||||||
|
}
|
||||||
|
.card > p {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.form-group label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
}
|
||||||
|
.form-group input, .form-group textarea {
|
||||||
|
padding: 9px 11px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: 'Segoe UI', Arial, sans-serif;
|
||||||
|
transition: border 0.2s;
|
||||||
|
}
|
||||||
|
.form-group textarea {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
.form-group input:focus, .form-group textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #0066cc;
|
||||||
|
box-shadow: 0 0 0 3px rgba(0,102,204,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: #0066cc;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-primary:hover { background: #0052a3; box-shadow: 0 2px 6px rgba(0,102,204,0.3); }
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #e0e0e0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.btn-secondary:hover { background: #d0d0d0; }
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-danger:hover { background: #c82333; }
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-success:hover { background: #218838; }
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
table th {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 2px solid #0066cc;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
table td {
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
table tr:hover { background: #f9f9f9; }
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.alert.show { display: block; }
|
||||||
|
.alert-success {
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
.alert-error {
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
.alert-info {
|
||||||
|
background: #d1ecf1;
|
||||||
|
border: 1px solid #bee5eb;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.action-buttons button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.edit-btn {
|
||||||
|
background: #0066cc;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.edit-btn:hover { background: #0052a3; }
|
||||||
|
.delete-btn {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.delete-btn:hover { background: #c82333; }
|
||||||
|
|
||||||
|
.password-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
.password-box {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 32px;
|
||||||
|
max-width: 400px;
|
||||||
|
box-shadow: 0 8px 40px rgba(0,0,0,0.3);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.password-box h2 { margin-bottom: 20px; color: #003366; }
|
||||||
|
.password-box input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 11px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.password-box .btn { width: 100%; margin-bottom: 10px; }
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.stat {
|
||||||
|
background: linear-gradient(135deg, #e8f4fd 0%, #d0e8fa 100%);
|
||||||
|
border: 1px solid #0066cc;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #003366;
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- PASSWORD OVERLAY -->
|
||||||
|
<div id="passwordOverlay" class="password-overlay">
|
||||||
|
<div class="password-box">
|
||||||
|
<h2>🔐 Roll Calculator Admin</h2>
|
||||||
|
<p style="font-size:13px;color:#666;margin-bottom:20px;">Geben Sie das Admin-Passwort ein</p>
|
||||||
|
<input type="password" id="passwordInput" placeholder="Passwort" autofocus>
|
||||||
|
<button class="btn btn-primary" onclick="checkPassword()">🔓 Anmelden</button>
|
||||||
|
<p style="font-size:11px;color:#999;margin-top:10px;">Standard: admin123</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MAIN APP -->
|
||||||
|
<div id="mainApp" style="display:none;">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🛠️ Roll Calculator Admin</h1>
|
||||||
|
<button class="btn btn-secondary" onclick="logout()">Abmelden</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="info-box">
|
||||||
|
💡 Tipp: Alle Änderungen werden im Browser gespeichert. Mit "Download" kannst du die neuen JSON-Dateien herunterladen und auf deinen Server kopieren.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab-btn active" onclick="switchTab('articles')">📄 Artikel-Daten</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('config')">⚙️ Konfiguration</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('tools')">🔧 Tools</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB: ARTICLES -->
|
||||||
|
<div id="articles" class="tab-content active">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Artikel verwalten</h2>
|
||||||
|
<p>Artikel hinzufügen, bearbeiten oder löschen</p>
|
||||||
|
|
||||||
|
<div id="articleAlert" class="alert"></div>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value" id="articleCount">0</div>
|
||||||
|
<div class="stat-label">Artikel</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="font-size:14px;margin-bottom:12px;margin-top:20px;">Neuer Artikel</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Artikel-Nr.</label>
|
||||||
|
<input type="text" id="artNr" placeholder="z.B. 180305">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Name</label>
|
||||||
|
<input type="text" id="artName" placeholder="z.B. Bfix NSP 5300, 5,00 x 40 m">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Produktdicke (mm)</label>
|
||||||
|
<input type="number" id="artThickness" placeholder="z.B. 6.918" step="0.001" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Area Weight (g/m²) - Optional</label>
|
||||||
|
<input type="number" id="artAreaWeight" placeholder="z.B. 5300" step="1" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="btn btn-primary" onclick="addArticle()">➕ Hinzufügen</button>
|
||||||
|
<button class="btn btn-secondary" onclick="clearArticleForm()">Zurücksetzen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="font-size:14px;margin-bottom:12px;margin-top:30px;">Artikel-Liste</h3>
|
||||||
|
<div style="overflow-x:auto;">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Nr.</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Dicke (mm)</th>
|
||||||
|
<th>Area Weight</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="articleTableBody">
|
||||||
|
<tr><td colspan="5" style="text-align:center;color:#999;">Keine Artikel</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB: CONFIG -->
|
||||||
|
<div id="config" class="tab-content">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Konfiguration (config.json)</h2>
|
||||||
|
<p>Gabelstapler-Regeln und Schwellenwerte - JSON direkt bearbeiten</p>
|
||||||
|
|
||||||
|
<div id="configAlert" class="alert"></div>
|
||||||
|
|
||||||
|
<textarea id="configJson" placeholder="config.json wird hier angezeigt..."></textarea>
|
||||||
|
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="btn btn-primary" onclick="saveConfigJson()">💾 Aktualisieren</button>
|
||||||
|
<button class="btn btn-secondary" onclick="resetConfigJson()">🔄 Zurücksetzen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="font-size:11px;color:#999;margin-top:16px;">
|
||||||
|
💡 JSON wird automatisch validiert. Im Fehlerfall siehst du eine Fehlermeldung.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB: TOOLS -->
|
||||||
|
<div id="tools" class="tab-content">
|
||||||
|
<div class="card">
|
||||||
|
<h2>📥 Dateien verwalten</h2>
|
||||||
|
<p>Herunterladen, hochladen oder zurücksetzen</p>
|
||||||
|
|
||||||
|
<div id="toolsAlert" class="alert"></div>
|
||||||
|
|
||||||
|
<h3 style="font-size:13px;margin-bottom:12px;">Download</h3>
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="btn btn-success" onclick="downloadArticleData()">📥 article-data.json</button>
|
||||||
|
<button class="btn btn-success" onclick="downloadConfig()">📥 config.json</button>
|
||||||
|
<button class="btn btn-success" onclick="downloadAll()">📦 Alle Dateien (ZIP)</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="font-size:13px;margin-bottom:12px;margin-top:20px;">Upload</h3>
|
||||||
|
<p style="font-size:12px;color:#666;margin-bottom:10px;">Dateien hochladen und überschreiben:</p>
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="btn btn-secondary" onclick="document.getElementById('uploadFile').click()">📤 JSON-Datei hochladen</button>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="uploadFile" class="file-input" accept=".json" onchange="handleFileUpload(event)">
|
||||||
|
|
||||||
|
<h3 style="font-size:13px;margin-bottom:12px;margin-top:20px;">Daten</h3>
|
||||||
|
<p style="font-size:12px;color:#666;margin-bottom:10px;">Alle bearbeiteten Daten als ein JSON exportieren:</p>
|
||||||
|
<button class="btn btn-primary" onclick="exportAllAsJson()">💾 Alles exportieren</button>
|
||||||
|
|
||||||
|
<h3 style="font-size:13px;margin-bottom:12px;margin-top:20px;">⚠️ Vorsicht</h3>
|
||||||
|
<p style="font-size:12px;color:#666;margin-bottom:10px;">Bearbeitete Daten zurücksetzen (Achtung: nicht rückgängig!):</p>
|
||||||
|
<button class="btn btn-danger" onclick="if(confirm('Wirklich auf Original zurücksetzen?')) resetAll()">🔄 Auf Standardwerte zurücksetzen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
// CONFIG
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
const ADMIN_PASSWORD = 'admin123'; // ÄNDERN!
|
||||||
|
|
||||||
|
// Beispiel article-data.json (wird von Server geladen)
|
||||||
|
let articlesData = [];
|
||||||
|
|
||||||
|
// Beispiel config.json (wird von Server geladen)
|
||||||
|
let configData = {};
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
// INIT
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadDataFromServer();
|
||||||
|
document.getElementById('passwordInput').addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') checkPassword();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadDataFromServer() {
|
||||||
|
// Versuche, Daten vom Server zu laden
|
||||||
|
Promise.all([
|
||||||
|
fetch('article-data.json').then(r => r.json()).catch(() => []),
|
||||||
|
fetch('config.json').then(r => r.json()).catch(() => ({}))
|
||||||
|
]).then(([articles, config]) => {
|
||||||
|
articlesData = articles || [];
|
||||||
|
configData = config || {};
|
||||||
|
console.log('✅ Daten geladen:', articlesData.length, 'Artikel, config vorhanden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
// AUTH
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
function checkPassword() {
|
||||||
|
const pwd = document.getElementById('passwordInput').value;
|
||||||
|
if (pwd === ADMIN_PASSWORD) {
|
||||||
|
document.getElementById('passwordOverlay').style.display = 'none';
|
||||||
|
document.getElementById('mainApp').style.display = 'block';
|
||||||
|
refreshUI();
|
||||||
|
} else {
|
||||||
|
alert('❌ Falsches Passwort!');
|
||||||
|
document.getElementById('passwordInput').value = '';
|
||||||
|
document.getElementById('passwordInput').focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
document.getElementById('mainApp').style.display = 'none';
|
||||||
|
document.getElementById('passwordOverlay').style.display = 'flex';
|
||||||
|
document.getElementById('passwordInput').value = '';
|
||||||
|
document.getElementById('passwordInput').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
// UI
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
function switchTab(tabName) {
|
||||||
|
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
document.getElementById(tabName).classList.add('active');
|
||||||
|
event.target.classList.add('active');
|
||||||
|
|
||||||
|
if (tabName === 'config') loadConfigJson();
|
||||||
|
if (tabName === 'articles') refreshArticleTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAlert(containerId, message, type, duration = 4000) {
|
||||||
|
const alert = document.getElementById(containerId);
|
||||||
|
alert.className = `alert show alert-${type}`;
|
||||||
|
alert.textContent = message;
|
||||||
|
if (duration) setTimeout(() => { alert.classList.remove('show'); }, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshUI() {
|
||||||
|
refreshArticleTable();
|
||||||
|
loadConfigJson();
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStats() {
|
||||||
|
document.getElementById('articleCount').textContent = articlesData.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
// ARTICLES
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
function addArticle() {
|
||||||
|
const nr = document.getElementById('artNr').value.trim();
|
||||||
|
const name = document.getElementById('artName').value.trim();
|
||||||
|
const thickness = parseFloat(document.getElementById('artThickness').value) || 0;
|
||||||
|
const area_weight = parseFloat(document.getElementById('artAreaWeight').value) || 0;
|
||||||
|
|
||||||
|
if (!nr || !name || thickness === 0) {
|
||||||
|
showAlert('articleAlert', '❌ Artikel-Nr., Name und Dicke sind erforderlich!', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (articlesData.some(a => a.nr === nr)) {
|
||||||
|
showAlert('articleAlert', '❌ Artikel-Nr. existiert bereits!', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
articlesData.push({ nr, name, thickness, area_weight });
|
||||||
|
showAlert('articleAlert', '✅ Artikel hinzugefügt!', 'success');
|
||||||
|
clearArticleForm();
|
||||||
|
refreshArticleTable();
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteArticle(nr) {
|
||||||
|
if (confirm('Artikel ' + nr + ' wirklich löschen?')) {
|
||||||
|
articlesData = articlesData.filter(a => a.nr !== nr);
|
||||||
|
showAlert('articleAlert', '✅ Artikel gelöscht!', 'success');
|
||||||
|
refreshArticleTable();
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshArticleTable() {
|
||||||
|
const tbody = document.getElementById('articleTableBody');
|
||||||
|
if (articlesData.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:#999;">Keine Artikel</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = articlesData.map(a => `
|
||||||
|
<tr>
|
||||||
|
<td><strong>${a.nr}</strong></td>
|
||||||
|
<td>${a.name}</td>
|
||||||
|
<td>${a.thickness.toFixed(3)}</td>
|
||||||
|
<td>${a.area_weight ? a.area_weight : '—'}</td>
|
||||||
|
<td>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button class="edit-btn" onclick="editArticle('${a.nr}')">✎ Bearbeiten</button>
|
||||||
|
<button class="delete-btn" onclick="deleteArticle('${a.nr}')">✕ Löschen</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function editArticle(nr) {
|
||||||
|
const article = articlesData.find(a => a.nr === nr);
|
||||||
|
if (!article) return;
|
||||||
|
|
||||||
|
const newThickness = prompt('Produktdicke (mm):', article.thickness);
|
||||||
|
if (newThickness !== null) {
|
||||||
|
article.thickness = parseFloat(newThickness) || 0;
|
||||||
|
showAlert('articleAlert', '✅ Aktualisiert!', 'success');
|
||||||
|
refreshArticleTable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearArticleForm() {
|
||||||
|
document.getElementById('artNr').value = '';
|
||||||
|
document.getElementById('artName').value = '';
|
||||||
|
document.getElementById('artThickness').value = '';
|
||||||
|
document.getElementById('artAreaWeight').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
// CONFIG
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
function loadConfigJson() {
|
||||||
|
document.getElementById('configJson').value = JSON.stringify(configData, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveConfigJson() {
|
||||||
|
try {
|
||||||
|
configData = JSON.parse(document.getElementById('configJson').value);
|
||||||
|
showAlert('configAlert', '✅ Konfiguration aktualisiert!', 'success');
|
||||||
|
} catch (e) {
|
||||||
|
showAlert('configAlert', '❌ Ungültiges JSON: ' + e.message, 'error', 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetConfigJson() {
|
||||||
|
loadConfigJson();
|
||||||
|
showAlert('configAlert', '🔄 Zurückgesetzt', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
// DOWNLOADS / UPLOADS
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
function downloadFile(data, filename) {
|
||||||
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadArticleData() {
|
||||||
|
downloadFile(articlesData, 'article-data.json');
|
||||||
|
showAlert('toolsAlert', '✅ article-data.json heruntergeladen', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadConfig() {
|
||||||
|
downloadFile(configData, 'config.json');
|
||||||
|
showAlert('toolsAlert', '✅ config.json heruntergeladen', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadAll() {
|
||||||
|
const allData = { articles: articlesData, config: configData, exported_at: new Date().toISOString() };
|
||||||
|
downloadFile(allData, 'roll-calculator-backup-' + new Date().toISOString().slice(0,10) + '.json');
|
||||||
|
showAlert('toolsAlert', '✅ Alle Daten heruntergeladen', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportAllAsJson() {
|
||||||
|
downloadAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileUpload(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.target.result);
|
||||||
|
if (data.articles) articlesData = data.articles;
|
||||||
|
if (data.config) configData = data.config;
|
||||||
|
if (data.articleData) articlesData = data.articleData; // alternate key
|
||||||
|
|
||||||
|
showAlert('toolsAlert', '✅ Datei hochgeladen und geladen!', 'success');
|
||||||
|
refreshUI();
|
||||||
|
} catch (err) {
|
||||||
|
showAlert('toolsAlert', '❌ Fehler beim Laden der Datei: ' + err.message, 'error', 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
event.target.value = ''; // Reset file input
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetAll() {
|
||||||
|
articlesData = [];
|
||||||
|
configData = {};
|
||||||
|
showAlert('toolsAlert', '🔄 Auf Standardwerte zurückgesetzt', 'success');
|
||||||
|
refreshUI();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,694 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Roll Calculator Admin - Einfach</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Arial, sans-serif;
|
||||||
|
background: #f4f6f9;
|
||||||
|
color: #333;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #003366 0%, #0066cc 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px 30px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.header h1 { font-size: 20px; }
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 30px auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
background: #fffbea;
|
||||||
|
border-left: 4px solid #f0b429;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 2px solid #e0e8f0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.tab-btn {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #666;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.tab-btn.active {
|
||||||
|
color: #0066cc;
|
||||||
|
border-bottom-color: #0066cc;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content { display: none; }
|
||||||
|
.tab-content.active { display: block; }
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
|
||||||
|
padding: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #003366;
|
||||||
|
}
|
||||||
|
.card > p {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.form-group label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
}
|
||||||
|
.form-group input, .form-group textarea {
|
||||||
|
padding: 9px 11px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: 'Segoe UI', Arial, sans-serif;
|
||||||
|
transition: border 0.2s;
|
||||||
|
}
|
||||||
|
.form-group textarea {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
.form-group input:focus, .form-group textarea:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #0066cc;
|
||||||
|
box-shadow: 0 0 0 3px rgba(0,102,204,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: #0066cc;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-primary:hover { background: #0052a3; box-shadow: 0 2px 6px rgba(0,102,204,0.3); }
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #e0e0e0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.btn-secondary:hover { background: #d0d0d0; }
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-danger:hover { background: #c82333; }
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-success:hover { background: #218838; }
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
table th {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
border-bottom: 2px solid #0066cc;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
table td {
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
table tr:hover { background: #f9f9f9; }
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.alert.show { display: block; }
|
||||||
|
.alert-success {
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
.alert-error {
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
.alert-info {
|
||||||
|
background: #d1ecf1;
|
||||||
|
border: 1px solid #bee5eb;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.action-buttons button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.edit-btn {
|
||||||
|
background: #0066cc;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.edit-btn:hover { background: #0052a3; }
|
||||||
|
.delete-btn {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.delete-btn:hover { background: #c82333; }
|
||||||
|
|
||||||
|
.password-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0,0,0,0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
.password-box {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 32px;
|
||||||
|
max-width: 400px;
|
||||||
|
box-shadow: 0 8px 40px rgba(0,0,0,0.3);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.password-box h2 { margin-bottom: 20px; color: #003366; }
|
||||||
|
.password-box input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 11px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.password-box .btn { width: 100%; margin-bottom: 10px; }
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.stat {
|
||||||
|
background: linear-gradient(135deg, #e8f4fd 0%, #d0e8fa 100%);
|
||||||
|
border: 1px solid #0066cc;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #003366;
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- PASSWORD OVERLAY -->
|
||||||
|
<div id="passwordOverlay" class="password-overlay">
|
||||||
|
<div class="password-box">
|
||||||
|
<h2>🔐 Roll Calculator Admin</h2>
|
||||||
|
<p style="font-size:13px;color:#666;margin-bottom:20px;">Geben Sie das Admin-Passwort ein</p>
|
||||||
|
<input type="password" id="passwordInput" placeholder="Passwort" autofocus>
|
||||||
|
<button class="btn btn-primary" onclick="checkPassword()">🔓 Anmelden</button>
|
||||||
|
<p style="font-size:11px;color:#999;margin-top:10px;">Standard: admin123</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MAIN APP -->
|
||||||
|
<div id="mainApp" style="display:none;">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🛠️ Roll Calculator Admin</h1>
|
||||||
|
<button class="btn btn-secondary" onclick="logout()">Abmelden</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="info-box">
|
||||||
|
💡 Tipp: Alle Änderungen werden im Browser gespeichert. Mit "Download" kannst du die neuen JSON-Dateien herunterladen und auf deinen Server kopieren.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab-btn active" onclick="switchTab('articles')">📄 Artikel-Daten</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('config')">⚙️ Konfiguration</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('tools')">🔧 Tools</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB: ARTICLES -->
|
||||||
|
<div id="articles" class="tab-content active">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Artikel verwalten</h2>
|
||||||
|
<p>Artikel hinzufügen, bearbeiten oder löschen</p>
|
||||||
|
|
||||||
|
<div id="articleAlert" class="alert"></div>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-value" id="articleCount">0</div>
|
||||||
|
<div class="stat-label">Artikel</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="font-size:14px;margin-bottom:12px;margin-top:20px;">Neuer Artikel</h3>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Artikel-Nr.</label>
|
||||||
|
<input type="text" id="artNr" placeholder="z.B. 180305">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Name</label>
|
||||||
|
<input type="text" id="artName" placeholder="z.B. Bfix NSP 5300, 5,00 x 40 m">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Produktdicke (mm)</label>
|
||||||
|
<input type="number" id="artThickness" placeholder="z.B. 6.918" step="0.001" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Area Weight (g/m²) - Optional</label>
|
||||||
|
<input type="number" id="artAreaWeight" placeholder="z.B. 5300" step="1" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="btn btn-primary" onclick="addArticle()">➕ Hinzufügen</button>
|
||||||
|
<button class="btn btn-secondary" onclick="clearArticleForm()">Zurücksetzen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="font-size:14px;margin-bottom:12px;margin-top:30px;">Artikel-Liste</h3>
|
||||||
|
<div style="overflow-x:auto;">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Nr.</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Dicke (mm)</th>
|
||||||
|
<th>Area Weight</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="articleTableBody">
|
||||||
|
<tr><td colspan="5" style="text-align:center;color:#999;">Keine Artikel</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB: CONFIG -->
|
||||||
|
<div id="config" class="tab-content">
|
||||||
|
<div class="card">
|
||||||
|
<h2>Konfiguration (config.json)</h2>
|
||||||
|
<p>Gabelstapler-Regeln und Schwellenwerte - JSON direkt bearbeiten</p>
|
||||||
|
|
||||||
|
<div id="configAlert" class="alert"></div>
|
||||||
|
|
||||||
|
<textarea id="configJson" placeholder="config.json wird hier angezeigt..."></textarea>
|
||||||
|
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="btn btn-primary" onclick="saveConfigJson()">💾 Aktualisieren</button>
|
||||||
|
<button class="btn btn-secondary" onclick="resetConfigJson()">🔄 Zurücksetzen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="font-size:11px;color:#999;margin-top:16px;">
|
||||||
|
💡 JSON wird automatisch validiert. Im Fehlerfall siehst du eine Fehlermeldung.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB: TOOLS -->
|
||||||
|
<div id="tools" class="tab-content">
|
||||||
|
<div class="card">
|
||||||
|
<h2>📥 Dateien verwalten</h2>
|
||||||
|
<p>Herunterladen, hochladen oder zurücksetzen</p>
|
||||||
|
|
||||||
|
<div id="toolsAlert" class="alert"></div>
|
||||||
|
|
||||||
|
<h3 style="font-size:13px;margin-bottom:12px;">Download</h3>
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="btn btn-success" onclick="downloadArticleData()">📥 article-data.json</button>
|
||||||
|
<button class="btn btn-success" onclick="downloadConfig()">📥 config.json</button>
|
||||||
|
<button class="btn btn-success" onclick="downloadAll()">📦 Alle Dateien (ZIP)</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="font-size:13px;margin-bottom:12px;margin-top:20px;">Upload</h3>
|
||||||
|
<p style="font-size:12px;color:#666;margin-bottom:10px;">Dateien hochladen und überschreiben:</p>
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="btn btn-secondary" onclick="document.getElementById('uploadFile').click()">📤 JSON-Datei hochladen</button>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="uploadFile" class="file-input" accept=".json" onchange="handleFileUpload(event)">
|
||||||
|
|
||||||
|
<h3 style="font-size:13px;margin-bottom:12px;margin-top:20px;">Daten</h3>
|
||||||
|
<p style="font-size:12px;color:#666;margin-bottom:10px;">Alle bearbeiteten Daten als ein JSON exportieren:</p>
|
||||||
|
<button class="btn btn-primary" onclick="exportAllAsJson()">💾 Alles exportieren</button>
|
||||||
|
|
||||||
|
<h3 style="font-size:13px;margin-bottom:12px;margin-top:20px;">⚠️ Vorsicht</h3>
|
||||||
|
<p style="font-size:12px;color:#666;margin-bottom:10px;">Bearbeitete Daten zurücksetzen (Achtung: nicht rückgängig!):</p>
|
||||||
|
<button class="btn btn-danger" onclick="if(confirm('Wirklich auf Original zurücksetzen?')) resetAll()">🔄 Auf Standardwerte zurücksetzen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
// CONFIG
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
const ADMIN_PASSWORD = 'admin123'; // ÄNDERN!
|
||||||
|
|
||||||
|
// Beispiel article-data.json (wird von Server geladen)
|
||||||
|
let articlesData = [];
|
||||||
|
|
||||||
|
// Beispiel config.json (wird von Server geladen)
|
||||||
|
let configData = {};
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
// INIT
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadDataFromServer();
|
||||||
|
document.getElementById('passwordInput').addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') checkPassword();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadDataFromServer() {
|
||||||
|
// Versuche, Daten vom Server zu laden
|
||||||
|
Promise.all([
|
||||||
|
fetch('article-data.json').then(r => r.json()).catch(() => []),
|
||||||
|
fetch('config.json').then(r => r.json()).catch(() => ({}))
|
||||||
|
]).then(([articles, config]) => {
|
||||||
|
articlesData = articles || [];
|
||||||
|
configData = config || {};
|
||||||
|
console.log('✅ Daten geladen:', articlesData.length, 'Artikel, config vorhanden');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
// AUTH
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
function checkPassword() {
|
||||||
|
const pwd = document.getElementById('passwordInput').value;
|
||||||
|
if (pwd === ADMIN_PASSWORD) {
|
||||||
|
document.getElementById('passwordOverlay').style.display = 'none';
|
||||||
|
document.getElementById('mainApp').style.display = 'block';
|
||||||
|
refreshUI();
|
||||||
|
} else {
|
||||||
|
alert('❌ Falsches Passwort!');
|
||||||
|
document.getElementById('passwordInput').value = '';
|
||||||
|
document.getElementById('passwordInput').focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
document.getElementById('mainApp').style.display = 'none';
|
||||||
|
document.getElementById('passwordOverlay').style.display = 'flex';
|
||||||
|
document.getElementById('passwordInput').value = '';
|
||||||
|
document.getElementById('passwordInput').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
// UI
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
function switchTab(tabName) {
|
||||||
|
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
document.getElementById(tabName).classList.add('active');
|
||||||
|
event.target.classList.add('active');
|
||||||
|
|
||||||
|
if (tabName === 'config') loadConfigJson();
|
||||||
|
if (tabName === 'articles') refreshArticleTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAlert(containerId, message, type, duration = 4000) {
|
||||||
|
const alert = document.getElementById(containerId);
|
||||||
|
alert.className = `alert show alert-${type}`;
|
||||||
|
alert.textContent = message;
|
||||||
|
if (duration) setTimeout(() => { alert.classList.remove('show'); }, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshUI() {
|
||||||
|
refreshArticleTable();
|
||||||
|
loadConfigJson();
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStats() {
|
||||||
|
document.getElementById('articleCount').textContent = articlesData.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
// ARTICLES
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
function addArticle() {
|
||||||
|
const nr = document.getElementById('artNr').value.trim();
|
||||||
|
const name = document.getElementById('artName').value.trim();
|
||||||
|
const thickness = parseFloat(document.getElementById('artThickness').value) || 0;
|
||||||
|
const area_weight = parseFloat(document.getElementById('artAreaWeight').value) || 0;
|
||||||
|
|
||||||
|
if (!nr || !name || thickness === 0) {
|
||||||
|
showAlert('articleAlert', '❌ Artikel-Nr., Name und Dicke sind erforderlich!', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (articlesData.some(a => a.nr === nr)) {
|
||||||
|
showAlert('articleAlert', '❌ Artikel-Nr. existiert bereits!', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
articlesData.push({ nr, name, thickness, area_weight });
|
||||||
|
showAlert('articleAlert', '✅ Artikel hinzugefügt!', 'success');
|
||||||
|
clearArticleForm();
|
||||||
|
refreshArticleTable();
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteArticle(nr) {
|
||||||
|
if (confirm('Artikel ' + nr + ' wirklich löschen?')) {
|
||||||
|
articlesData = articlesData.filter(a => a.nr !== nr);
|
||||||
|
showAlert('articleAlert', '✅ Artikel gelöscht!', 'success');
|
||||||
|
refreshArticleTable();
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshArticleTable() {
|
||||||
|
const tbody = document.getElementById('articleTableBody');
|
||||||
|
if (articlesData.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:#999;">Keine Artikel</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = articlesData.map(a => `
|
||||||
|
<tr>
|
||||||
|
<td><strong>${a.nr}</strong></td>
|
||||||
|
<td>${a.name}</td>
|
||||||
|
<td>${a.thickness.toFixed(3)}</td>
|
||||||
|
<td>${a.area_weight ? a.area_weight : '—'}</td>
|
||||||
|
<td>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button class="edit-btn" onclick="editArticle('${a.nr}')">✎ Bearbeiten</button>
|
||||||
|
<button class="delete-btn" onclick="deleteArticle('${a.nr}')">✕ Löschen</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function editArticle(nr) {
|
||||||
|
const article = articlesData.find(a => a.nr === nr);
|
||||||
|
if (!article) return;
|
||||||
|
|
||||||
|
const newThickness = prompt('Produktdicke (mm):', article.thickness);
|
||||||
|
if (newThickness !== null) {
|
||||||
|
article.thickness = parseFloat(newThickness) || 0;
|
||||||
|
showAlert('articleAlert', '✅ Aktualisiert!', 'success');
|
||||||
|
refreshArticleTable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearArticleForm() {
|
||||||
|
document.getElementById('artNr').value = '';
|
||||||
|
document.getElementById('artName').value = '';
|
||||||
|
document.getElementById('artThickness').value = '';
|
||||||
|
document.getElementById('artAreaWeight').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
// CONFIG
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
function loadConfigJson() {
|
||||||
|
document.getElementById('configJson').value = JSON.stringify(configData, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveConfigJson() {
|
||||||
|
try {
|
||||||
|
configData = JSON.parse(document.getElementById('configJson').value);
|
||||||
|
showAlert('configAlert', '✅ Konfiguration aktualisiert!', 'success');
|
||||||
|
} catch (e) {
|
||||||
|
showAlert('configAlert', '❌ Ungültiges JSON: ' + e.message, 'error', 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetConfigJson() {
|
||||||
|
loadConfigJson();
|
||||||
|
showAlert('configAlert', '🔄 Zurückgesetzt', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
// DOWNLOADS / UPLOADS
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
function downloadFile(data, filename) {
|
||||||
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadArticleData() {
|
||||||
|
downloadFile(articlesData, 'article-data.json');
|
||||||
|
showAlert('toolsAlert', '✅ article-data.json heruntergeladen', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadConfig() {
|
||||||
|
downloadFile(configData, 'config.json');
|
||||||
|
showAlert('toolsAlert', '✅ config.json heruntergeladen', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadAll() {
|
||||||
|
const allData = { articles: articlesData, config: configData, exported_at: new Date().toISOString() };
|
||||||
|
downloadFile(allData, 'roll-calculator-backup-' + new Date().toISOString().slice(0,10) + '.json');
|
||||||
|
showAlert('toolsAlert', '✅ Alle Daten heruntergeladen', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportAllAsJson() {
|
||||||
|
downloadAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileUpload(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.target.result);
|
||||||
|
if (data.articles) articlesData = data.articles;
|
||||||
|
if (data.config) configData = data.config;
|
||||||
|
if (data.articleData) articlesData = data.articleData; // alternate key
|
||||||
|
|
||||||
|
showAlert('toolsAlert', '✅ Datei hochgeladen und geladen!', 'success');
|
||||||
|
refreshUI();
|
||||||
|
} catch (err) {
|
||||||
|
showAlert('toolsAlert', '❌ Fehler beim Laden der Datei: ' + err.message, 'error', 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
event.target.value = ''; // Reset file input
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetAll() {
|
||||||
|
articlesData = [];
|
||||||
|
configData = {};
|
||||||
|
showAlert('toolsAlert', '🔄 Auf Standardwerte zurückgesetzt', 'success');
|
||||||
|
refreshUI();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
"""
|
||||||
|
Naue Roll Calculator - Beta
|
||||||
|
Flask app with HTTP Basic Authentication
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Flask, render_template, request, send_file, send_from_directory, jsonify
|
||||||
|
from flask_httpauth import HTTPBasicAuth
|
||||||
|
from functools import wraps
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
auth = HTTPBasicAuth()
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# CONFIGURATION
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Benutzer für Beta-Phase (in Produktion aus env-Variablen laden!)
|
||||||
|
BETA_USERS = {
|
||||||
|
"beta": "rollcalc_beta_2026", # Ändere das Passwort!
|
||||||
|
"naue": "naue_access_2026", # Zweiter User optional
|
||||||
|
"cniehues": "beta_test_2026", # Christian Niehues EDD
|
||||||
|
"lvollmert": "Geheim!", # Lars Vollmert EDD
|
||||||
|
"mtazl": "rollcalc", # Martin Tazl EDD
|
||||||
|
"controlling": "beta2026" # Lars Krückemeier und Lea-Marie
|
||||||
|
}
|
||||||
|
|
||||||
|
# Optional: Admin-Features (z.B. Logs, Stats)
|
||||||
|
ADMIN_USERS = {
|
||||||
|
"admin": "admin_secure_pwd_2026"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Logging für Auditing
|
||||||
|
LOG_FILE = "access_log.json"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# AUTHENTICATION
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@auth.verify_password
|
||||||
|
def verify_password(username, password):
|
||||||
|
"""Verify HTTP Basic Auth credentials"""
|
||||||
|
all_users = {**BETA_USERS, **ADMIN_USERS}
|
||||||
|
if username in all_users and all_users[username] == password:
|
||||||
|
return username
|
||||||
|
return None
|
||||||
|
|
||||||
|
def log_access(username, endpoint, method, status=200):
|
||||||
|
"""Log all access attempts for audit trail"""
|
||||||
|
log_entry = {
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"username": username,
|
||||||
|
"endpoint": endpoint,
|
||||||
|
"method": method,
|
||||||
|
"status": status
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
logs = []
|
||||||
|
if os.path.exists(LOG_FILE):
|
||||||
|
with open(LOG_FILE, "r") as f:
|
||||||
|
logs = json.load(f)
|
||||||
|
logs.append(log_entry)
|
||||||
|
with open(LOG_FILE, "w") as f:
|
||||||
|
json.dump(logs, f, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Logging Error] {e}")
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ROUTES
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@app.route("/", methods=["GET"])
|
||||||
|
@auth.login_required
|
||||||
|
def index():
|
||||||
|
"""Main calculator page - requires authentication"""
|
||||||
|
log_access(auth.current_user(), "/", "GET")
|
||||||
|
return render_template("roll_calculator.html")
|
||||||
|
|
||||||
|
@app.route("/static/<path:filename>", methods=["GET"])
|
||||||
|
@auth.login_required
|
||||||
|
def serve_static(filename):
|
||||||
|
"""Serve static files (CSS, JS, JSON) - protected"""
|
||||||
|
log_access(auth.current_user(), f"/static/{filename}", "GET")
|
||||||
|
return send_from_directory("static", filename)
|
||||||
|
|
||||||
|
@app.route("/api/health", methods=["GET"])
|
||||||
|
@auth.login_required
|
||||||
|
def health_check():
|
||||||
|
"""Simple health check endpoint"""
|
||||||
|
return jsonify({"status": "ok", "version": "1.0.0-beta"}), 200
|
||||||
|
|
||||||
|
@app.route("/api/user", methods=["GET"])
|
||||||
|
@auth.login_required
|
||||||
|
def get_user():
|
||||||
|
"""Get current authenticated user info"""
|
||||||
|
user = auth.current_user()
|
||||||
|
is_admin = user in ADMIN_USERS
|
||||||
|
return jsonify({
|
||||||
|
"username": user,
|
||||||
|
"authenticated": True,
|
||||||
|
"is_admin": is_admin,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
# Admin-only endpoint (optional)
|
||||||
|
@app.route("/admin/logs", methods=["GET"])
|
||||||
|
@auth.login_required
|
||||||
|
def get_logs():
|
||||||
|
"""View access logs - admin only"""
|
||||||
|
user = auth.current_user()
|
||||||
|
if user not in ADMIN_USERS:
|
||||||
|
log_access(user, "/admin/logs", "GET", 403)
|
||||||
|
return jsonify({"error": "Unauthorized"}), 403
|
||||||
|
|
||||||
|
log_access(user, "/admin/logs", "GET")
|
||||||
|
logs = []
|
||||||
|
if os.path.exists(LOG_FILE):
|
||||||
|
with open(LOG_FILE, "r") as f:
|
||||||
|
logs = json.load(f)
|
||||||
|
return jsonify({"logs": logs}), 200
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ERROR HANDLERS
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@app.errorhandler(401)
|
||||||
|
def unauthorized(e):
|
||||||
|
"""Handle 401 Unauthorized - browser will prompt for credentials"""
|
||||||
|
return jsonify({"error": "Unauthorized - please provide valid credentials"}), 401
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def not_found(e):
|
||||||
|
"""Handle 404 Not Found"""
|
||||||
|
return jsonify({"error": "Not found"}), 404
|
||||||
|
|
||||||
|
@app.errorhandler(500)
|
||||||
|
def internal_error(e):
|
||||||
|
"""Handle 500 Internal Server Error"""
|
||||||
|
return jsonify({"error": "Internal server error"}), 500
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ADMIN ROUTES
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@app.route("/admin", methods=["GET"])
|
||||||
|
@auth.login_required
|
||||||
|
def admin_dashboard():
|
||||||
|
"""Admin Dashboard - nur für Admin-Benutzer"""
|
||||||
|
user = auth.current_user()
|
||||||
|
if user not in ADMIN_USERS:
|
||||||
|
log_access(user, "/admin", "GET", 403)
|
||||||
|
return jsonify({"error": "Unauthorized - Admin access required"}), 403
|
||||||
|
|
||||||
|
log_access(user, "/admin", "GET")
|
||||||
|
return render_template("admin.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/admin/stats", methods=["GET"])
|
||||||
|
@auth.login_required
|
||||||
|
def get_admin_stats():
|
||||||
|
"""Statistiken für Admin Dashboard"""
|
||||||
|
user = auth.current_user()
|
||||||
|
if user not in ADMIN_USERS:
|
||||||
|
log_access(user, "/api/admin/stats", "GET", 403)
|
||||||
|
return jsonify({"error": "Unauthorized"}), 403
|
||||||
|
|
||||||
|
log_access(user, "/api/admin/stats", "GET")
|
||||||
|
|
||||||
|
logs = []
|
||||||
|
if os.path.exists(LOG_FILE):
|
||||||
|
try:
|
||||||
|
with open(LOG_FILE, "r") as f:
|
||||||
|
logs = json.load(f)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logs = []
|
||||||
|
|
||||||
|
total_requests = len(logs)
|
||||||
|
unique_users = len(set(log.get("username", "unknown") for log in logs))
|
||||||
|
failed_logins = len([l for l in logs if l.get("status") == 401])
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"total_requests": total_requests,
|
||||||
|
"unique_users": unique_users,
|
||||||
|
"failed_logins": failed_logins,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/admin/logs", methods=["GET"])
|
||||||
|
@auth.login_required
|
||||||
|
def get_admin_logs():
|
||||||
|
"""Access Logs für Admin Dashboard"""
|
||||||
|
user = auth.current_user()
|
||||||
|
if user not in ADMIN_USERS:
|
||||||
|
log_access(user, "/api/admin/logs", "GET", 403)
|
||||||
|
return jsonify({"error": "Unauthorized"}), 403
|
||||||
|
|
||||||
|
log_access(user, "/api/admin/logs", "GET")
|
||||||
|
|
||||||
|
logs = []
|
||||||
|
if os.path.exists(LOG_FILE):
|
||||||
|
try:
|
||||||
|
with open(LOG_FILE, "r") as f:
|
||||||
|
logs = json.load(f)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logs = []
|
||||||
|
|
||||||
|
logs_sorted = sorted(logs, key=lambda x: x.get("timestamp", ""), reverse=True)
|
||||||
|
return jsonify({"logs": logs_sorted[-500:]}), 200
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# STARTUP
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Erstelle templates-Verzeichnis falls nicht vorhanden
|
||||||
|
os.makedirs("templates", exist_ok=True)
|
||||||
|
os.makedirs("static", exist_ok=True)
|
||||||
|
|
||||||
|
print("""
|
||||||
|
╔═══════════════════════════════════════════════════════════╗
|
||||||
|
║ Naue Roll Calculator - Beta ║
|
||||||
|
║ HTTP Basic Auth Enabled ║
|
||||||
|
║ ║
|
||||||
|
║ Test credentials: ║
|
||||||
|
║ User: beta / Password: rollcalc_beta_2026 ║
|
||||||
|
║ User: admin / Password: admin_secure_pwd_2026 ║
|
||||||
|
║ ║
|
||||||
|
║ Starten auf: http://localhost:5000 ║
|
||||||
|
║ ⚠️ ÄNDERE DIE PASSWÖRTER VOR PRODUKTIVBEREITSTELLUNG! ║
|
||||||
|
╚═══════════════════════════════════════════════════════════╝
|
||||||
|
""")
|
||||||
|
|
||||||
|
app.run(
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=5000,
|
||||||
|
debug=False, # In Produktion: False
|
||||||
|
use_reloader=True
|
||||||
|
)
|
||||||
+1920
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+59
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Naue Roll Calculator - General Configuration. Article data stays in article-data.json.",
|
||||||
|
"forklift_rules": {
|
||||||
|
"_comment": "Weight-based forklift and core diameter rules. Applies per product category.",
|
||||||
|
"applicable_categories": [
|
||||||
|
"bentofix"
|
||||||
|
],
|
||||||
|
"applicable_sites": [
|
||||||
|
"espelkamp",
|
||||||
|
"malaysia"
|
||||||
|
],
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "heavy_roll",
|
||||||
|
"condition": "weight_kg >= 1700",
|
||||||
|
"weight_threshold_kg": 1700,
|
||||||
|
"max_weight_kg": 2750,
|
||||||
|
"requirements": {
|
||||||
|
"min_core_outer_diameter_mm": 170,
|
||||||
|
"forklift_mandrel_diameter_mm": 140,
|
||||||
|
"forklift_capacity_t": 7.5
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"en": {
|
||||||
|
"warning_title": "Heavy Roll – Special Equipment Required",
|
||||||
|
"requirements_label": "The following requirements apply for rolls ≥ 1700 kg (Bentofix):",
|
||||||
|
"req_core": "Core outer diameter ≥ 170 mm required (current: {core} mm)",
|
||||||
|
"req_mandrel": "Forklift mandrel diameter: 140 mm required",
|
||||||
|
"req_forklift": "Forklift capacity: 7.5 t required",
|
||||||
|
"core_ok": "Core diameter OK (≥ 170 mm)",
|
||||||
|
"core_nok": "Core diameter too small – minimum 170 mm required (current: {core} mm)"
|
||||||
|
},
|
||||||
|
"de": {
|
||||||
|
"warning_title": "Schwere Rolle – Sonderausrüstung erforderlich",
|
||||||
|
"requirements_label": "Für Rollen ≥ 1700 kg (Bentofix) gelten folgende Anforderungen:",
|
||||||
|
"req_core": "Kernaußendurchmesser ≥ 170 mm erforderlich (aktuell: {core} mm)",
|
||||||
|
"req_mandrel": "Gabelstapler-Dorn: 140 mm Durchmesser erforderlich",
|
||||||
|
"req_forklift": "Gabelstapler: 7,5 t Tragkraft erforderlich",
|
||||||
|
"core_ok": "Kerndurchmesser OK (≥ 170 mm)",
|
||||||
|
"core_nok": "Kerndurchmesser zu klein – Minimum 170 mm erforderlich (aktuell: {core} mm)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hard_limit": {
|
||||||
|
"max_weight_kg": 2750,
|
||||||
|
"messages": {
|
||||||
|
"en": {
|
||||||
|
"block_title": "Weight Limit Exceeded – Calculation Blocked",
|
||||||
|
"block_text": "The calculated roll weight of {weight} kg exceeds the absolute maximum of 2750 kg for Bentofix. Please adjust the product length or roll dimensions."
|
||||||
|
},
|
||||||
|
"de": {
|
||||||
|
"block_title": "Gewichtslimit überschritten – Berechnung gesperrt",
|
||||||
|
"block_text": "Das berechnete Rollengewicht von {weight} kg überschreitet das absolute Maximum von 2750 kg für Bentofix. Bitte Produktlänge oder Rollenmaße anpassen."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Automatischer Fix für article-data.json Fetch-URLs in Flask
|
||||||
|
Ersetzt relative URLs durch /static/ URLs
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def fix_html_file(html_path):
|
||||||
|
"""Ersetzt die alten Fetch-URLs durch die neuen Flask-URLs"""
|
||||||
|
|
||||||
|
if not os.path.exists(html_path):
|
||||||
|
print(f"❌ Datei nicht gefunden: {html_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"📖 Öffne: {html_path}")
|
||||||
|
|
||||||
|
with open(html_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
original_content = content
|
||||||
|
changes = []
|
||||||
|
|
||||||
|
# Fix 1: fetch('article-data.json') → fetch('/static/article-data.json')
|
||||||
|
if "fetch('article-data.json')" in content or 'fetch("article-data.json")' in content:
|
||||||
|
content = content.replace("fetch('article-data.json')", "fetch('/static/article-data.json')")
|
||||||
|
content = content.replace('fetch("article-data.json")', 'fetch("/static/article-data.json")')
|
||||||
|
changes.append("✓ article-data.json URL aktualisiert")
|
||||||
|
|
||||||
|
# Fix 2: fetch('config.json') → fetch('/static/config.json')
|
||||||
|
if "fetch('config.json')" in content or 'fetch("config.json")' in content:
|
||||||
|
content = content.replace("fetch('config.json')", "fetch('/static/config.json')")
|
||||||
|
content = content.replace('fetch("config.json")', 'fetch("/static/config.json")')
|
||||||
|
changes.append("✓ config.json URL aktualisiert")
|
||||||
|
|
||||||
|
# Fix 3: register('service-worker.js') → register('/static/service-worker.js')
|
||||||
|
if "register('service-worker.js')" in content or 'register("service-worker.js")' in content:
|
||||||
|
content = content.replace("register('service-worker.js')", "register('/static/service-worker.js')")
|
||||||
|
content = content.replace('register("service-worker.js")', 'register("/static/service-worker.js")')
|
||||||
|
changes.append("✓ service-worker.js URL aktualisiert")
|
||||||
|
|
||||||
|
if content != original_content:
|
||||||
|
# Backup erstellen
|
||||||
|
backup_path = html_path + ".backup"
|
||||||
|
with open(backup_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(original_content)
|
||||||
|
print(f"💾 Backup erstellt: {backup_path}")
|
||||||
|
|
||||||
|
# Neue Version speichern
|
||||||
|
with open(html_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
print(f"✅ HTML aktualisiert: {html_path}")
|
||||||
|
for change in changes:
|
||||||
|
print(f" {change}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("⚠️ Keine Änderungen notwendig (URLs sind bereits korrekt)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_static_files():
|
||||||
|
"""Prüft ob die benötigten Dateien im static/ Ordner sind"""
|
||||||
|
print("\n🔍 Prüfe static/ Verzeichnis...")
|
||||||
|
|
||||||
|
files_to_check = ['article-data.json', 'config.json']
|
||||||
|
found = []
|
||||||
|
missing = []
|
||||||
|
|
||||||
|
for filename in files_to_check:
|
||||||
|
path = os.path.join('static', filename)
|
||||||
|
if os.path.exists(path):
|
||||||
|
size = os.path.getsize(path)
|
||||||
|
found.append(f" ✓ {filename} ({size} bytes)")
|
||||||
|
else:
|
||||||
|
missing.append(f" ❌ {filename} (NICHT GEFUNDEN)")
|
||||||
|
|
||||||
|
for msg in found:
|
||||||
|
print(msg)
|
||||||
|
for msg in missing:
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
return len(missing) == 0
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("=" * 60)
|
||||||
|
print("Fix für article-data.json Flask-URLs")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
html_file = "templates/roll_calculator.html"
|
||||||
|
|
||||||
|
success = fix_html_file(html_file)
|
||||||
|
files_ok = check_static_files()
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
if success or files_ok:
|
||||||
|
print("✅ Fix abgeschlossen!")
|
||||||
|
print("\n📝 Nächste Schritte:")
|
||||||
|
print(" 1. Browser neuladen (Ctrl+Shift+R)")
|
||||||
|
print(" 2. Öffne Browser Console (F12)")
|
||||||
|
print(" 3. Du solltest sehen:")
|
||||||
|
print(" 'Article data loaded: XXX items'")
|
||||||
|
print(" 'Config updated from file'")
|
||||||
|
else:
|
||||||
|
print("⚠️ Keine Änderungen nötig")
|
||||||
|
print("=" * 60)
|
||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
roll_calculator.html
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
Flask==2.3.3
|
||||||
|
Flask-HTTPAuth==4.8.0
|
||||||
|
Werkzeug==2.3.7
|
||||||
|
click==8.1.7
|
||||||
|
itsdangerous==2.1.2
|
||||||
|
Jinja2==3.1.2
|
||||||
|
MarkupSafe==2.1.3
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,83 @@
|
|||||||
|
// service-worker.js
|
||||||
|
// Cache-Strategie: Cache First (offline works, then network)
|
||||||
|
|
||||||
|
const CACHE_NAME = 'roll-calculator-v3'; // bumped to v2 for new clearance settings
|
||||||
|
const ASSETS_TO_CACHE = [
|
||||||
|
'./',
|
||||||
|
'./roll_calculator_v9.html',
|
||||||
|
'./article-data.json',
|
||||||
|
'./config.json',
|
||||||
|
'./service-worker.js'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Install Event
|
||||||
|
self.addEventListener('install', event => {
|
||||||
|
console.log('[ServiceWorker] Installing...');
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME).then(cache => {
|
||||||
|
console.log('[ServiceWorker] Caching assets:', ASSETS_TO_CACHE);
|
||||||
|
return cache.addAll(ASSETS_TO_CACHE);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[ServiceWorker] Cache failed:', err);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Activate Event: Cleanup alte Caches
|
||||||
|
self.addEventListener('activate', event => {
|
||||||
|
console.log('[ServiceWorker] Activating...');
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then(cacheNames => {
|
||||||
|
return Promise.all(
|
||||||
|
cacheNames.map(cacheName => {
|
||||||
|
if (cacheName !== CACHE_NAME) {
|
||||||
|
console.log('[ServiceWorker] Deleting old cache:', cacheName);
|
||||||
|
return caches.delete(cacheName);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
self.clients.claim();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch Event: Cache First Strategy
|
||||||
|
self.addEventListener('fetch', event => {
|
||||||
|
const url = event.request.url;
|
||||||
|
|
||||||
|
if (event.request.method !== 'GET') return;
|
||||||
|
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request)
|
||||||
|
.then(response => {
|
||||||
|
if (response) {
|
||||||
|
console.log('[ServiceWorker] Cache HIT:', url);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
console.log('[ServiceWorker] Cache MISS, fetching:', url);
|
||||||
|
return fetch(event.request)
|
||||||
|
.then(response => {
|
||||||
|
if (!response || response.status !== 200 || response.type === 'error') {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
const responseClone = response.clone();
|
||||||
|
caches.open(CACHE_NAME).then(cache => {
|
||||||
|
cache.put(event.request, responseClone);
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return caches.match(event.request).then(response => {
|
||||||
|
if (response) return response;
|
||||||
|
return new Response('Offline - resource not available', {
|
||||||
|
status: 503,
|
||||||
|
statusText: 'Service Unavailable'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[ServiceWorker] Script loaded (v2)');
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Naue Roll Calculator - General Configuration. Article data stays in article-data.json.",
|
||||||
|
"forklift_rules": {
|
||||||
|
"_comment": "Weight-based forklift and core diameter rules. Applies per product category.",
|
||||||
|
"applicable_categories": [
|
||||||
|
"bentofix"
|
||||||
|
],
|
||||||
|
"applicable_sites": [
|
||||||
|
"espelkamp",
|
||||||
|
"malaysia"
|
||||||
|
],
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "heavy_roll",
|
||||||
|
"condition": "weight_kg >= 1700",
|
||||||
|
"weight_threshold_kg": 1700,
|
||||||
|
"max_weight_kg": 2750,
|
||||||
|
"requirements": {
|
||||||
|
"min_core_outer_diameter_mm": 170,
|
||||||
|
"forklift_mandrel_diameter_mm": 140,
|
||||||
|
"forklift_capacity_t": 7.5
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"en": {
|
||||||
|
"warning_title": "Heavy Roll – Special Equipment Required",
|
||||||
|
"requirements_label": "The following requirements apply for rolls ≥ 1700 kg (Bentofix):",
|
||||||
|
"req_core": "Core outer diameter ≥ 170 mm required (current: {core} mm)",
|
||||||
|
"req_mandrel": "Forklift mandrel diameter: 140 mm required",
|
||||||
|
"req_forklift": "Forklift capacity: 7.5 t required",
|
||||||
|
"core_ok": "Core diameter OK (≥ 170 mm)",
|
||||||
|
"core_nok": "Core diameter too small – minimum 170 mm required (current: {core} mm)"
|
||||||
|
},
|
||||||
|
"de": {
|
||||||
|
"warning_title": "Schwere Rolle – Sonderausrüstung erforderlich",
|
||||||
|
"requirements_label": "Für Rollen ≥ 1700 kg (Bentofix) gelten folgende Anforderungen:",
|
||||||
|
"req_core": "Kernaußendurchmesser ≥ 170 mm erforderlich (aktuell: {core} mm)",
|
||||||
|
"req_mandrel": "Gabelstapler-Dorn: 140 mm Durchmesser erforderlich",
|
||||||
|
"req_forklift": "Gabelstapler: 7,5 t Tragkraft erforderlich",
|
||||||
|
"core_ok": "Kerndurchmesser OK (≥ 170 mm)",
|
||||||
|
"core_nok": "Kerndurchmesser zu klein – Minimum 170 mm erforderlich (aktuell: {core} mm)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hard_limit": {
|
||||||
|
"max_weight_kg": 2750,
|
||||||
|
"messages": {
|
||||||
|
"en": {
|
||||||
|
"block_title": "Weight Limit Exceeded – Calculation Blocked",
|
||||||
|
"block_text": "The calculated roll weight of {weight} kg exceeds the absolute maximum of 2750 kg for Bentofix. Please adjust the product length or roll dimensions."
|
||||||
|
},
|
||||||
|
"de": {
|
||||||
|
"block_title": "Gewichtslimit überschritten – Berechnung gesperrt",
|
||||||
|
"block_text": "Das berechnete Rollengewicht von {weight} kg überschreitet das absolute Maximum von 2750 kg für Bentofix. Bitte Produktlänge oder Rollenmaße anpassen."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
// service-worker.js
|
||||||
|
// Cache-Strategie: Cache First (offline works, then network)
|
||||||
|
|
||||||
|
const CACHE_NAME = 'roll-calculator-v3'; // bumped to v2 for new clearance settings
|
||||||
|
const ASSETS_TO_CACHE = [
|
||||||
|
'./',
|
||||||
|
'./roll_calculator_v9.html',
|
||||||
|
'./article-data.json',
|
||||||
|
'./config.json',
|
||||||
|
'./service-worker.js'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Install Event
|
||||||
|
self.addEventListener('install', event => {
|
||||||
|
console.log('[ServiceWorker] Installing...');
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME).then(cache => {
|
||||||
|
console.log('[ServiceWorker] Caching assets:', ASSETS_TO_CACHE);
|
||||||
|
return cache.addAll(ASSETS_TO_CACHE);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('[ServiceWorker] Cache failed:', err);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Activate Event: Cleanup alte Caches
|
||||||
|
self.addEventListener('activate', event => {
|
||||||
|
console.log('[ServiceWorker] Activating...');
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then(cacheNames => {
|
||||||
|
return Promise.all(
|
||||||
|
cacheNames.map(cacheName => {
|
||||||
|
if (cacheName !== CACHE_NAME) {
|
||||||
|
console.log('[ServiceWorker] Deleting old cache:', cacheName);
|
||||||
|
return caches.delete(cacheName);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
self.clients.claim();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch Event: Cache First Strategy
|
||||||
|
self.addEventListener('fetch', event => {
|
||||||
|
const url = event.request.url;
|
||||||
|
|
||||||
|
if (event.request.method !== 'GET') return;
|
||||||
|
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request)
|
||||||
|
.then(response => {
|
||||||
|
if (response) {
|
||||||
|
console.log('[ServiceWorker] Cache HIT:', url);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
console.log('[ServiceWorker] Cache MISS, fetching:', url);
|
||||||
|
return fetch(event.request)
|
||||||
|
.then(response => {
|
||||||
|
if (!response || response.status !== 200 || response.type === 'error') {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
const responseClone = response.clone();
|
||||||
|
caches.open(CACHE_NAME).then(cache => {
|
||||||
|
cache.put(event.request, responseClone);
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return caches.match(event.request).then(response => {
|
||||||
|
if (response) return response;
|
||||||
|
return new Response('Offline - resource not available', {
|
||||||
|
status: 503,
|
||||||
|
statusText: 'Service Unavailable'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[ServiceWorker] Script loaded (v2)');
|
||||||
@@ -0,0 +1,568 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="theme-color" content="#0066cc">
|
||||||
|
<title>Admin Panel – Roll Calculator</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Arial, sans-serif;
|
||||||
|
background: #f4f6f9;
|
||||||
|
color: #333;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- HEADER ---- */
|
||||||
|
.admin-header {
|
||||||
|
background: linear-gradient(135deg, #003366 0%, #0066cc 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px 32px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header .subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header .user-info {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.9;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header .user-info strong {
|
||||||
|
opacity: 1;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- CONTAINER ---- */
|
||||||
|
.admin-container {
|
||||||
|
max-width: 1300px;
|
||||||
|
margin: 30px auto;
|
||||||
|
padding: 0 20px 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- STATS GRID ---- */
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
|
||||||
|
border-left: 4px solid #0066cc;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.warning {
|
||||||
|
border-left-color: #f0b429;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.danger {
|
||||||
|
border-left-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .value {
|
||||||
|
font-size: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0066cc;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.warning .value {
|
||||||
|
color: #f0b429;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.danger .value {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .sublabel {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- LOGS SECTION ---- */
|
||||||
|
.logs-section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-header h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #003366;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background: #0066cc;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background: #0052a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.secondary {
|
||||||
|
background: #e0e0e0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.secondary:hover {
|
||||||
|
background: #d0d0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-refresh {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-refresh.spinning {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- TABLE ---- */
|
||||||
|
.logs-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-table thead {
|
||||||
|
background: #f0f4fa;
|
||||||
|
border-bottom: 2px solid #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-table th {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-table td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid #e8ecf0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-table tbody tr {
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-table tbody tr:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-200 {
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: 600;
|
||||||
|
background: #f0f9f6;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-401 {
|
||||||
|
color: #dc3545;
|
||||||
|
font-weight: 600;
|
||||||
|
background: #fff0f0;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-403 {
|
||||||
|
color: #f0b429;
|
||||||
|
font-weight: 600;
|
||||||
|
background: #fffbea;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- LOADING / EMPTY ---- */
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: #fff0f0;
|
||||||
|
color: #dc3545;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border-left: 4px solid #dc3545;
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- FILTERS ---- */
|
||||||
|
.filters {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #0066cc;
|
||||||
|
box-shadow: 0 0 0 2px rgba(0,102,204,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- FOOTER ---- */
|
||||||
|
.admin-footer {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #aaa;
|
||||||
|
margin-top: 40px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e8ecf0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- RESPONSIVE ---- */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.admin-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header .user-info {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-table {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-table th, .logs-table td {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- HEADER -->
|
||||||
|
<div class="admin-header">
|
||||||
|
<div>
|
||||||
|
<h1>🔐 Admin Panel</h1>
|
||||||
|
<div class="subtitle">Roll Calculator – Naue GmbH & Co. KG</div>
|
||||||
|
</div>
|
||||||
|
<div class="user-info">
|
||||||
|
<span>Angemeldet als:</span>
|
||||||
|
<strong id="authUser">admin</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MAIN CONTAINER -->
|
||||||
|
<div class="admin-container">
|
||||||
|
|
||||||
|
<!-- STATISTICS -->
|
||||||
|
<div class="stats-grid" id="statsGrid">
|
||||||
|
<div style="grid-column: 1 / -1; text-align: center; padding: 40px; color: #999;">
|
||||||
|
⏳ Statistiken werden geladen...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- LOGS SECTION -->
|
||||||
|
<div class="logs-section">
|
||||||
|
|
||||||
|
<!-- HEADER -->
|
||||||
|
<div class="logs-header">
|
||||||
|
<h2>📋 Access Logs</h2>
|
||||||
|
<div class="controls">
|
||||||
|
<button class="btn" id="refreshBtn" onclick="refreshData()">
|
||||||
|
<span id="refreshIcon">🔄</span> Aktualisieren
|
||||||
|
</button>
|
||||||
|
<button class="btn secondary" onclick="downloadLogs()">
|
||||||
|
⬇️ Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FILTERS -->
|
||||||
|
<div class="filters">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="filter-input"
|
||||||
|
placeholder="Nach Benutzer filtern..."
|
||||||
|
id="filterUser"
|
||||||
|
onkeyup="filterLogs()"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="filter-input"
|
||||||
|
placeholder="Nach Endpoint filtern..."
|
||||||
|
id="filterEndpoint"
|
||||||
|
onkeyup="filterLogs()"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TABLE -->
|
||||||
|
<div id="errorContainer"></div>
|
||||||
|
|
||||||
|
<table class="logs-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Zeitstempel</th>
|
||||||
|
<th>Benutzer</th>
|
||||||
|
<th>Endpoint</th>
|
||||||
|
<th>Methode</th>
|
||||||
|
<th style="width: 80px;">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="logsList">
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="loading">Logs werden geladen...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FOOTER -->
|
||||||
|
<div class="admin-footer">
|
||||||
|
ℹ️ Logs werden automatisch alle 30 Sekunden aktualisiert |
|
||||||
|
Letztes Update: <span id="lastUpdate">-</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SCRIPTS -->
|
||||||
|
<script>
|
||||||
|
let allLogs = [];
|
||||||
|
|
||||||
|
// Get authenticated user from API
|
||||||
|
async function getAuthUser() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/user');
|
||||||
|
const data = await res.json();
|
||||||
|
document.getElementById('authUser').textContent = data.username || 'admin';
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Konnte Benutzerinformation nicht laden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh Data (Statistiken + Logs)
|
||||||
|
async function refreshData() {
|
||||||
|
const btn = document.getElementById('refreshBtn');
|
||||||
|
const icon = document.getElementById('refreshIcon');
|
||||||
|
|
||||||
|
try {
|
||||||
|
icon.style.animation = 'spin 1s linear infinite';
|
||||||
|
|
||||||
|
// Load Statistics
|
||||||
|
const statsRes = await fetch('/api/admin/stats');
|
||||||
|
if (!statsRes.ok) throw new Error('Statistiken konnten nicht geladen werden');
|
||||||
|
const stats = await statsRes.json();
|
||||||
|
|
||||||
|
// Load Logs
|
||||||
|
const logsRes = await fetch('/api/admin/logs');
|
||||||
|
if (!logsRes.ok) throw new Error('Logs konnten nicht geladen werden');
|
||||||
|
const logsData = await logsRes.json();
|
||||||
|
|
||||||
|
allLogs = logsData.logs || [];
|
||||||
|
|
||||||
|
// Render Statistics
|
||||||
|
renderStats(stats);
|
||||||
|
|
||||||
|
// Render Logs
|
||||||
|
renderLogs(allLogs);
|
||||||
|
|
||||||
|
// Update timestamp
|
||||||
|
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString('de-DE');
|
||||||
|
|
||||||
|
// Clear error
|
||||||
|
document.getElementById('errorContainer').innerHTML = '';
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Laden der Daten:', error);
|
||||||
|
document.getElementById('errorContainer').innerHTML =
|
||||||
|
'<div class="error">❌ ' + error.message + '</div>';
|
||||||
|
} finally {
|
||||||
|
icon.style.animation = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Statistics
|
||||||
|
function renderStats(stats) {
|
||||||
|
const html = `
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">📊 Gesamte Anfragen</div>
|
||||||
|
<div class="value">${stats.total_requests || 0}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">👥 Eindeutige Benutzer</div>
|
||||||
|
<div class="value">${stats.unique_users || 0}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card danger">
|
||||||
|
<div class="label">⛔ Fehlgeschlagene Logins</div>
|
||||||
|
<div class="value">${stats.failed_logins || 0}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.getElementById('statsGrid').innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render Logs
|
||||||
|
function renderLogs(logs) {
|
||||||
|
if (!logs || logs.length === 0) {
|
||||||
|
document.getElementById('logsList').innerHTML =
|
||||||
|
'<tr><td colspan="5" class="empty">Keine Logs verfügbar</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = logs.reverse().map(log => {
|
||||||
|
const time = new Date(log.timestamp).toLocaleString('de-DE');
|
||||||
|
const statusClass = `status-${log.status}`;
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${time}</td>
|
||||||
|
<td><strong>${log.username}</strong></td>
|
||||||
|
<td><code>${log.endpoint}</code></td>
|
||||||
|
<td>${log.method}</td>
|
||||||
|
<td><span class="${statusClass}">${log.status}</span></td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
document.getElementById('logsList').innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter Logs
|
||||||
|
function filterLogs() {
|
||||||
|
const userFilter = document.getElementById('filterUser').value.toLowerCase();
|
||||||
|
const endpointFilter = document.getElementById('filterEndpoint').value.toLowerCase();
|
||||||
|
|
||||||
|
const filtered = allLogs.filter(log =>
|
||||||
|
log.username.toLowerCase().includes(userFilter) &&
|
||||||
|
log.endpoint.toLowerCase().includes(endpointFilter)
|
||||||
|
);
|
||||||
|
|
||||||
|
renderLogs(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download Logs as JSON
|
||||||
|
function downloadLogs() {
|
||||||
|
const data = JSON.stringify(allLogs, null, 2);
|
||||||
|
const blob = new Blob([data], { type: 'application/json' });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `access-logs-${new Date().toISOString()}.json`;
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
getAuthUser();
|
||||||
|
refreshData();
|
||||||
|
|
||||||
|
// Auto-refresh every 30 seconds
|
||||||
|
setInterval(refreshData, 30000);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user