feat: initial commit

This commit is contained in:
2026-03-08 08:34:55 +01:00
parent 3eb7c3ca8e
commit 43c9efd8f5
39 changed files with 13242 additions and 688 deletions

View File

@@ -1,690 +1,19 @@
<!DOCTYPE html>
<!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>
<head>
<meta charset="UTF-8" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,100..900&display=swap"
rel="stylesheet"
/>
<link rel="icon" type="image/svg+xml" href="/appwrite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DefektTrack Defekte Ware verwalten</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>