added article_data.json and config.json. Removed product data from main html
This commit is contained in:
@@ -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>
|
||||
File diff suppressed because it is too large
Load Diff
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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,16 @@
|
||||
|
||||
// service-worker.js
|
||||
// Cache-Strategie: Cache First (offline works, then network)
|
||||
|
||||
const CACHE_NAME = 'roll-calculator-v1';
|
||||
const CACHE_NAME = 'roll-calculator-v3'; // bumped to v2 for new clearance settings
|
||||
const ASSETS_TO_CACHE = [
|
||||
'./',
|
||||
'./roll_calculator_v8.html',
|
||||
'./roll_calculator_v9.html',
|
||||
'./article-data.json',
|
||||
'./config.json',
|
||||
'./service-worker.js'
|
||||
];
|
||||
|
||||
// Install Event: Cache alle kritischen Assets
|
||||
// Install Event
|
||||
self.addEventListener('install', event => {
|
||||
console.log('[ServiceWorker] Installing...');
|
||||
event.waitUntil(
|
||||
@@ -21,7 +21,6 @@ self.addEventListener('install', event => {
|
||||
console.error('[ServiceWorker] Cache failed:', err);
|
||||
})
|
||||
);
|
||||
// Aktiviere sofort (optional)
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
@@ -44,52 +43,33 @@ self.addEventListener('activate', event => {
|
||||
});
|
||||
|
||||
// Fetch Event: Cache First Strategy
|
||||
// 1. Prüfe Cache
|
||||
// 2. Wenn nicht da, frag Network
|
||||
// 3. Cache die Response für nächstes Mal
|
||||
self.addEventListener('fetch', event => {
|
||||
const url = event.request.url;
|
||||
|
||||
// Skip nicht-GET Requests
|
||||
if (event.request.method !== 'GET') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.request.method !== 'GET') return;
|
||||
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then(response => {
|
||||
// Cache HIT
|
||||
if (response) {
|
||||
console.log('[ServiceWorker] Cache HIT:', url);
|
||||
return response;
|
||||
}
|
||||
|
||||
// Cache MISS - versuche Network
|
||||
console.log('[ServiceWorker] Cache MISS, fetching:', url);
|
||||
return fetch(event.request)
|
||||
.then(response => {
|
||||
// Nur 200er Responses cachen
|
||||
if (!response || response.status !== 200 || response.type === 'error') {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Clone die Response (weil sie nur einmal consumable ist)
|
||||
const responseClone = response.clone();
|
||||
caches.open(CACHE_NAME).then(cache => {
|
||||
console.log('[ServiceWorker] Caching response:', url);
|
||||
cache.put(event.request, responseClone);
|
||||
});
|
||||
|
||||
return response;
|
||||
})
|
||||
.catch(err => {
|
||||
// Network fail - versuche aus Cache
|
||||
console.log('[ServiceWorker] Network failed, trying cache fallback:', url);
|
||||
.catch(() => {
|
||||
return caches.match(event.request).then(response => {
|
||||
if (response) return response;
|
||||
|
||||
// Fallback für offline
|
||||
console.warn('[ServiceWorker] No cache fallback for:', url);
|
||||
return new Response('Offline - resource not available', {
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable'
|
||||
@@ -100,4 +80,4 @@ self.addEventListener('fetch', event => {
|
||||
);
|
||||
});
|
||||
|
||||
console.log('[ServiceWorker] Script loaded');
|
||||
console.log('[ServiceWorker] Script loaded (v2)');
|
||||
|
||||
Reference in New Issue
Block a user