diff --git a/app.css b/app.css index 2579ad6..97a8196 100644 --- a/app.css +++ b/app.css @@ -439,6 +439,63 @@ body { .btn[disabled] { opacity: 0.5; cursor: not-allowed; } +/* PIN-Modal */ +.pin-modal .pin-body { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 24px; + background: #1a1a1f; +} +.pin-modal .pin-icon { font-size: 56px; margin-bottom: 12px; } +.pin-modal .pin-title { font-size: 18px; color: #e0e0e0; margin-bottom: 24px; } +.pin-modal .pin-display { + display: flex; gap: 16px; margin-bottom: 32px; +} +.pin-modal .pin-display span { + width: 18px; height: 18px; border-radius: 50%; + border: 2px solid #7aa2f7; background: transparent; + transition: background 0.1s; +} +.pin-modal .pin-display span.filled { background: #7aa2f7; } +.pin-modal .pin-keypad { + display: grid; + grid-template-columns: repeat(3, 72px); + gap: 12px; +} +.pin-modal .pin-key { + width: 72px; height: 72px; + border-radius: 50%; + background: #2a2a30; + color: #fff; + border: 1px solid #444; + font-size: 24px; + cursor: pointer; + -webkit-appearance: none; +} +.pin-modal .pin-key:active { background: #3a3a40; transform: scale(0.97); } +.pin-modal .pin-cancel { + margin-top: 32px; + background: transparent; + border: none; + color: #999; + cursor: pointer; + font-size: 14px; +} + +/* Toggle-Row für Settings */ +.toggle-row { + display: flex; align-items: center; gap: 10px; + padding: 10px 0; + cursor: pointer; +} +.toggle-row input[type="checkbox"] { + width: 18px; height: 18px; + accent-color: #337ab7; +} + /* Mini-Cards für Kundendetail (Aufträge/Rechnungen/Berichte) */ .mini-card { background: #2a2a30; @@ -472,7 +529,12 @@ body { } /* Report-Page-Thumb mit Nummer */ -.report-page-thumb { position: relative; } +.report-page-thumb { position: relative; transition: transform 0.1s, opacity 0.1s; } +.report-page-thumb.dragging { + opacity: 0.6; + transform: scale(1.05); + box-shadow: 0 4px 20px rgba(51, 122, 183, 0.5); +} .report-page-thumb .page-num { position: absolute; top: 4px; left: 4px; diff --git a/app.js b/app.js index 6fb676c..ffdebfc 100644 --- a/app.js +++ b/app.js @@ -39,6 +39,107 @@ async function ensureAuth() { return true; } +/* ============================================================ + * PIN-Schutz (optional, Settings → Sicherheit) + * ============================================================ */ +async function hashPin(pin) { + const salt = crypto.getRandomValues(new Uint8Array(16)); + const saltStr = Array.from(salt).map(b => b.toString(16).padStart(2, '0')).join(''); + const enc = new TextEncoder(); + const data = enc.encode(saltStr + ':' + pin); + const buf = await crypto.subtle.digest('SHA-256', data); + const hash = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join(''); + return { hash, salt: saltStr }; +} +async function verifyPin(pin) { + const salt = await idb.get('pin_salt'); + const stored = await idb.get('pin_hash'); + if (!salt || !stored) return false; + const enc = new TextEncoder(); + const data = enc.encode(salt + ':' + pin); + const buf = await crypto.subtle.digest('SHA-256', data); + const hash = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join(''); + return hash === stored; +} +function promptPin(title) { + return new Promise((resolve) => { + const modal = document.createElement('div'); + modal.className = 'fullscreen-modal pin-modal'; + modal.innerHTML = ` +
${escapeHtml(user.name || user.login || 'Unbekannt')}
${escapeHtml(user.login || '')}
PIN-Schutz beim App-Start — nützlich falls das Handy verloren geht.
+ + ${pinEnabled ? '' : ''} +Baustelle PWA v1.0
@@ -577,8 +689,43 @@ router.on('/settings', async () => { }; document.getElementById('btn-logout').onclick = async () => { await api.logout(); + await idb.del('pin_hash'); + await idb.del('pin_salt'); router.go('#/login'); }; + document.getElementById('pin-toggle').addEventListener('change', async (e) => { + if (e.target.checked) { + // PIN setzen + const pin = await promptNewPin(); + if (!pin) { e.target.checked = false; return; } + const { hash, salt } = await hashPin(pin); + await idb.set('pin_salt', salt); + await idb.set('pin_hash', hash); + showToast('✓ PIN gesetzt'); + router.navigate(); + } else { + if (!confirm('PIN-Schutz wirklich deaktivieren?')) { + e.target.checked = true; + return; + } + await idb.del('pin_hash'); + await idb.del('pin_salt'); + showToast('PIN-Schutz aus'); + router.navigate(); + } + }); + const changeBtn = document.getElementById('btn-pin-change'); + if (changeBtn) changeBtn.onclick = async () => { + const old = await promptPin('Aktuelle PIN eingeben'); + if (!old) return; + if (!(await verifyPin(old))) { showToast('Falsche PIN', 'error'); return; } + const neu = await promptNewPin(); + if (!neu) return; + const { hash, salt } = await hashPin(neu); + await idb.set('pin_salt', salt); + await idb.set('pin_hash', hash); + showToast('✓ PIN geändert'); + }; }); /* Bottom nav + Hilfe-Button */ @@ -590,6 +737,83 @@ document.addEventListener('DOMContentLoaded', () => { if (helpBtn) helpBtn.addEventListener('click', openHelpModal); }); +/** + * Bindet Tap + Long-Press-Drag an alle .report-page-thumb des Bericht-Details. + */ +function bindReportPageInteractions(reportId) { + const grid = document.getElementById('report-pages'); + if (!grid) return; + const thumbs = Array.from(grid.querySelectorAll('.report-page-thumb')); + + let longPressTimer = null; + let dragging = null; + let touchStart = null; + + thumbs.forEach(t => { + // Click/Tap → Aktion-Modal (nur wenn nicht gedraggt wurde) + t.addEventListener('click', (e) => { + if (dragging) { e.preventDefault(); return; } + openPageActionsModal(reportId, t.dataset.pageId, t.dataset.relpath, t.dataset.note || ''); + }); + + // Long-Press → Drag-Modus + const startLongPress = (e) => { + touchStart = e.touches ? { x: e.touches[0].clientX, y: e.touches[0].clientY } : null; + longPressTimer = setTimeout(() => { + dragging = t; + t.classList.add('dragging'); + showToast('Seite verschieben — ziehen und loslassen'); + if (navigator.vibrate) navigator.vibrate(50); + }, 500); + }; + const cancelLongPress = () => { + if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } + }; + + t.addEventListener('touchstart', startLongPress, { passive: true }); + t.addEventListener('touchmove', (e) => { + if (!dragging && touchStart) { + const dx = e.touches[0].clientX - touchStart.x; + const dy = e.touches[0].clientY - touchStart.y; + if (Math.abs(dx) > 8 || Math.abs(dy) > 8) cancelLongPress(); + } + if (dragging) { + e.preventDefault(); + // Welcher Thumb liegt unter dem Finger? + const tx = e.touches[0].clientX; + const ty = e.touches[0].clientY; + const below = document.elementFromPoint(tx, ty); + const target = below ? below.closest('.report-page-thumb') : null; + if (target && target !== dragging) { + const rect = target.getBoundingClientRect(); + const middle = rect.left + rect.width / 2; + if (tx < middle) grid.insertBefore(dragging, target); + else grid.insertBefore(dragging, target.nextSibling); + } + } + }, { passive: false }); + t.addEventListener('touchend', async () => { + cancelLongPress(); + if (dragging) { + dragging.classList.remove('dragging'); + const ids = Array.from(grid.querySelectorAll('.report-page-thumb')).map(x => x.dataset.pageId); + try { + await api.reorderPages(ids); + showToast('✓ Reihenfolge gespeichert'); + // Page-Nummer-Badges neu setzen + Array.from(grid.querySelectorAll('.report-page-thumb')).forEach((el, i) => { + const num = el.querySelector('.page-num'); + if (num) num.textContent = (i + 1); + }); + } catch (e) { showToast('Fehler: ' + e.message, 'error'); } + dragging = null; + } + touchStart = null; + }); + t.addEventListener('touchcancel', () => { cancelLongPress(); dragging = null; touchStart = null; }); + }); +} + /* ============================================================ * SEITEN-AKTIONEN MODAL (Notiz, Löschen, Vollbild) * ============================================================ */ @@ -1021,16 +1245,21 @@ async function openSketchEditor(orderId, imageUrl, sourceRelpath) {