Files
assetsTracker/index.html.bak
2026-03-08 08:34:55 +01:00

691 lines
27 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DefektTrack Defekte Ware verwalten</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Segoe UI', Arial, sans-serif; background: #F4F6FA; color: #333; }
/* HEADER */
header {
background: #1A2B4A;
color: white;
padding: 0 24px;
display: flex;
align-items: center;
justify-content: space-between;
height: 60px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.logo { font-size: 22px; font-weight: 700; }
.logo span { color: #F57C00; }
.header-sub { font-size: 12px; color: #B0C4DE; margin-top: 2px; }
/* DASHBOARD */
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 16px;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.stat-card {
background: white;
border-radius: 8px;
padding: 20px;
text-align: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
border-top: 4px solid #ccc;
}
.stat-card.red { border-color: #C62828; }
.stat-card.yellow { border-color: #F9A825; }
.stat-card.blue { border-color: #1565C0; }
.stat-card.green { border-color: #2E7D32; }
.stat-card.gray { border-color: #607D8B; }
.stat-number { font-size: 40px; font-weight: 700; color: #1A2B4A; }
.stat-label { font-size: 12px; color: #888; margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
/* MAIN LAYOUT */
.main { max-width: 1200px; margin: 0 auto; padding: 0 24px 40px; display: grid; grid-template-columns: 340px 1fr; gap: 24px; }
@media(max-width: 900px) { .main { grid-template-columns: 1fr; } }
/* FORM */
.form-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
overflow: hidden;
}
.form-header {
background: #1A2B4A;
color: white;
padding: 14px 20px;
font-weight: 600;
font-size: 15px;
}
.form-body { padding: 20px; }
.form-group { margin-bottom: 14px; }
label { display: block; font-size: 12px; font-weight: 600; color: #555; margin-bottom: 5px; text-transform: uppercase; letter-spacing: 0.4px; }
input, select, textarea {
width: 100%; padding: 9px 12px; border: 1px solid #ddd; border-radius: 6px;
font-size: 13px; color: #333; transition: border 0.2s;
font-family: inherit;
}
input:focus, select:focus, textarea:focus { outline: none; border-color: #F57C00; }
textarea { resize: vertical; min-height: 70px; }
.btn-submit {
width: 100%; padding: 12px; background: #F57C00; color: white; border: none;
border-radius: 6px; font-size: 14px; font-weight: 700; cursor: pointer;
transition: background 0.2s; margin-top: 6px;
}
.btn-submit:hover { background: #E65100; }
/* TABLE AREA */
.table-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
overflow: hidden;
}
.table-header {
background: #1A2B4A;
color: white;
padding: 14px 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.table-title { font-weight: 600; font-size: 15px; }
.filter-bar { display: flex; gap: 8px; flex-wrap: wrap; }
.filter-bar input, .filter-bar select {
background: rgba(255,255,255,0.15); border: 1px solid rgba(255,255,255,0.3);
color: white; padding: 6px 10px; border-radius: 5px; font-size: 12px; width: auto;
}
.filter-bar input::placeholder { color: rgba(255,255,255,0.6); }
.filter-bar select option { color: #333; background: white; }
.btn-print {
background: #F57C00; color: white; border: none; padding: 6px 12px;
border-radius: 5px; font-size: 12px; font-weight: 600; cursor: pointer;
transition: background 0.2s;
}
.btn-print:hover { background: #E65100; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th { background: #F4F6FA; padding: 10px 12px; text-align: left; font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 2px solid #eee; }
td { padding: 11px 12px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; }
tr:hover td { background: #fafbfd; }
tr.overdue td { background: #FFF3E0; }
tr.overdue td:first-child { border-left: 3px solid #F57C00; }
.badge {
display: inline-block; padding: 3px 10px; border-radius: 20px;
font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;
}
.badge-offen { background: #FFEBEE; color: #C62828; }
.badge-bearbeitung { background: #FFF9C4; color: #F57F17; }
.badge-erledigt { background: #E8F5E9; color: #2E7D32; }
.badge-entsorgt { background: #ECEFF1; color: #607D8B; }
.prio-badge {
display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 5px;
}
.prio-kritisch { background: #C62828; }
.prio-hoch { background: #F57C00; }
.prio-mittel { background: #F9A825; }
.prio-niedrig { background: #43A047; }
.btn-action {
padding: 4px 10px; border-radius: 4px; font-size: 11px; cursor: pointer;
border: none; font-weight: 600; margin: 0 2px; transition: opacity 0.15s;
}
.btn-action:hover { opacity: 0.8; }
.btn-status { background: #E3F2FD; color: #1565C0; }
.btn-delete { background: #FFEBEE; color: #C62828; }
.btn-info { background: #F3E5F5; color: #7B1FA2; }
.comment-popup {
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: white; padding: 24px; border-radius: 10px; max-width: 500px; width: 90%;
box-shadow: 0 8px 32px rgba(0,0,0,0.3); z-index: 1000;
}
.comment-popup h3 { margin-bottom: 12px; color: #1A2B4A; font-size: 16px; }
.comment-popup .subject { background: #FFF3E0; color: #E65100; padding: 8px 12px; border-radius: 6px; margin-bottom: 12px; font-weight: 600; }
.comment-popup .text { background: #F5F5F5; padding: 12px; border-radius: 6px; white-space: pre-wrap; font-size: 13px; max-height: 200px; overflow-y: auto; }
.comment-popup .close-btn { margin-top: 16px; width: 100%; padding: 10px; background: #1A2B4A; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; }
.comment-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 999; }
.empty-state { text-align: center; padding: 60px 20px; color: #aaa; }
.empty-state .emoji { font-size: 48px; margin-bottom: 12px; }
.empty-state p { font-size: 14px; }
.age-warn { font-size: 10px; color: #F57C00; font-weight: 600; }
.toast {
position: fixed; bottom: 24px; right: 24px; background: #2E7D32; color: white;
padding: 12px 20px; border-radius: 6px; font-size: 13px; font-weight: 600;
box-shadow: 0 4px 12px rgba(0,0,0,0.2); transform: translateY(100px);
transition: transform 0.3s; z-index: 999;
}
.toast.show { transform: translateY(0); }
.header-buttons { display: flex; gap: 8px; align-items: center; }
.btn-header {
padding: 6px 12px; border-radius: 5px; font-size: 11px; font-weight: 600;
cursor: pointer; border: 1px solid rgba(255,255,255,0.3); transition: all 0.2s;
}
.btn-export { background: #43A047; color: white; }
.btn-export:hover { background: #388E3C; }
.btn-import { background: #1976D2; color: white; }
.btn-import:hover { background: #1565C0; }
</style>
</head>
<body>
<header>
<div>
<div class="logo">Defekt<span>Track</span></div>
<div class="header-sub">Lager & Logistik · Defekte Ware im Griff by Justin Klein</div>
</div>
<div class="header-buttons">
<button class="btn-header btn-export" onclick="exportData()">📤 Export</button>
<button class="btn-header btn-import" onclick="document.getElementById('import-file').click()">📥 Import</button>
<input type="file" id="import-file" accept=".json" style="display:none;" onchange="importData(event)">
</div>
</header>
<!-- DASHBOARD -->
<div class="dashboard">
<div class="stat-card red">
<div class="stat-number" id="cnt-offen">0</div>
<div class="stat-label">🔴 Offen</div>
</div>
<div class="stat-card yellow">
<div class="stat-number" id="cnt-bearbeitung">0</div>
<div class="stat-label">🟡 In Bearbeitung</div>
</div>
<div class="stat-card green">
<div class="stat-number" id="cnt-erledigt">0</div>
<div class="stat-label">🟢 Erledigt</div>
</div>
<div class="stat-card gray">
<div class="stat-number" id="cnt-entsorgt">0</div>
<div class="stat-label">⚫ Entsorgt</div>
</div>
<div class="stat-card blue">
<div class="stat-number" id="cnt-overdue">0</div>
<div class="stat-label">⚠️ Überfällig (&gt;7 Tage)</div>
</div>
</div>
<!-- MAIN -->
<div class="main">
<!-- FORMULAR -->
<div class="form-card">
<div class="form-header"> Defekte Ware erfassen</div>
<div class="form-body">
<div class="form-group">
<label>ERL-Nummer (Logistik) *</label>
<input type="text" id="f-erl" placeholder="z.B. ERL-00001">
</div>
<div class="form-group">
<label>Seriennummer *</label>
<input type="text" id="f-seriennummer" placeholder="z.B. SN-ABC123456">
</div>
<div class="form-group">
<label>Artikelnummer</label>
<input type="text" id="f-artikel" placeholder="z.B. ART-20341">
</div>
<div class="form-group">
<label>Bezeichnung</label>
<input type="text" id="f-bezeichnung" placeholder="z.B. Hydraulikpumpe XL">
</div>
<div class="form-group">
<label>Defektbeschreibung</label>
<textarea id="f-defekt" placeholder="Was genau ist defekt? Wie sieht der Schaden aus?"></textarea>
</div>
<div class="form-group">
<label>Lagerstandort</label>
<input type="text" id="f-standort" placeholder="z.B. Regal B-12 / Halle 3">
</div>
<div class="form-group">
<label>Zuständig *</label>
<input type="text" id="f-zustaendig" placeholder="Name des Mitarbeiters">
</div>
<div class="form-group">
<label>Priorität *</label>
<select id="f-prio">
<option value="niedrig">🟢 Niedrig</option>
<option value="mittel" selected>🟡 Mittel</option>
<option value="hoch">🟠 Hoch</option>
<option value="kritisch">🔴 Kritisch</option>
</select>
</div>
<div class="form-group">
<label>Kommentar</label>
<textarea id="f-kommentar" placeholder="*E-Mail Betreff* Notizen... (Betreff mit * markieren)"></textarea>
</div>
<button class="btn-submit" onclick="addArtikel()">✔ Ware erfassen</button>
</div>
</div>
<!-- TABELLE -->
<div class="table-card">
<div class="table-header">
<div class="table-title">📋 Alle erfassten Artikel</div>
<div class="filter-bar">
<input type="text" id="search" placeholder="🔍 ERL, SN, Artikel, *Betreff*..." oninput="renderTable()">
<select id="filter-status" onchange="renderTable()">
<option value="">Alle Status</option>
<option value="offen">Offen</option>
<option value="in_bearbeitung">In Bearbeitung</option>
<option value="erledigt">Erledigt</option>
<option value="entsorgt">Entsorgt</option>
</select>
<button class="btn-print" onclick="openPrintView()">🖨️ Drucken</button>
</div>
</div>
<div style="overflow-x:auto;">
<table>
<thead>
<tr>
<th>ERL-Nr.</th>
<th>Artikel</th>
<th>Seriennr.</th>
<th>Defekt</th>
<th>Standort</th>
<th>Status</th>
<th>Alter</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody id="table-body"></tbody>
</table>
<div class="empty-state" id="empty-state">
<div class="emoji">📦</div>
<p>Noch keine defekten Artikel erfasst.</p>
<p style="margin-top:8px;">Nutze das Formular links um den ersten Artikel einzutragen.</p>
</div>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
// ─── DATEN ───────────────────────────────────
let artikel = JSON.parse(localStorage.getItem('defekttrack') || '[]');
// Demo-Daten beim ersten Start
if (artikel.length === 0) {
const now = Date.now();
artikel = [
{ id: '1', erlNummer: 'ERL-00001', seriennummer: 'SN-HYD2024001', artikelNr: 'ART-1042', bezeichnung: 'Hydraulikpumpe XL', defekt: 'Ölleck am Anschluss', standort: 'Halle B / Regal 3', zustaendig: 'M. Weber', status: 'offen', prio: 'kritisch', kommentar: 'Sofortige Prüfung nötig', erstelltAm: now - 10 * 86400000 },
{ id: '2', erlNummer: 'ERL-00002', seriennummer: 'SN-MOT2024055', artikelNr: 'ART-2210', bezeichnung: 'Förderband-Motor', defekt: 'Lager defekt, lautes Geräusch', standort: 'Lager A', zustaendig: 'S. Klein', status: 'in_bearbeitung', prio: 'hoch', kommentar: '', erstelltAm: now - 4 * 86400000 },
{ id: '3', erlNummer: 'ERL-00003', seriennummer: 'SN-PCB2023189', artikelNr: 'ART-0055', bezeichnung: 'Steuerungsplatine', defekt: 'Kurzschluss nach Wassereinbruch', standort: 'Technikraum', zustaendig: 'T. Braun', status: 'erledigt', prio: 'mittel', kommentar: 'Ersatzteil bestellt', erstelltAm: now - 15 * 86400000 },
{ id: '4', erlNummer: 'ERL-00004', seriennummer: '', artikelNr: 'ART-0891', bezeichnung: 'Gabelstapler-Gabel', defekt: 'Riss in der Schweißnaht', standort: 'Fahrzeughalle', zustaendig: 'K. Müller', status: 'offen', prio: 'hoch', kommentar: '', erstelltAm: now - 2 * 86400000 },
{ id: '5', erlNummer: 'ERL-00005', seriennummer: 'SN-SCH2022044', artikelNr: 'ART-3300', bezeichnung: 'Lagerschiene Typ A', defekt: 'Verformt, nicht mehr nutzbar', standort: 'Regal C-08', zustaendig: '', status: 'entsorgt', prio: 'niedrig', kommentar: 'Wurde entsorgt', erstelltAm: now - 20 * 86400000 },
];
save();
}
function save() {
localStorage.setItem('defekttrack', JSON.stringify(artikel));
}
function getDaysOld(ts) {
return Math.floor((Date.now() - ts) / 86400000);
}
function isOverdue(a) {
return (a.status === 'offen' || a.status === 'in_bearbeitung') && getDaysOld(a.erstelltAm) > 7;
}
// ─── DASHBOARD ───────────────────────────────
function updateDashboard() {
document.getElementById('cnt-offen').textContent = artikel.filter(a => a.status === 'offen').length;
document.getElementById('cnt-bearbeitung').textContent = artikel.filter(a => a.status === 'in_bearbeitung').length;
document.getElementById('cnt-erledigt').textContent = artikel.filter(a => a.status === 'erledigt').length;
document.getElementById('cnt-entsorgt').textContent = artikel.filter(a => a.status === 'entsorgt').length;
document.getElementById('cnt-overdue').textContent = artikel.filter(isOverdue).length;
}
// ─── TABELLE ─────────────────────────────────
function renderTable() {
const search = document.getElementById('search').value.toLowerCase();
const filterStatus = document.getElementById('filter-status').value;
let filtered = artikel.filter(a => {
const matchSearch = !search ||
(a.erlNummer || '').toLowerCase().includes(search) ||
(a.seriennummer || '').toLowerCase().includes(search) ||
(a.artikelNr || '').toLowerCase().includes(search) ||
(a.bezeichnung || '').toLowerCase().includes(search) ||
(a.defekt || '').toLowerCase().includes(search) ||
(a.zustaendig || '').toLowerCase().includes(search) ||
(a.kommentar || '').toLowerCase().includes(search);
const matchStatus = !filterStatus || a.status === filterStatus;
return matchSearch && matchStatus;
});
const prioOrder = { kritisch: 0, hoch: 1, mittel: 2, niedrig: 3 };
filtered.sort((a, b) => (prioOrder[a.prio] ?? 4) - (prioOrder[b.prio] ?? 4));
const tbody = document.getElementById('table-body');
const emptyState = document.getElementById('empty-state');
if (filtered.length === 0) {
tbody.innerHTML = '';
emptyState.style.display = 'block';
return;
}
emptyState.style.display = 'none';
const statusMap = { offen: 'offen', in_bearbeitung: 'bearbeitung', erledigt: 'erledigt', entsorgt: 'entsorgt' };
const statusLabel = { offen: 'Offen', in_bearbeitung: 'In Bearbeitung', erledigt: 'Erledigt', entsorgt: 'Entsorgt' };
const nextStatus = { offen: 'in_bearbeitung', in_bearbeitung: 'erledigt', erledigt: 'entsorgt', entsorgt: 'offen' };
const nextLabel = { offen: '→ In Bearbeitung', in_bearbeitung: '→ Erledigt', erledigt: '→ Entsorgen', entsorgt: '→ Neu öffnen' };
tbody.innerHTML = filtered.map(a => {
const days = getDaysOld(a.erstelltAm);
const overdue = isOverdue(a);
const ageText = days === 0 ? 'Heute' : days === 1 ? '1 Tag' : `${days} Tage`;
const ageWarn = overdue ? `<br><span class="age-warn">⚠ Überfällig!</span>` : '';
const prioClass = 'prio-' + a.prio;
return `<tr class="${overdue ? 'overdue' : ''}">
<td>
<span class="prio-badge ${prioClass}"></span>
<strong style="color:#1565C0;">${a.erlNummer || ''}</strong>
</td>
<td>
<strong>${a.artikelNr}</strong><br>
<span style="font-size:12px;color:#555;">${a.bezeichnung}</span>
</td>
<td style="font-size:12px;font-family:monospace;">${a.seriennummer || ''}</td>
<td style="max-width:180px;font-size:12px;">${a.defekt}</td>
<td style="font-size:12px;">${a.standort || ''}</td>
<td><span class="badge badge-${statusMap[a.status]}">${statusLabel[a.status]}</span></td>
<td style="font-size:12px;">${ageText}${ageWarn}</td>
<td>
<button class="btn-action btn-status" onclick="changeStatus('${a.id}')">${nextLabel[a.status]}</button>
${a.kommentar ? `<button class="btn-action btn-info" onclick="showComment('${a.id}')">💬</button>` : ''}
<button class="btn-action btn-delete" onclick="deleteArtikel('${a.id}')">🗑</button>
</td>
</tr>`;
}).join('');
updateDashboard();
}
// ─── AKTIONEN ────────────────────────────────
function addArtikel() {
const erlNummer = document.getElementById('f-erl').value.trim();
const seriennummer = document.getElementById('f-seriennummer').value.trim();
const zustaendig = document.getElementById('f-zustaendig').value.trim();
if (!erlNummer || !seriennummer || !zustaendig) {
showToast('⚠️ Bitte ERL, Seriennummer und Zuständig ausfüllen!', '#C62828');
return;
}
const newItem = {
id: Date.now().toString(),
erlNummer,
seriennummer,
artikelNr: document.getElementById('f-artikel').value.trim(),
bezeichnung: document.getElementById('f-bezeichnung').value.trim(),
defekt: document.getElementById('f-defekt').value.trim(),
standort: document.getElementById('f-standort').value.trim(),
zustaendig,
prio: document.getElementById('f-prio').value,
kommentar: document.getElementById('f-kommentar').value.trim(),
status: 'offen',
erstelltAm: Date.now(),
};
artikel.unshift(newItem);
save();
renderTable();
showToast('✅ Artikel erfasst: ' + erlNummer);
// Felder leeren
['f-erl','f-seriennummer','f-artikel','f-bezeichnung','f-defekt','f-standort','f-zustaendig','f-kommentar'].forEach(id => {
document.getElementById(id).value = '';
});
}
function changeStatus(id) {
const nextStatus = { offen: 'in_bearbeitung', in_bearbeitung: 'erledigt', erledigt: 'entsorgt', entsorgt: 'offen' };
const a = artikel.find(x => x.id === id);
if (a) {
a.status = nextStatus[a.status];
save();
renderTable();
showToast('Status geändert → ' + a.status.replace('_', ' '));
}
}
function deleteArtikel(id) {
if (!confirm('Diesen Artikel wirklich löschen?')) return;
artikel = artikel.filter(x => x.id !== id);
save();
renderTable();
showToast('🗑 Artikel gelöscht', '#607D8B');
}
function showComment(id) {
const a = artikel.find(x => x.id === id);
if (!a || !a.kommentar) return;
let subject = '';
let text = a.kommentar;
const match = a.kommentar.match(/^\*([^*]+)\*/);
if (match) {
subject = match[1].trim();
text = a.kommentar.substring(match[0].length).trim();
}
const overlay = document.createElement('div');
overlay.className = 'comment-overlay';
overlay.onclick = () => { overlay.remove(); popup.remove(); };
const popup = document.createElement('div');
popup.className = 'comment-popup';
popup.innerHTML = `
<h3>💬 Kommentar zu ${a.erlNummer}</h3>
${subject ? `<div class="subject">📧 ${subject}</div>` : ''}
<div class="text">${text || '(Kein weiterer Kommentar)'}</div>
<button class="close-btn" onclick="this.parentElement.remove(); document.querySelector('.comment-overlay').remove();">Schließen</button>
`;
document.body.appendChild(overlay);
document.body.appendChild(popup);
}
function openPrintView() {
const search = document.getElementById('search').value.toLowerCase();
let filtered = artikel.filter(a => {
const isActive = a.status === 'offen' || a.status === 'in_bearbeitung';
const matchSearch = !search ||
(a.erlNummer || '').toLowerCase().includes(search) ||
(a.seriennummer || '').toLowerCase().includes(search) ||
(a.artikelNr || '').toLowerCase().includes(search) ||
(a.bezeichnung || '').toLowerCase().includes(search) ||
(a.defekt || '').toLowerCase().includes(search) ||
(a.zustaendig || '').toLowerCase().includes(search) ||
(a.kommentar || '').toLowerCase().includes(search);
return isActive && matchSearch;
});
const prioOrder = { kritisch: 0, hoch: 1, mittel: 2, niedrig: 3 };
filtered.sort((a, b) => (prioOrder[a.prio] ?? 4) - (prioOrder[b.prio] ?? 4));
if (filtered.length === 0) {
showToast('⚠️ Keine Artikel zum Drucken vorhanden!', '#C62828');
return;
}
const prioColors = { kritisch: '#C62828', hoch: '#F57C00', mittel: '#F9A825', niedrig: '#43A047' };
const prioLabels = { kritisch: 'Kritisch', hoch: 'Hoch', mittel: 'Mittel', niedrig: 'Niedrig' };
const rows = filtered.map(a => `
<tr>
<td>${a.erlNummer || ''}</td>
<td style="font-family: monospace;">${a.seriennummer || ''}</td>
<td>${a.defekt || ''}</td>
<td>
<span style="display:inline-block; width:10px; height:10px; border-radius:50%; background:${prioColors[a.prio]}; margin-right:6px;"></span>
${prioLabels[a.prio]}
</td>
</tr>
`).join('');
const printHTML = `
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>DefektTrack - Übersicht</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Segoe UI', Arial, sans-serif; padding: 30px; color: #333; }
h1 { font-size: 22px; margin-bottom: 5px; color: #1A2B4A; }
.subtitle { font-size: 12px; color: #888; margin-bottom: 20px; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th { background: #1A2B4A; color: white; padding: 10px 12px; text-align: left; font-size: 12px; text-transform: uppercase; }
td { padding: 10px 12px; border-bottom: 1px solid #ddd; font-size: 13px; }
tr:nth-child(even) { background: #f9f9f9; }
.footer { margin-top: 30px; font-size: 11px; color: #888; text-align: right; }
@media print {
body { padding: 15px; }
.no-print { display: none; }
}
</style>
</head>
<body>
<h1>DefektTrack Defekte Ware Übersicht</h1>
<div class="subtitle">Erstellt am: ${new Date().toLocaleDateString('de-DE')} um ${new Date().toLocaleTimeString('de-DE', {hour: '2-digit', minute: '2-digit'})} Uhr · ${filtered.length} Artikel</div>
<table>
<thead>
<tr>
<th>ERL-Nr.</th>
<th>Seriennummer</th>
<th>Defektbeschreibung</th>
<th>Priorität</th>
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>
<div class="footer">DefektTrack · Lager & Logistik</div>
<scr` + `ipt>window.onload = function() { window.print(); }</scr` + `ipt>
</body>
</html>
`;
const printWindow = window.open('', '_blank');
printWindow.document.write(printHTML);
printWindow.document.close();
}
// ─── EXPORT / IMPORT ─────────────────────────
function exportData() {
if (artikel.length === 0) {
showToast('⚠️ Keine Daten zum Exportieren!', '#C62828');
return;
}
const exportObj = {
version: '1.0',
exportedAt: Date.now(),
data: artikel
};
const blob = new Blob([JSON.stringify(exportObj, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const now = new Date();
const timestamp = now.toISOString().slice(0,10) + '-' + now.toTimeString().slice(0,8).replace(/:/g, '');
a.download = `defekttrack-${timestamp}.json`;
a.click();
URL.revokeObjectURL(url);
showToast(`📤 ${artikel.length} Einträge exportiert!`);
}
function importData(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const imported = JSON.parse(e.target.result);
const importedData = imported.data || imported;
if (!Array.isArray(importedData)) {
showToast('⚠️ Ungültiges Dateiformat!', '#C62828');
return;
}
let added = 0, updated = 0, kept = 0;
importedData.forEach(item => {
const existing = artikel.find(a => a.id === item.id);
if (!existing) {
artikel.push(item);
added++;
} else {
const existingTime = existing.erstelltAm || 0;
const importedTime = item.erstelltAm || 0;
if (importedTime > existingTime) {
Object.assign(existing, item);
updated++;
} else {
kept++;
}
}
});
artikel.sort((a, b) => (b.erstelltAm || 0) - (a.erstelltAm || 0));
save();
renderTable();
showToast(`📥 Import: +${added} neu, ${updated} aktualisiert, ${kept} unverändert`);
} catch (err) {
showToast('⚠️ Fehler beim Lesen der Datei!', '#C62828');
}
};
reader.readAsText(file);
event.target.value = '';
}
function showToast(msg, color = '#2E7D32') {
const t = document.getElementById('toast');
t.textContent = msg;
t.style.background = color;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 2800);
}
// Start
renderTable();
updateDashboard();
</script>
</body>
</html>