/**
* 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 += `
`;
});
}
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('');
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 += `
`;
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 += `
`;
// 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);