/** * KundenKarte PWA - Mobile Schaltschrank-Dokumentation * Offline-First App für Elektriker */ (function($) { 'use strict'; // ============================================ // APP STATE // ============================================ const App = { // Auth token: null, user: null, // Current selection customerId: null, customerName: '', anlageId: null, anlageName: '', // Data panels: [], carriers: [], equipment: [], equipmentTypes: [], outputs: [], // Offline queue offlineQueue: [], isOnline: navigator.onLine, // Current modal state currentCarrierId: null, selectedTypeId: null, // Abgang-Labels: 'top' (Standard, wie echtes Panel) oder 'bottom' labelsPosition: localStorage.getItem('kundenkarte_labels_pos') || 'top', }; // ============================================ // INIT // ============================================ function init() { // Register Service Worker if ('serviceWorker' in navigator) { navigator.serviceWorker.register('sw.js') .then(reg => console.log('[PWA] Service Worker registered')) .catch(err => console.error('[PWA] SW registration failed:', err)); } // Check online status window.addEventListener('online', () => { App.isOnline = true; hideOfflineBar(); syncOfflineChanges(); }); window.addEventListener('offline', () => { App.isOnline = false; showOfflineBar(); }); // Check stored auth const storedToken = localStorage.getItem('kundenkarte_pwa_token'); const storedUser = localStorage.getItem('kundenkarte_pwa_user'); if (storedToken && storedUser) { App.token = storedToken; App.user = JSON.parse(storedUser); // Letzten Zustand wiederherstellen const lastState = JSON.parse(sessionStorage.getItem('kundenkarte_pwa_state') || 'null'); if (lastState && lastState.screen) { if (lastState.customerId) { App.customerId = lastState.customerId; App.customerName = lastState.customerName || ''; $('#customer-name').text(App.customerName); } if (lastState.anlageId) { App.anlageId = lastState.anlageId; App.anlageName = lastState.anlageName || ''; $('#anlage-name').text(App.anlageName); } // Screen wiederherstellen if (lastState.screen === 'editor' && App.anlageId) { showScreen('editor'); loadEditorData(); } else if (lastState.screen === 'anlagen' && App.customerId) { showScreen('anlagen'); reloadAnlagen(); } else { showScreen('search'); } } else { showScreen('search'); } } // Initialen History-State setzen history.replaceState({ screen: $('.screen.active').attr('id')?.replace('screen-', '') || 'login' }, ''); // Load offline queue const storedQueue = localStorage.getItem('kundenkarte_offline_queue'); if (storedQueue) { App.offlineQueue = JSON.parse(storedQueue); updateSyncBadge(); } // Bind events bindEvents(); } // ============================================ // EVENTS // ============================================ function bindEvents() { // Login $('#login-form').on('submit', handleLogin); $('#btn-logout').on('click', handleLogout); // Navigation $('#btn-back-search').on('click', () => history.back()); $('#btn-back-anlagen').on('click', () => history.back()); // Browser/Hardware Zurück-Button window.addEventListener('popstate', function(e) { if (e.state && e.state.screen) { showScreen(e.state.screen, true); } else { // Kein State = zurück zum Anfang const activeScreen = App.token ? 'search' : 'login'; showScreen(activeScreen, true); } }); // Search $('#search-customer').on('input', debounce(handleSearch, 300)); // Customer/Anlage selection $('#customer-list').on('click', '.list-item', handleCustomerSelect); $('#anlagen-list').on('click', '.anlage-card', handleAnlageSelect); $('#anlagen-list').on('click', '.contact-group-header', handleContactGroupClick); // Editor actions $('#btn-add-panel').on('click', () => openModal('add-panel')); $('#btn-save-panel').on('click', handleSavePanel); $('#editor-content').on('click', '.btn-add-carrier', handleAddCarrier); $('#btn-save-carrier').on('click', handleSaveCarrier); $('#editor-content').on('click', '.btn-add-equipment', handleAddEquipment); $('#editor-content').on('click', '.equipment-block', handleEquipmentClick); // Equipment modal $('#type-grid').on('click', '.type-btn', handleTypeSelect); $('#btn-save-equipment').on('click', handleSaveEquipment); $('#btn-cancel-equipment').on('click', () => closeModal('add-equipment')); // TE buttons $('.te-btn').on('click', function() { $('.te-btn').removeClass('selected'); $(this).addClass('selected'); }); // Modal close $('.modal-close').on('click', function() { $(this).closest('.modal').removeClass('active'); }); // Sync button $('#btn-sync').on('click', handleRefresh); // Abgang-Labels Toggle (oben/unten) $('#btn-toggle-labels').on('click', function() { App.labelsPosition = App.labelsPosition === 'top' ? 'bottom' : 'top'; localStorage.setItem('kundenkarte_labels_pos', App.labelsPosition); $(this).removeClass('labels-top labels-bottom').addClass('labels-' + App.labelsPosition); renderEditor(); }); // Initialen Toggle-Zustand setzen $('#btn-toggle-labels').removeClass('labels-top labels-bottom').addClass('labels-' + App.labelsPosition); } // ============================================ // AUTH // ============================================ async function handleLogin(e) { e.preventDefault(); const user = $('#login-user').val().trim(); const pass = $('#login-pass').val(); if (!user || !pass) { $('#login-error').text('Bitte Benutzername und Passwort eingeben'); return; } $('#login-error').text(''); try { const response = await apiCall('pwa_auth.php', { action: 'login', username: user, password: pass }); if (response.success) { App.token = response.token; App.user = response.user; localStorage.setItem('kundenkarte_pwa_token', response.token); localStorage.setItem('kundenkarte_pwa_user', JSON.stringify(response.user)); showScreen('search'); } else { $('#login-error').text(response.error || 'Login fehlgeschlagen'); } } catch (err) { $('#login-error').text('Verbindungsfehler'); } } function handleLogout() { App.token = null; App.user = null; App.customerId = null; App.customerName = ''; App.anlageId = null; App.anlageName = ''; localStorage.removeItem('kundenkarte_pwa_token'); localStorage.removeItem('kundenkarte_pwa_user'); sessionStorage.removeItem('kundenkarte_pwa_state'); showScreen('login'); } // ============================================ // SCREENS // ============================================ function showScreen(name, skipHistory) { $('.screen').removeClass('active'); $('#screen-' + name).addClass('active'); // Browser-History für Zurück-Button if (!skipHistory) { history.pushState({ screen: name }, '', '#' + name); } // State speichern für Refresh-Wiederherstellung saveState(name); } // Zustand in sessionStorage speichern function saveState(screen) { const state = { screen: screen || 'search', customerId: App.customerId, customerName: App.customerName, anlageId: App.anlageId, anlageName: App.anlageName }; sessionStorage.setItem('kundenkarte_pwa_state', JSON.stringify(state)); } // Anlagen-Liste für aktuellen Kunden neu laden async function reloadAnlagen() { if (!App.customerId) return; $('#anlagen-list').html('
'); try { const response = await apiCall('ajax/pwa_api.php', { action: 'get_anlagen', customer_id: App.customerId }); if (response.success) { renderAnlagenList(response.anlagen, response.contacts || []); localStorage.setItem('kundenkarte_anlagen_' + App.customerId, JSON.stringify({ anlagen: response.anlagen, contacts: response.contacts || [] })); } else { $('#anlagen-list').html('
Keine Anlagen gefunden
'); } } catch (err) { // Gecachte Daten verwenden const cached = localStorage.getItem('kundenkarte_anlagen_' + App.customerId); if (cached) { const data = JSON.parse(cached); renderAnlagenList(data.anlagen || data, data.contacts || []); showToast('Offline - Zeige gecachte Daten', 'warning'); } else { $('#anlagen-list').html('
Fehler beim Laden
'); } } } // ============================================ // CUSTOMER SEARCH // ============================================ async function handleSearch() { const query = $('#search-customer').val().trim(); if (query.length < 2) { $('#customer-list').html('
Mindestens 2 Zeichen eingeben...
'); return; } $('#customer-list').html('
'); try { const response = await apiCall('ajax/pwa_api.php', { action: 'search_customers', query: query }); if (response.success && response.customers) { renderCustomerList(response.customers); } else { $('#customer-list').html('
Keine Kunden gefunden
'); } } catch (err) { $('#customer-list').html('
Fehler bei der Suche
'); } } function renderCustomerList(customers) { if (!customers.length) { $('#customer-list').html('
Keine Kunden gefunden
'); return; } let html = ''; customers.forEach(c => { html += `
${escapeHtml(c.name)}
${escapeHtml(c.town || '')}
`; }); $('#customer-list').html(html); } // ============================================ // CUSTOMER & ANLAGE SELECTION // ============================================ async function handleCustomerSelect() { const id = $(this).data('id'); const name = $(this).find('.list-item-title').text(); App.customerId = id; App.customerName = name; $('#customer-name').text(name); showScreen('anlagen'); $('#anlagen-list').html('
'); try { const response = await apiCall('ajax/pwa_api.php', { action: 'get_anlagen', customer_id: id }); if (response.success) { renderAnlagenList(response.anlagen, response.contacts || []); // Cache für Offline localStorage.setItem('kundenkarte_anlagen_' + id, JSON.stringify({ anlagen: response.anlagen, contacts: response.contacts || [] })); } else { $('#anlagen-list').html('
Keine Anlagen gefunden
'); } } catch (err) { // Try cached const cached = localStorage.getItem('kundenkarte_anlagen_' + id); if (cached) { const data = JSON.parse(cached); renderAnlagenList(data.anlagen || data, data.contacts || []); showToast('Offline - Zeige gecachte Daten', 'warning'); } else { $('#anlagen-list').html('
Fehler beim Laden
'); } } } function renderAnlagenList(anlagen, contacts) { let html = ''; // Kunden-Anlagen (ohne Kontaktzuweisung) if (anlagen && anlagen.length) { anlagen.forEach(a => { html += renderAnlageCard(a); }); } // Kontakt-Adressen als Gruppen if (contacts && contacts.length) { contacts.forEach(c => { const subtitle = [c.address, c.town].filter(Boolean).join(', '); html += `
${escapeHtml(c.name)}
${subtitle ? '
' + escapeHtml(subtitle) + '
' : ''}
${c.anlage_count}
`; }); } if (!html) { $('#anlagen-list').html('
Keine Anlagen gefunden
'); return; } $('#anlagen-list').html(html); } function renderAnlageCard(a) { return `
${escapeHtml(a.label || 'Anlage ' + a.id)}
${a.type ? '
' + escapeHtml(a.type) + '
' : ''}
`; } async function handleAnlageSelect() { const id = $(this).data('id'); const name = $(this).find('.anlage-card-title').text(); App.anlageId = id; App.anlageName = name; $('#anlage-name').text(name); showScreen('editor'); await loadEditorData(); } // ============================================ // CONTACT GROUP EXPAND/COLLAPSE // ============================================ async function handleContactGroupClick() { const $group = $(this).closest('.contact-group'); const $list = $group.find('.contact-anlagen-list'); const contactId = $group.data('contact-id'); const customerId = $group.data('customer-id'); // Toggle anzeigen/verstecken if ($group.hasClass('expanded')) { $group.removeClass('expanded'); return; } $group.addClass('expanded'); $list.html('
'); try { const response = await apiCall('ajax/pwa_api.php', { action: 'get_contact_anlagen', customer_id: customerId, contact_id: contactId }); if (response.success && response.anlagen && response.anlagen.length) { let html = ''; response.anlagen.forEach(a => { html += renderAnlageCard(a); }); $list.html(html); } else { $list.html('
Keine Anlagen
'); } } catch (err) { $list.html('
Fehler beim Laden
'); } } // ============================================ // EDITOR // ============================================ async function loadEditorData() { $('#editor-content').html('
Lade Daten...
'); try { const response = await apiCall('ajax/pwa_api.php', { action: 'get_anlage_data', anlage_id: App.anlageId }); if (response.success) { App.panels = response.panels || []; App.carriers = response.carriers || []; App.equipment = response.equipment || []; App.equipmentTypes = response.types || []; App.outputs = response.outputs || []; // Cache for offline localStorage.setItem('kundenkarte_data_' + App.anlageId, JSON.stringify({ panels: App.panels, carriers: App.carriers, equipment: App.equipment, types: App.equipmentTypes, outputs: App.outputs })); renderEditor(); } } catch (err) { // Try cached const cached = localStorage.getItem('kundenkarte_data_' + App.anlageId); if (cached) { const data = JSON.parse(cached); App.panels = data.panels || []; App.carriers = data.carriers || []; App.equipment = data.equipment || []; App.equipmentTypes = data.types || []; App.outputs = data.outputs || []; renderEditor(); showToast('Offline - Zeige gecachte Daten', 'warning'); } else { $('#editor-content').html('
Fehler beim Laden
'); } } } function renderEditor() { if (!App.panels.length) { $('#editor-content').html('
Noch keine Felder angelegt.
Tippe auf "+ Feld" um zu beginnen.
'); return; } let html = ''; App.panels.forEach(panel => { const panelCarriers = App.carriers.filter(c => c.fk_panel == panel.id); html += `
${escapeHtml(panel.label || 'Feld ' + panel.id)}
`; panelCarriers.forEach(carrier => { const carrierEquipment = App.equipment.filter(e => e.fk_carrier == carrier.id); carrierEquipment.sort((a, b) => (a.position_te || 0) - (b.position_te || 0)); const totalTe = parseInt(carrier.total_te) || 12; const usedTe = carrierEquipment.reduce((sum, eq) => sum + (parseInt(eq.width_te) || 1), 0); const isFull = usedTe >= totalTe; const labelsTop = App.labelsPosition === 'top'; // Abgang-Labels aus Connections (output_label + Kabeltyp) generieren let labelsHtml = `
`; carrierEquipment.forEach(eq => { const widthTe = parseInt(eq.width_te) || 1; const posTe = parseInt(eq.position_te) || 0; const gridCol = posTe > 0 ? `grid-column: ${posTe} / span ${widthTe}` : `grid-column: span ${widthTe}`; // Abgang aus equipment_connection (fk_target IS NULL) const output = App.outputs ? App.outputs.find(o => o.fk_source == eq.id) : null; labelsHtml += `
`; if (output && output.output_label) { // Kabelinfo zusammenbauen (wie Website) let cableInfo = ''; if (output.medium_type) cableInfo = output.medium_type; if (output.medium_spec) cableInfo += ' ' + output.medium_spec; labelsHtml += ``; labelsHtml += escapeHtml(output.output_label); if (cableInfo) labelsHtml += `
${escapeHtml(cableInfo.trim())}`; labelsHtml += `
`; } labelsHtml += `
`; }); labelsHtml += `
`; html += `
${escapeHtml(carrier.label || 'Hutschiene')} ${usedTe}/${totalTe} TE
`; // Labels oben if (labelsTop) html += labelsHtml; html += `
`; carrierEquipment.forEach(eq => { const type = App.equipmentTypes.find(t => t.id == eq.fk_equipment_type); const widthTe = parseInt(eq.width_te) || 1; const posTe = parseInt(eq.position_te) || 0; // Wie Website: Zeile 1 = Typ-Kurzname, Zeile 2 = Feldwerte, Zeile 3 = Bezeichnung const typeLabel = type?.label_short || type?.ref || ''; const blockColor = eq.block_color || type?.color || '#3498db'; const eqLabel = eq.label || ''; // block_label kann = type_label_short sein wenn keine Feldwerte vorhanden // Nur anzeigen wenn es echte Feldwerte sind (nicht gleich dem Typ-Kurznamen) const blockFields = eq.block_label || ''; const showBlockFields = blockFields && blockFields !== typeLabel && blockFields !== (type?.ref || ''); const gridCol = posTe > 0 ? `grid-column: ${posTe} / span ${widthTe}` : `grid-column: span ${widthTe}`; html += `
${escapeHtml(typeLabel)} ${showBlockFields ? `${escapeHtml(blockFields)}` : ''} ${escapeHtml(eqLabel)}
`; }); html += `
`; html += ` `; html += `
`; // Labels unten if (!labelsTop) html += labelsHtml; html += `
`; }); html += `
`; }); $('#editor-content').html(html); // Load type grid renderTypeGrid(); } function renderTypeGrid() { let html = ''; App.equipmentTypes.forEach(type => { html += ` `; }); $('#type-grid').html(html); } // ============================================ // PANEL (FELD) ACTIONS // ============================================ async function handleSavePanel() { const label = $('#panel-label').val().trim() || 'Feld ' + (App.panels.length + 1); const data = { action: 'create_panel', anlage_id: App.anlageId, label: label }; closeModal('add-panel'); $('#panel-label').val(''); if (App.isOnline) { try { const response = await apiCall('ajax/pwa_api.php', data); if (response.success) { App.panels.push({ id: response.panel_id, label: label }); renderEditor(); showToast('Feld angelegt'); } } catch (err) { queueOfflineAction(data); } } else { queueOfflineAction(data); // Optimistic UI App.panels.push({ id: 'temp_' + Date.now(), label: label }); renderEditor(); showToast('Feld wird synchronisiert...', 'warning'); } } // ============================================ // CARRIER (HUTSCHIENE) ACTIONS // ============================================ function handleAddCarrier() { const panelId = $(this).data('panel-id'); App.currentPanelId = panelId; $('.te-btn').removeClass('selected'); $('#carrier-label').val(''); openModal('add-carrier'); } async function handleSaveCarrier() { const teBtn = $('.te-btn.selected'); if (!teBtn.length) { showToast('Bitte Größe wählen', 'error'); return; } const totalTe = parseInt(teBtn.data('te')); const label = $('#carrier-label').val().trim() || 'Hutschiene'; const data = { action: 'create_carrier', panel_id: App.currentPanelId, total_te: totalTe, label: label }; closeModal('add-carrier'); if (App.isOnline) { try { const response = await apiCall('ajax/pwa_api.php', data); if (response.success) { App.carriers.push({ id: response.carrier_id, fk_panel: App.currentPanelId, total_te: totalTe, label: label }); renderEditor(); showToast('Hutschiene angelegt'); } } catch (err) { queueOfflineAction(data); } } else { queueOfflineAction(data); App.carriers.push({ id: 'temp_' + Date.now(), fk_panel: App.currentPanelId, total_te: totalTe, label: label }); renderEditor(); showToast('Wird synchronisiert...', 'warning'); } } // ============================================ // EQUIPMENT (AUTOMAT) ACTIONS // ============================================ function handleAddEquipment() { const carrierId = $(this).data('carrier-id'); App.currentCarrierId = carrierId; App.selectedTypeId = null; // Reset modal $('.type-btn').removeClass('selected'); $('#step-values').hide(); $('#equipment-label').val(''); $('#value-fields').html(''); openModal('add-equipment'); } function handleTypeSelect() { $('.type-btn').removeClass('selected'); $(this).addClass('selected'); App.selectedTypeId = $(this).data('type-id'); const type = App.equipmentTypes.find(t => t.id == App.selectedTypeId); // Werte-Bereich einblenden $('#step-values').show(); // Felder basierend auf Typ aufbauen let html = ''; // Quick-Select für LS-Schalter if (type && (type.ref?.includes('LS') || type.label?.includes('Leitungsschutz'))) { html += '

Kennlinie + Ampere:

'; html += '
'; ['B6', 'B10', 'B13', 'B16', 'B20', 'B25', 'B32', 'C6', 'C10', 'C13', 'C16', 'C20', 'C25', 'C32'].forEach(v => { html += ``; }); html += '
'; // Quick-Select für FI-Schalter } else if (type && (type.ref?.includes('FI') || type.label?.includes('RCD'))) { html += '

Ampere:

'; html += '
'; ['25', '40', '63', '80'].forEach(v => { html += ``; }); html += '
'; html += '

Empfindlichkeit:

'; html += '
'; ['30', '100', '300'].forEach(v => { html += ``; }); html += '
'; // Quick-Select für AFDD } else if (type && type.ref?.includes('AFDD')) { html += '

Ampere:

'; html += '
'; ['10', '13', '16', '20', '25', '32'].forEach(v => { html += ``; }); html += '
'; // Quick-Select für FI/LS-Kombi } else if (type && type.ref?.includes('FILS')) { html += '

Kennlinie + Ampere:

'; html += '
'; ['B10', 'B13', 'B16', 'B20', 'B25', 'B32'].forEach(v => { html += ``; }); html += '
'; } $('#value-fields').html(html); // Chip-Klick-Handler $('#value-fields .value-chip').on('click', function() { if ($(this).hasClass('chip-sens')) { $('.chip-sens').removeClass('selected'); } else { $('.value-chip:not(.chip-sens)').removeClass('selected'); } $(this).addClass('selected'); }); // Focus auf Label-Feld wenn keine Chips vorhanden if (!html) { $('#equipment-label').focus(); } } async function handleSaveEquipment() { if (!App.selectedTypeId) { showToast('Bitte Typ wählen', 'error'); return; } const type = App.equipmentTypes.find(t => t.id == App.selectedTypeId); const label = $('#equipment-label').val().trim(); // Collect field values const fieldValues = {}; const selectedChip = $('.value-chip.selected:not(.chip-sens)'); const selectedSens = $('.chip-sens.selected'); if (selectedChip.length) { if (selectedChip.data('char')) fieldValues.characteristic = selectedChip.data('char'); if (selectedChip.data('amp')) fieldValues.ampere = selectedChip.data('amp'); } if (selectedSens.length) { fieldValues.sensitivity = selectedSens.data('sens'); } // Nächste freie Position berechnen (Lücken berücksichtigen) const carrierEquipment = App.equipment.filter(e => e.fk_carrier == App.currentCarrierId); const carrier = App.carriers.find(c => c.id == App.currentCarrierId); const totalTe = parseInt(carrier?.total_te) || 12; const eqWidth = parseInt(type?.width_te) || 1; // Belegungsarray erstellen const occupied = new Array(totalTe + 1).fill(false); carrierEquipment.forEach(e => { const pos = parseInt(e.position_te) || 1; const w = parseInt(e.width_te) || 1; for (let i = pos; i < pos + w && i <= totalTe; i++) { occupied[i] = true; } }); // Erste Lücke finden die breit genug ist let nextPos = 0; for (let i = 1; i <= totalTe - eqWidth + 1; i++) { let fits = true; for (let j = 0; j < eqWidth; j++) { if (occupied[i + j]) { fits = false; break; } } if (fits) { nextPos = i; break; } } if (nextPos === 0) { showToast('Kein Platz frei', 'error'); return; } const data = { action: 'create_equipment', carrier_id: App.currentCarrierId, type_id: App.selectedTypeId, label: label, position_te: nextPos, field_values: JSON.stringify(fieldValues) }; closeModal('add-equipment'); if (App.isOnline) { try { const response = await apiCall('ajax/pwa_api.php', data); if (response.success) { App.equipment.push({ id: response.equipment_id, fk_carrier: App.currentCarrierId, fk_equipment_type: App.selectedTypeId, label: label, position_te: nextPos, width_te: type?.width_te || 1, field_values: fieldValues }); renderEditor(); showToast('Automat angelegt', 'success'); } else { showToast(response.error || 'Fehler beim Speichern', 'error'); } } catch (err) { showToast('Netzwerkfehler - wird offline gespeichert', 'warning'); queueOfflineAction(data); } } else { queueOfflineAction(data); App.equipment.push({ id: 'temp_' + Date.now(), fk_carrier: App.currentCarrierId, fk_equipment_type: App.selectedTypeId, label: label, position_te: nextPos, width_te: type?.width_te || 1, field_values: fieldValues }); renderEditor(); showToast('Wird synchronisiert...', 'warning'); } } function handleEquipmentClick() { const eqId = $(this).data('equipment-id'); // TODO: Edit/Delete popup showToast('Bearbeiten kommt noch...'); } // ============================================ // OFFLINE SYNC // ============================================ function queueOfflineAction(data) { data._timestamp = Date.now(); App.offlineQueue.push(data); localStorage.setItem('kundenkarte_offline_queue', JSON.stringify(App.offlineQueue)); updateSyncBadge(); } function updateSyncBadge() { const count = App.offlineQueue.length; const $badge = $('#sync-badge'); if (count > 0) { $badge.text(count).removeClass('hidden'); } else { $badge.addClass('hidden'); } } async function handleRefresh() { // Zuerst Offline-Queue syncen falls vorhanden if (App.offlineQueue.length && App.isOnline) { await syncOfflineChanges(); } // Dann Daten neu laden showToast('Aktualisiere...'); await loadEditorData(); } async function syncOfflineChanges() { if (!App.offlineQueue.length) { showToast('Alles synchronisiert'); return; } if (!App.isOnline) { showToast('Offline - Sync nicht möglich', 'error'); return; } showToast('Synchronisiere...'); const queue = [...App.offlineQueue]; let successCount = 0; for (const data of queue) { try { const response = await apiCall('ajax/pwa_api.php', data); if (response.success) { successCount++; // Remove from queue const idx = App.offlineQueue.findIndex(q => q._timestamp === data._timestamp); if (idx > -1) App.offlineQueue.splice(idx, 1); } } catch (err) { console.error('Sync failed for:', data, err); } } localStorage.setItem('kundenkarte_offline_queue', JSON.stringify(App.offlineQueue)); updateSyncBadge(); if (successCount === queue.length) { showToast('Alle Änderungen synchronisiert', 'success'); // Reload data loadEditorData(); } else { showToast(`${successCount}/${queue.length} synchronisiert`, 'warning'); } } // ============================================ // API HELPER // ============================================ async function apiCall(endpoint, data = {}) { const url = window.MODULE_URL + '/' + endpoint; // Add token if (App.token) { data.token = App.token; } const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams(data) }); if (!response.ok) { throw new Error('Network error'); } return response.json(); } // ============================================ // UI HELPERS // ============================================ function openModal(name) { $('#modal-' + name).addClass('active'); } function closeModal(name) { $('#modal-' + name).removeClass('active'); } function showToast(message, type = '') { const $toast = $('#toast'); $toast.text(message).removeClass('success error warning visible').addClass(type); setTimeout(() => $toast.addClass('visible'), 10); setTimeout(() => $toast.removeClass('visible'), 3000); } function showOfflineBar() { $('#offline-indicator').removeClass('hidden'); } function hideOfflineBar() { $('#offline-indicator').addClass('hidden'); } function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function debounce(func, wait) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } // jQuery wird als $ Parameter der IIFE übergeben // ============================================ // START // ============================================ document.addEventListener('DOMContentLoaded', init); })(jQuery);