Lade deinen sicheren Bereich...

${content} `); printWindow.document.close(); printWindow.print(); } function downloadSpickzettel() { const content = document.getElementById('spickzettelContent'); const text = content.innerText; const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'TechHilfe_Spickzettel.txt'; a.click(); URL.revokeObjectURL(url); } // ── ACTIVITY TIMELINE ── function openTimeline() { const modal = document.getElementById('timelineModal'); modal.classList.add('active'); renderTimeline(); } function renderTimeline() { const container = document.getElementById('timelineContent'); const bookings = window.customerBookings || []; const reviews = window.customerReviews || []; const devices = window.customerDevices || []; // Build unified event list const events = []; bookings.forEach(b => { const date = b.created_at || b.date; events.push({ date: date, type: 'booking', icon: b.status === 'completed' ? 'green' : b.status === 'cancelled' ? 'red' : b.status === 'confirmed' ? 'blue' : 'orange', title: b.service, subtitle: getStatusLabel(b.status) + ' · ' + (b.booking_number || ''), extra: b.date ? formatDateDE(b.date) + (b.time ? ' ' + b.time + ' Uhr' : '') : '' }); }); reviews.forEach(r => { events.push({ date: r.created_at, type: 'review', icon: 'green', title: '⭐ Bewertung abgegeben (' + (r.rating || 5) + '/5)', subtitle: (r.text || r.comment || '').substring(0, 60) + ((r.text || r.comment || '').length > 60 ? '...' : ''), extra: '' }); }); devices.forEach(d => { events.push({ date: d.created_at || new Date().toISOString(), type: 'device', icon: 'blue', title: '💻 Gerät hinzugefügt: ' + d.device_name, subtitle: d.brand || d.category || '', extra: '' }); }); // Sort newest first events.sort((a, b) => new Date(b.date || 0) - new Date(a.date || 0)); if (events.length === 0) { container.innerHTML = `

Noch keine Aktivitäten vorhanden.

`; return; } container.innerHTML = `
${events.slice(0, 20).map(e => { const dateStr = e.date ? new Date(e.date).toLocaleDateString('de-DE', { day: '2-digit', month: 'short', year: 'numeric' }) : ''; return `
${e.title}
${e.subtitle ? `
${e.subtitle}
` : ''}
${dateStr}${e.extra ? ' · ' + e.extra : ''}
`; }).join('')}
`; } function getStatusLabel(status) { switch (status) { case 'pending': return '⏳ Wartend'; case 'confirmed': return '✅ Bestätigt'; case 'completed': return '🎉 Abgeschlossen'; case 'cancelled': return '❌ Storniert'; default: return status; } } function formatDateDE(dateStr) { if (!dateStr) return ''; const parts = dateStr.split('-'); if (parts.length !== 3) return dateStr; const d = new Date(parts[0], parts[1] - 1, parts[2]); return d.toLocaleDateString('de-DE', { day: '2-digit', month: 'short', year: 'numeric' }); } // ── PULL TO REFRESH LOGIC ── let ptrStartY = 0; let ptrCurrentY = 0; const ptrIndicator = document.getElementById('pullToRefreshIndicator'); let isPtrRefreshing = false; document.addEventListener('touchstart', (e) => { if (window.scrollY === 0) { ptrStartY = e.touches[0].clientY; } else { ptrStartY = 0; } }, { passive: true }); document.addEventListener('touchmove', (e) => { if (!ptrStartY || isPtrRefreshing) return; ptrCurrentY = e.touches[0].clientY; const diff = ptrCurrentY - ptrStartY; if (diff > 0 && window.scrollY === 0) { const opacity = Math.min(diff / 100, 1); ptrIndicator.style.opacity = opacity; ptrIndicator.style.transform = `translateY(${Math.min(diff / 2, 40)}px)`; } }, { passive: true }); document.addEventListener('touchend', (e) => { if (!ptrStartY || isPtrRefreshing) return; const diff = ptrCurrentY - ptrStartY; if (diff > 80 && window.scrollY === 0) { isPtrRefreshing = true; ptrIndicator.classList.add('refreshing'); ptrIndicator.style.transform = ''; ptrIndicator.style.opacity = '1'; // Refresh Dashboard Data directly setTimeout(async () => { const session = JSON.parse(localStorage.getItem('techhilfe_ticket_session')); if (session) await loadDashboard(session); ptrIndicator.classList.remove('refreshing'); ptrIndicator.style.opacity = '0'; isPtrRefreshing = false; ptrStartY = 0; }, 800); } else { ptrIndicator.style.transform = `translateY(-100%)`; ptrIndicator.style.opacity = '0'; ptrStartY = 0; ptrCurrentY = 0; } }); // ── PWA INSTALL PROMPT LOGIC ── let deferredPrompt; window.addEventListener('beforeinstallprompt', (e) => { // Prevent the mini-infobar from appearing on mobile e.preventDefault(); // Stash the event so it can be triggered later. deferredPrompt = e; // Show the custom install banner const installBanner = document.getElementById('pwaInstallBanner'); if (installBanner) { installBanner.style.display = 'flex'; // Add click event to the install button const installBtn = document.getElementById('pwaInstallBtn'); installBtn.addEventListener('click', async () => { // Hide the app provided install promotion installBanner.style.display = 'none'; // Show the install prompt deferredPrompt.prompt(); // Wait for the user to respond to the prompt const { outcome } = await deferredPrompt.userChoice; console.log(`User response to the install prompt: ${outcome}`); // We've used the prompt, and can't use it again, throw it away deferredPrompt = null; }); } }); window.addEventListener('appinstalled', () => { // Handle post-install logic const installBanner = document.getElementById('pwaInstallBanner'); if (installBanner) installBanner.style.display = 'none'; console.log('PWA was installed'); }); // ── PUSH NOTIFICATION SETUP ── async function setupPushNotifications() { if (!('serviceWorker' in navigator) || !('PushManager' in window)) return; try { const registration = await navigator.serviceWorker.register('/sw.js'); // Let's ask for permission and subscribe if granted const permission = await Notification.requestPermission(); if (permission === 'granted') { // Fetch VAPID public key const pkResp = await fetch(window.apiUrl('/api/push?action=public_key')); if (!pkResp.ok) return; const { publicKey } = await pkResp.json(); if (!publicKey) return; const applicationServerKey = formatVapidKey(publicKey); const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey }); // Send to our backend const sessionParams = JSON.parse(localStorage.getItem('techhilfe_ticket_session')); if (sessionParams && sessionParams.email) { await fetch(window.apiUrl('/api/push'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'subscribe', email: sessionParams.email, subscription: subscription }) }); } } } catch (err) { console.error("Push registration error:", err); } } function formatVapidKey(base64String) { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/'); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i]=rawData.charCodeAt(i); } return outputArray; } window.addEventListener('load', ()=> { setTimeout(setupPushNotifications, 2000); });