added article_data.json and config.json. Removed product data from main html

This commit is contained in:
2026-05-28 14:29:07 +02:00
parent f3376e4ff7
commit 6594405d9c
10 changed files with 11368 additions and 28 deletions
+849
View File
@@ -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>
+694
View File
@@ -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>
+694
View File
@@ -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
+59
View File
@@ -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
+7 -27
View File
@@ -1,16 +1,16 @@
// service-worker.js // service-worker.js
// Cache-Strategie: Cache First (offline works, then network) // 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 = [ const ASSETS_TO_CACHE = [
'./', './',
'./roll_calculator_v8.html', './roll_calculator_v9.html',
'./article-data.json', './article-data.json',
'./config.json',
'./service-worker.js' './service-worker.js'
]; ];
// Install Event: Cache alle kritischen Assets // Install Event
self.addEventListener('install', event => { self.addEventListener('install', event => {
console.log('[ServiceWorker] Installing...'); console.log('[ServiceWorker] Installing...');
event.waitUntil( event.waitUntil(
@@ -21,7 +21,6 @@ self.addEventListener('install', event => {
console.error('[ServiceWorker] Cache failed:', err); console.error('[ServiceWorker] Cache failed:', err);
}) })
); );
// Aktiviere sofort (optional)
self.skipWaiting(); self.skipWaiting();
}); });
@@ -44,52 +43,33 @@ self.addEventListener('activate', event => {
}); });
// Fetch Event: Cache First Strategy // 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 => { self.addEventListener('fetch', event => {
const url = event.request.url; const url = event.request.url;
// Skip nicht-GET Requests if (event.request.method !== 'GET') return;
if (event.request.method !== 'GET') {
return;
}
event.respondWith( event.respondWith(
caches.match(event.request) caches.match(event.request)
.then(response => { .then(response => {
// Cache HIT
if (response) { if (response) {
console.log('[ServiceWorker] Cache HIT:', url); console.log('[ServiceWorker] Cache HIT:', url);
return response; return response;
} }
// Cache MISS - versuche Network
console.log('[ServiceWorker] Cache MISS, fetching:', url); console.log('[ServiceWorker] Cache MISS, fetching:', url);
return fetch(event.request) return fetch(event.request)
.then(response => { .then(response => {
// Nur 200er Responses cachen
if (!response || response.status !== 200 || response.type === 'error') { if (!response || response.status !== 200 || response.type === 'error') {
return response; return response;
} }
// Clone die Response (weil sie nur einmal consumable ist)
const responseClone = response.clone(); const responseClone = response.clone();
caches.open(CACHE_NAME).then(cache => { caches.open(CACHE_NAME).then(cache => {
console.log('[ServiceWorker] Caching response:', url);
cache.put(event.request, responseClone); cache.put(event.request, responseClone);
}); });
return response; return response;
}) })
.catch(err => { .catch(() => {
// Network fail - versuche aus Cache
console.log('[ServiceWorker] Network failed, trying cache fallback:', url);
return caches.match(event.request).then(response => { return caches.match(event.request).then(response => {
if (response) return response; if (response) return response;
// Fallback für offline
console.warn('[ServiceWorker] No cache fallback for:', url);
return new Response('Offline - resource not available', { return new Response('Offline - resource not available', {
status: 503, status: 503,
statusText: 'Service Unavailable' statusText: 'Service Unavailable'
@@ -100,4 +80,4 @@ self.addEventListener('fetch', event => {
); );
}); });
console.log('[ServiceWorker] Script loaded'); console.log('[ServiceWorker] Script loaded (v2)');