PWA Mobile App für Schaltschrank-Dokumentation vor Ort: - Token-basierte Authentifizierung (15 Tage gültig) - Kundensuche mit Offline-Cache - Anlagen-Auswahl und Offline-Laden - Felder/Hutschienen/Automaten erfassen - Automatische Synchronisierung wenn wieder online - Installierbar auf dem Smartphone Home Screen - Touch-optimiertes Dark Mode Design - Quick-Select für Automaten-Werte (B16, C32, etc.) Schaltplan-Editor Verbesserungen: - Block Hover-Tooltip mit show_in_hover Feldern - Produktinfo mit Icon im Tooltip - Position und Breite in TE Neue Dateien: - pwa.php, pwa_auth.php - PWA Einstieg & Auth - ajax/pwa_api.php - PWA AJAX API - js/pwa.js, css/pwa.css - PWA App & Styles - sw.js, manifest.json - Service Worker & Manifest - img/pwa-icon-192.png, img/pwa-icon-512.png Version: 5.2.0 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
932 lines
25 KiB
JavaScript
932 lines
25 KiB
JavaScript
/**
|
|
* 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: [],
|
|
|
|
// Offline queue
|
|
offlineQueue: [],
|
|
isOnline: navigator.onLine,
|
|
|
|
// Current modal state
|
|
currentCarrierId: null,
|
|
selectedTypeId: null,
|
|
};
|
|
|
|
// ============================================
|
|
// 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);
|
|
showScreen('search');
|
|
}
|
|
|
|
// 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', () => showScreen('search'));
|
|
$('#btn-back-anlagen').on('click', () => showScreen('anlagen'));
|
|
|
|
// 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);
|
|
|
|
// 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', syncOfflineChanges);
|
|
}
|
|
|
|
// ============================================
|
|
// 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;
|
|
localStorage.removeItem('kundenkarte_pwa_token');
|
|
localStorage.removeItem('kundenkarte_pwa_user');
|
|
showScreen('login');
|
|
}
|
|
|
|
// ============================================
|
|
// SCREENS
|
|
// ============================================
|
|
|
|
function showScreen(name) {
|
|
$('.screen').removeClass('active');
|
|
$('#screen-' + name).addClass('active');
|
|
|
|
// Load data if needed
|
|
if (name === 'search') {
|
|
$('#search-customer').val('').focus();
|
|
$('#customer-list').html('<div class="list-empty">Suchbegriff eingeben...</div>');
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// CUSTOMER SEARCH
|
|
// ============================================
|
|
|
|
async function handleSearch() {
|
|
const query = $('#search-customer').val().trim();
|
|
if (query.length < 2) {
|
|
$('#customer-list').html('<div class="list-empty">Mindestens 2 Zeichen eingeben...</div>');
|
|
return;
|
|
}
|
|
|
|
$('#customer-list').html('<div class="loading-container"><div class="spinner"></div></div>');
|
|
|
|
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('<div class="list-empty">Keine Kunden gefunden</div>');
|
|
}
|
|
} catch (err) {
|
|
$('#customer-list').html('<div class="list-empty">Fehler bei der Suche</div>');
|
|
}
|
|
}
|
|
|
|
function renderCustomerList(customers) {
|
|
if (!customers.length) {
|
|
$('#customer-list').html('<div class="list-empty">Keine Kunden gefunden</div>');
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
customers.forEach(c => {
|
|
html += `
|
|
<div class="list-item" data-id="${c.id}">
|
|
<div class="list-item-icon">
|
|
<svg viewBox="0 0 24 24"><path d="M12 7V3H2v18h20V7H12zM6 19H4v-2h2v2zm0-4H4v-2h2v2zm0-4H4V9h2v2zm0-4H4V5h2v2zm4 12H8v-2h2v2zm0-4H8v-2h2v2zm0-4H8V9h2v2zm0-4H8V5h2v2zm10 12h-8v-2h2v-2h-2v-2h2v-2h-2V9h8v10zm-2-8h-2v2h2v-2zm0 4h-2v2h2v-2z"/></svg>
|
|
</div>
|
|
<div class="list-item-content">
|
|
<div class="list-item-title">${escapeHtml(c.name)}</div>
|
|
<div class="list-item-subtitle">${escapeHtml(c.town || '')}</div>
|
|
</div>
|
|
<svg class="list-item-arrow" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
$('#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('<div class="loading-container"><div class="spinner"></div></div>');
|
|
|
|
try {
|
|
const response = await apiCall('ajax/pwa_api.php', {
|
|
action: 'get_anlagen',
|
|
customer_id: id
|
|
});
|
|
|
|
if (response.success && response.anlagen) {
|
|
renderAnlagenList(response.anlagen);
|
|
// Cache for offline
|
|
localStorage.setItem('kundenkarte_anlagen_' + id, JSON.stringify(response.anlagen));
|
|
} else {
|
|
$('#anlagen-list').html('<div class="list-empty">Keine Anlagen gefunden</div>');
|
|
}
|
|
} catch (err) {
|
|
// Try cached
|
|
const cached = localStorage.getItem('kundenkarte_anlagen_' + id);
|
|
if (cached) {
|
|
renderAnlagenList(JSON.parse(cached));
|
|
showToast('Offline - Zeige gecachte Daten', 'warning');
|
|
} else {
|
|
$('#anlagen-list').html('<div class="list-empty">Fehler beim Laden</div>');
|
|
}
|
|
}
|
|
}
|
|
|
|
function renderAnlagenList(anlagen) {
|
|
// Filter nur Anlagen mit Editor
|
|
const withEditor = anlagen.filter(a => a.has_editor);
|
|
|
|
if (!withEditor.length) {
|
|
$('#anlagen-list').html('<div class="list-empty">Keine Anlagen mit Schaltplan-Editor</div>');
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
withEditor.forEach(a => {
|
|
html += `
|
|
<div class="anlage-card" data-id="${a.id}">
|
|
<div class="anlage-card-icon">
|
|
<svg viewBox="0 0 24 24"><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14zM9 7H7v2h2V7zm0 4H7v2h2v-2zm0 4H7v2h2v-2zm8-8h-6v2h6V7zm0 4h-6v2h6v-2zm0 4h-6v2h6v-2z"/></svg>
|
|
</div>
|
|
<div class="anlage-card-title">${escapeHtml(a.label || 'Anlage ' + a.id)}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
$('#anlagen-list').html(html);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
// ============================================
|
|
// EDITOR
|
|
// ============================================
|
|
|
|
async function loadEditorData() {
|
|
$('#editor-content').html('<div class="loading-container"><div class="spinner"></div><div class="text-muted">Lade Daten...</div></div>');
|
|
|
|
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 || [];
|
|
|
|
// Cache for offline
|
|
localStorage.setItem('kundenkarte_data_' + App.anlageId, JSON.stringify({
|
|
panels: App.panels,
|
|
carriers: App.carriers,
|
|
equipment: App.equipment,
|
|
types: App.equipmentTypes
|
|
}));
|
|
|
|
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 || [];
|
|
renderEditor();
|
|
showToast('Offline - Zeige gecachte Daten', 'warning');
|
|
} else {
|
|
$('#editor-content').html('<div class="list-empty">Fehler beim Laden</div>');
|
|
}
|
|
}
|
|
}
|
|
|
|
function renderEditor() {
|
|
if (!App.panels.length) {
|
|
$('#editor-content').html('<div class="list-empty">Noch keine Felder angelegt.<br>Tippe auf "+ Feld" um zu beginnen.</div>');
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
|
|
App.panels.forEach(panel => {
|
|
const panelCarriers = App.carriers.filter(c => c.fk_panel == panel.id);
|
|
|
|
html += `
|
|
<div class="panel-card" data-panel-id="${panel.id}">
|
|
<div class="panel-header">
|
|
<div class="panel-title">${escapeHtml(panel.label || 'Feld ' + panel.id)}</div>
|
|
</div>
|
|
<div class="panel-body">
|
|
`;
|
|
|
|
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));
|
|
|
|
html += `
|
|
<div class="carrier-item" data-carrier-id="${carrier.id}">
|
|
<div class="carrier-header">
|
|
<span class="carrier-label">${escapeHtml(carrier.label || 'Hutschiene')}</span>
|
|
<span class="carrier-te">${carrier.total_te || 12} TE</span>
|
|
</div>
|
|
<div class="carrier-body">
|
|
`;
|
|
|
|
carrierEquipment.forEach(eq => {
|
|
const type = App.equipmentTypes.find(t => t.id == eq.fk_equipment_type);
|
|
const typeLabel = type ? (type.label_short || type.ref) : '?';
|
|
const fieldVals = eq.field_values ? (typeof eq.field_values === 'string' ? JSON.parse(eq.field_values) : eq.field_values) : {};
|
|
const value = fieldVals.ampere ? fieldVals.ampere + 'A' : (fieldVals.characteristic || '');
|
|
|
|
html += `
|
|
<div class="equipment-block" data-equipment-id="${eq.id}" style="background:${type?.color || '#3498db'}">
|
|
<div class="equipment-block-type">${escapeHtml(typeLabel)}</div>
|
|
<div class="equipment-block-value">${escapeHtml(value)}</div>
|
|
${eq.label ? '<div class="equipment-block-label">' + escapeHtml(eq.label) + '</div>' : ''}
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
html += `
|
|
<button class="btn-add-equipment" data-carrier-id="${carrier.id}">
|
|
<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
html += `
|
|
<button class="btn-add-carrier" data-panel-id="${panel.id}">
|
|
<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
|
Hutschiene hinzufügen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
$('#editor-content').html(html);
|
|
|
|
// Load type grid
|
|
renderTypeGrid();
|
|
}
|
|
|
|
function renderTypeGrid() {
|
|
let html = '';
|
|
App.equipmentTypes.forEach(type => {
|
|
html += `
|
|
<button class="type-btn" data-type-id="${type.id}" data-width="${type.width_te || 1}">
|
|
<div class="type-btn-icon" style="color:${type.color || '#3498db'}">⚡</div>
|
|
<div class="type-btn-label">${escapeHtml(type.label_short || type.ref || type.label)}</div>
|
|
</button>
|
|
`;
|
|
});
|
|
$('#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-type').addClass('active');
|
|
$('#step-values').removeClass('active');
|
|
$('#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);
|
|
|
|
// Show value step
|
|
$('#step-type').removeClass('active');
|
|
$('#step-values').addClass('active');
|
|
|
|
// Build value fields based on type
|
|
let html = '';
|
|
|
|
// Quick select for common values
|
|
if (type && (type.ref?.includes('LS') || type.label?.includes('Leitungsschutz'))) {
|
|
html += '<p class="step-label">Kennlinie + Ampere:</p>';
|
|
html += '<div class="value-quick">';
|
|
['B6', 'B10', 'B13', 'B16', 'B20', 'B25', 'B32', 'C6', 'C10', 'C13', 'C16', 'C20', 'C25', 'C32'].forEach(v => {
|
|
html += `<button type="button" class="value-chip" data-char="${v[0]}" data-amp="${v.slice(1)}">${v}</button>`;
|
|
});
|
|
html += '</div>';
|
|
} else if (type && (type.ref?.includes('FI') || type.label?.includes('RCD'))) {
|
|
html += '<p class="step-label">Ampere:</p>';
|
|
html += '<div class="value-quick">';
|
|
['25', '40', '63', '80'].forEach(v => {
|
|
html += `<button type="button" class="value-chip" data-amp="${v}">${v}A</button>`;
|
|
});
|
|
html += '</div>';
|
|
html += '<p class="step-label" style="margin-top:12px;">Empfindlichkeit:</p>';
|
|
html += '<div class="value-quick">';
|
|
['30', '100', '300'].forEach(v => {
|
|
html += `<button type="button" class="value-chip chip-sens" data-sens="${v}">${v}mA</button>`;
|
|
});
|
|
html += '</div>';
|
|
}
|
|
|
|
$('#value-fields').html(html);
|
|
|
|
// Bind chip clicks
|
|
$('#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');
|
|
});
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
// Calculate position
|
|
const carrierEquipment = App.equipment.filter(e => e.fk_carrier == App.currentCarrierId);
|
|
let nextPos = 1;
|
|
carrierEquipment.forEach(e => {
|
|
const endPos = (parseInt(e.position_te) || 1) + (parseInt(e.width_te) || 1);
|
|
if (endPos > nextPos) nextPos = endPos;
|
|
});
|
|
|
|
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');
|
|
}
|
|
} catch (err) {
|
|
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 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 shorthand
|
|
function $(selector) {
|
|
if (typeof selector === 'string') {
|
|
const elements = document.querySelectorAll(selector);
|
|
return new ElementCollection(elements);
|
|
}
|
|
return new ElementCollection([selector]);
|
|
}
|
|
|
|
class ElementCollection {
|
|
constructor(elements) {
|
|
this.elements = Array.from(elements);
|
|
this.length = this.elements.length;
|
|
}
|
|
|
|
on(event, selectorOrHandler, handler) {
|
|
if (typeof selectorOrHandler === 'function') {
|
|
// Direct event
|
|
this.elements.forEach(el => el.addEventListener(event, selectorOrHandler));
|
|
} else {
|
|
// Delegated event
|
|
this.elements.forEach(el => {
|
|
el.addEventListener(event, function(e) {
|
|
const target = e.target.closest(selectorOrHandler);
|
|
if (target && el.contains(target)) {
|
|
handler.call(target, e);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
return this;
|
|
}
|
|
|
|
addClass(className) {
|
|
this.elements.forEach(el => el.classList.add(className));
|
|
return this;
|
|
}
|
|
|
|
removeClass(className) {
|
|
this.elements.forEach(el => el.classList.remove(className));
|
|
return this;
|
|
}
|
|
|
|
hasClass(className) {
|
|
return this.elements[0]?.classList.contains(className);
|
|
}
|
|
|
|
html(content) {
|
|
if (content === undefined) {
|
|
return this.elements[0]?.innerHTML;
|
|
}
|
|
this.elements.forEach(el => el.innerHTML = content);
|
|
return this;
|
|
}
|
|
|
|
text(content) {
|
|
if (content === undefined) {
|
|
return this.elements[0]?.textContent;
|
|
}
|
|
this.elements.forEach(el => el.textContent = content);
|
|
return this;
|
|
}
|
|
|
|
val(value) {
|
|
if (value === undefined) {
|
|
return this.elements[0]?.value;
|
|
}
|
|
this.elements.forEach(el => el.value = value);
|
|
return this;
|
|
}
|
|
|
|
data(key) {
|
|
return this.elements[0]?.dataset[key];
|
|
}
|
|
|
|
find(selector) {
|
|
const found = [];
|
|
this.elements.forEach(el => {
|
|
found.push(...el.querySelectorAll(selector));
|
|
});
|
|
return new ElementCollection(found);
|
|
}
|
|
|
|
closest(selector) {
|
|
return new ElementCollection([this.elements[0]?.closest(selector)].filter(Boolean));
|
|
}
|
|
|
|
focus() {
|
|
this.elements[0]?.focus();
|
|
return this;
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// START
|
|
// ============================================
|
|
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
|
|
})();
|