691 lines
27 KiB
HTML
691 lines
27 KiB
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 (>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>
|