- Anlagen-Screen: Kontakte/Adressen oben als vertikale Liste mit Chevron - Anlagen-Cards: Horizontales Layout (Icon + Titel + Pfeil), volle Breite - Feld-Badges aus Admin-Einstellungen (show_in_tree) auf Anlagen-Cards - Kunden-Adresse als Trennlabel bei Anlagen ohne Kontaktzuweisung - Back-Navigation Fix: Anlagen werden nachgeladen falls leer (Refresh→Back) - TE-Lücken-Berechnung: getMaxGap() für zusammenhängende freie Slots - Typ-Buttons gefiltert: Nur Typen die in verfügbare Lücke passen - Equipment-Blöcke: 80px Höhe, Sicherungsautomat-Optik - PHP 8.1: trim() null-safe mit ?? '' - Cache-Versionen synchronisiert auf v2.7 - equipmentconnection: source/target_terminal_id im UPDATE SQL ergänzt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1930 lines
58 KiB
JavaScript
1930 lines
58 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: '',
|
||
customerAddress: '',
|
||
anlageId: null,
|
||
anlageName: '',
|
||
|
||
// Data
|
||
panels: [],
|
||
carriers: [],
|
||
equipment: [],
|
||
equipmentTypes: [],
|
||
outputs: [],
|
||
inputs: [],
|
||
|
||
// Offline queue
|
||
offlineQueue: [],
|
||
isOnline: navigator.onLine,
|
||
|
||
// Current modal state
|
||
currentCarrierId: null,
|
||
editCarrierId: null, // null = Add-Modus, ID = Edit-Modus (Hutschiene)
|
||
selectedTypeId: null,
|
||
editEquipmentId: null, // null = Add-Modus, ID = Edit-Modus
|
||
confirmCallback: null, // Callback für Bestätigungsdialog
|
||
editConnectionId: null, // null = Neu, ID = Edit
|
||
connectionEquipmentId: null, // Equipment für aktuelle Connection
|
||
connectionDirection: 'output', // 'output' oder 'input'
|
||
mediumTypes: null, // Kabeltypen aus DB (gecacht)
|
||
cachedTypeFields: null, // Equipment-Felder Cache
|
||
|
||
};
|
||
|
||
// ============================================
|
||
// 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 || '';
|
||
App.customerAddress = lastState.customerAddress || '';
|
||
$('#customer-name').text(App.customerName);
|
||
}
|
||
if (lastState.anlageId) {
|
||
App.anlageId = lastState.anlageId;
|
||
App.anlageName = lastState.anlageName || '';
|
||
$('#anlage-name').text(App.anlageName);
|
||
}
|
||
|
||
// Screen wiederherstellen inkl. vollständiger History-Stack
|
||
// Damit Zurück-Button auch nach App-Suspend korrekt funktioniert
|
||
if (lastState.screen === 'editor' && App.anlageId) {
|
||
history.replaceState({ screen: 'search' }, '', '#search');
|
||
history.pushState({ screen: 'anlagen' }, '', '#anlagen');
|
||
showScreen('editor');
|
||
loadEditorData();
|
||
} else if (lastState.screen === 'anlagen' && App.customerId) {
|
||
history.replaceState({ screen: 'search' }, '', '#search');
|
||
showScreen('anlagen');
|
||
reloadAnlagen();
|
||
} else {
|
||
showScreen('search');
|
||
}
|
||
} else {
|
||
showScreen('search');
|
||
}
|
||
}
|
||
|
||
// Initialen History-State setzen (nur wenn kein Session-Restore)
|
||
if (!sessionStorage.getItem('kundenkarte_pwa_state')) {
|
||
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) {
|
||
// Wenn ein Modal offen ist: Modal schließen statt navigieren
|
||
const $activeModal = $('.modal.active');
|
||
if ($activeModal.length) {
|
||
$activeModal.removeClass('active');
|
||
// Aktuellen State wieder pushen (Navigation verhindern)
|
||
const currentScreen = $('.screen.active').attr('id')?.replace('screen-', '') || 'search';
|
||
history.pushState({ screen: currentScreen }, '', '#' + currentScreen);
|
||
return;
|
||
}
|
||
|
||
if (e.state && e.state.screen) {
|
||
showScreen(e.state.screen, true);
|
||
// Anlagen-Liste nachladen falls leer (z.B. nach Seiten-Refresh)
|
||
if (e.state.screen === 'anlagen' && App.customerId && !$('#anlagen-list').children('.anlage-card, .contact-group').length) {
|
||
reloadAnlagen();
|
||
}
|
||
} 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);
|
||
$('#editor-content').on('click', '.carrier-header', handleCarrierClick);
|
||
$('#btn-save-carrier').on('click', handleSaveCarrier);
|
||
$('#btn-delete-carrier').on('click', handleDeleteCarrierConfirm);
|
||
|
||
$('#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-eq-back').on('click', () => showEquipmentStep('type'));
|
||
$('#btn-save-equipment').on('click', handleSaveEquipment);
|
||
$('#btn-cancel-equipment').on('click', () => closeModal('add-equipment'));
|
||
$('#btn-delete-equipment').on('click', handleDeleteEquipmentConfirm);
|
||
|
||
// Terminal/Connection - Klick auf einzelne Klemme
|
||
$('#editor-content').on('click', '.terminal-point', handleTerminalClick);
|
||
$('#btn-save-connection').on('click', handleSaveConnection);
|
||
$('#btn-delete-connection').on('click', handleDeleteConnectionConfirm);
|
||
// Abgangsseite-Buttons
|
||
$('#conn-side-grid').on('click', '.side-btn', function() {
|
||
$('.side-btn').removeClass('selected');
|
||
$(this).addClass('selected');
|
||
});
|
||
|
||
// Bestätigungsdialog
|
||
$('#btn-confirm-ok').on('click', function() {
|
||
closeModal('confirm');
|
||
if (App.confirmCallback) {
|
||
App.confirmCallback();
|
||
App.confirmCallback = null;
|
||
}
|
||
});
|
||
|
||
// 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);
|
||
|
||
}
|
||
|
||
// ============================================
|
||
// 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,
|
||
customerAddress: App.customerAddress,
|
||
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('<div class="loading-container"><div class="spinner"></div></div>');
|
||
|
||
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('<div class="list-empty">Keine Anlagen gefunden</div>');
|
||
}
|
||
} 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('<div class="list-empty">Fehler beim Laden</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();
|
||
const address = $(this).find('.list-item-subtitle').text();
|
||
|
||
App.customerId = id;
|
||
App.customerName = name;
|
||
App.customerAddress = address;
|
||
$('#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) {
|
||
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('<div class="list-empty">Keine Anlagen gefunden</div>');
|
||
}
|
||
} 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('<div class="list-empty">Fehler beim Laden</div>');
|
||
}
|
||
}
|
||
}
|
||
|
||
function renderAnlagenList(anlagen, contacts) {
|
||
let html = '';
|
||
|
||
// Kontakt-Adressen (Gebäude/Standorte) als Liste
|
||
if (contacts && contacts.length) {
|
||
html += '<div class="contact-list">';
|
||
contacts.forEach(c => {
|
||
const subtitle = [c.address, c.town].filter(Boolean).join(', ');
|
||
html += `
|
||
<div class="contact-group" data-contact-id="${c.id}" data-customer-id="${App.customerId}">
|
||
<div class="contact-group-header">
|
||
<svg viewBox="0 0 24 24"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/></svg>
|
||
<div class="contact-group-info">
|
||
<div class="contact-group-name">${escapeHtml(c.name)}</div>
|
||
${subtitle ? '<div class="contact-group-address">' + escapeHtml(subtitle) + '</div>' : ''}
|
||
</div>
|
||
<span class="contact-group-count">${c.anlage_count}</span>
|
||
<svg class="contact-group-chevron" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
|
||
</div>
|
||
<div class="contact-anlagen-list"></div>
|
||
</div>
|
||
`;
|
||
});
|
||
html += '</div>';
|
||
}
|
||
|
||
// Kunden-Anlagen (ohne Kontaktzuweisung) darunter
|
||
if (anlagen && anlagen.length) {
|
||
if (contacts && contacts.length && App.customerAddress) {
|
||
html += `<div class="anlagen-section-label">${escapeHtml(App.customerName)} – ${escapeHtml(App.customerAddress)}</div>`;
|
||
}
|
||
anlagen.forEach(a => {
|
||
html += renderAnlageCard(a);
|
||
});
|
||
}
|
||
|
||
if (!html) {
|
||
$('#anlagen-list').html('<div class="list-empty">Keine Anlagen gefunden</div>');
|
||
return;
|
||
}
|
||
|
||
$('#anlagen-list').html(html);
|
||
}
|
||
|
||
function renderAnlageCard(a) {
|
||
let fieldsHtml = '';
|
||
if (a.fields && a.fields.length) {
|
||
fieldsHtml = '<div class="anlage-card-fields">';
|
||
a.fields.forEach(f => {
|
||
const style = f.color ? ` style="background:${f.color}"` : '';
|
||
fieldsHtml += `<span class="anlage-field-badge"${style}>${escapeHtml(f.value)}</span>`;
|
||
});
|
||
fieldsHtml += '</div>';
|
||
}
|
||
return `
|
||
<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-content">
|
||
<div class="anlage-card-title">${escapeHtml(a.label || 'Anlage ' + a.id)}</div>
|
||
${a.type ? '<div class="anlage-card-type">' + escapeHtml(a.type) + '</div>' : ''}
|
||
${fieldsHtml}
|
||
</div>
|
||
<svg class="anlage-card-arrow" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
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('<div class="loading-container"><div class="spinner small"></div></div>');
|
||
|
||
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('<div class="list-empty small">Keine Anlagen</div>');
|
||
}
|
||
} catch (err) {
|
||
$list.html('<div class="list-empty small">Fehler beim Laden</div>');
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// 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 || [];
|
||
App.outputs = response.outputs || [];
|
||
App.inputs = response.inputs || [];
|
||
|
||
// 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,
|
||
inputs: App.inputs
|
||
}));
|
||
|
||
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 || [];
|
||
App.inputs = data.inputs || [];
|
||
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));
|
||
|
||
const totalTe = parseInt(carrier.total_te) || 12;
|
||
const usedTe = carrierEquipment.reduce((sum, eq) => sum + (parseInt(eq.width_te) || 1), 0);
|
||
const isFull = usedTe >= totalTe;
|
||
|
||
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">${usedTe}/${totalTe} TE</span>
|
||
</div>
|
||
<div class="carrier-content" style="grid-template-columns: repeat(${totalTe}, 1fr) auto">
|
||
`;
|
||
|
||
// === Zeile 1: Terminals oben (Inputs + Top-Outputs) ===
|
||
carrierEquipment.forEach(eq => {
|
||
const widthTe = parseInt(eq.width_te) || 1;
|
||
const posTe = parseInt(eq.position_te) || 0;
|
||
const eqInputs = App.inputs ? App.inputs.filter(i => i.fk_target == eq.id) : [];
|
||
const eqTopOutputs = App.outputs ? App.outputs.filter(o => o.fk_source == eq.id && o.is_top) : [];
|
||
|
||
for (let t = 0; t < widthTe; t++) {
|
||
const colPos = posTe > 0 ? posTe + t : 0;
|
||
const style = `grid-row:1;${colPos > 0 ? ' grid-column:' + colPos : ''}`;
|
||
const inp = eqInputs[t] || null;
|
||
const topOut = eqTopOutputs[t] || null;
|
||
|
||
if (topOut) {
|
||
// Top-Output: Pfeil nach oben ▲
|
||
const phaseColor = topOut.color || getPhaseColor(topOut.connection_type);
|
||
html += `<span class="terminal-point terminal-output" data-equipment-id="${eq.id}" data-direction="output" data-connection-id="${topOut.id}" style="${style}">`;
|
||
html += renderOutputLabel(topOut, phaseColor, 'up');
|
||
html += `</span>`;
|
||
} else if (inp) {
|
||
const phaseColor = inp.color || getPhaseColor(inp.connection_type);
|
||
html += `<span class="terminal-point terminal-input" data-equipment-id="${eq.id}" data-direction="input" data-connection-id="${inp.id}" style="${style}">`;
|
||
html += `<span class="terminal-dot" style="background:${phaseColor}"></span>`;
|
||
html += `<span class="terminal-phase">${escapeHtml(inp.connection_type || '')}</span>`;
|
||
html += `</span>`;
|
||
} else {
|
||
html += `<span class="terminal-point terminal-input" data-equipment-id="${eq.id}" data-direction="input" data-connection-id="" style="${style}">`;
|
||
html += `<span class="terminal-dot terminal-empty"></span>`;
|
||
html += `</span>`;
|
||
}
|
||
}
|
||
});
|
||
|
||
// === Zeile 2: Equipment-Blöcke ===
|
||
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;
|
||
|
||
const typeLabel = type?.label_short || type?.ref || '';
|
||
const blockColor = eq.block_color || type?.color || '#3498db';
|
||
const eqLabel = eq.label || '';
|
||
const blockFields = eq.block_label || '';
|
||
const showBlockFields = blockFields && blockFields !== typeLabel && blockFields !== (type?.ref || '');
|
||
|
||
const gridCol = posTe > 0
|
||
? `grid-row:2; grid-column: ${posTe} / span ${widthTe}`
|
||
: `grid-row:2; grid-column: span ${widthTe}`;
|
||
|
||
html += `
|
||
<div class="equipment-block" data-equipment-id="${eq.id}" style="background:${blockColor}; ${gridCol}">
|
||
<span class="equipment-block-type">${escapeHtml(typeLabel)}</span>
|
||
${showBlockFields ? `<span class="equipment-block-value">${escapeHtml(blockFields)}</span>` : ''}
|
||
<span class="equipment-block-label">${escapeHtml(eqLabel)}</span>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
// +-Button in letzter Spalte (auto), Zeile 2
|
||
html += `
|
||
<button class="btn-add-equipment${isFull ? ' disabled' : ''}" data-carrier-id="${carrier.id}"${isFull ? ' disabled' : ''} style="grid-row:2; grid-column:-1">
|
||
<svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
||
</button>
|
||
`;
|
||
|
||
// === Zeile 3: Output-Terminals unten (Standard-Abgänge) ===
|
||
carrierEquipment.forEach(eq => {
|
||
const widthTe = parseInt(eq.width_te) || 1;
|
||
const posTe = parseInt(eq.position_te) || 0;
|
||
const eqBottomOutputs = App.outputs ? App.outputs.filter(o => o.fk_source == eq.id && !o.is_top) : [];
|
||
|
||
for (let t = 0; t < widthTe; t++) {
|
||
const colPos = posTe > 0 ? posTe + t : 0;
|
||
const style = `grid-row:3;${colPos > 0 ? ' grid-column:' + colPos : ''}`;
|
||
const out = eqBottomOutputs[t] || null;
|
||
|
||
if (out) {
|
||
const phaseColor = out.color || getPhaseColor(out.connection_type);
|
||
html += `<span class="terminal-point terminal-output" data-equipment-id="${eq.id}" data-direction="output" data-connection-id="${out.id}" style="${style}">`;
|
||
html += renderOutputLabel(out, phaseColor, 'down');
|
||
html += `</span>`;
|
||
} else {
|
||
html += `<span class="terminal-point terminal-output" data-equipment-id="${eq.id}" data-direction="output" data-connection-id="" style="${style}">`;
|
||
html += `<span class="terminal-dot terminal-empty"></span>`;
|
||
html += `</span>`;
|
||
}
|
||
}
|
||
});
|
||
|
||
html += `</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');
|
||
} else {
|
||
showToast(response.error || 'Fehler beim Anlegen', 'error');
|
||
}
|
||
} catch (err) {
|
||
showToast('Netzwerkfehler - wird offline gespeichert', 'warning');
|
||
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;
|
||
App.editCarrierId = null;
|
||
$('.te-btn').removeClass('selected');
|
||
$('#carrier-label').val('');
|
||
$('#carrier-modal-title').text('Hutschiene hinzufügen');
|
||
$('#btn-save-carrier').text('Hinzufügen');
|
||
$('#btn-delete-carrier').addClass('hidden');
|
||
openModal('add-carrier');
|
||
}
|
||
|
||
function handleCarrierClick() {
|
||
const carrierId = $(this).closest('.carrier-item').data('carrier-id');
|
||
const carrier = App.carriers.find(c => c.id == carrierId);
|
||
if (!carrier) return;
|
||
|
||
App.editCarrierId = carrierId;
|
||
App.currentPanelId = carrier.fk_panel;
|
||
|
||
// TE-Button vorselektieren
|
||
$('.te-btn').removeClass('selected');
|
||
$(`.te-btn[data-te="${carrier.total_te}"]`).addClass('selected');
|
||
$('#carrier-label').val(carrier.label || '');
|
||
$('#carrier-modal-title').text('Hutschiene bearbeiten');
|
||
$('#btn-save-carrier').text('Speichern');
|
||
$('#btn-delete-carrier').removeClass('hidden');
|
||
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';
|
||
|
||
closeModal('add-carrier');
|
||
|
||
if (App.editCarrierId) {
|
||
// Update
|
||
const data = {
|
||
action: 'update_carrier',
|
||
carrier_id: App.editCarrierId,
|
||
total_te: totalTe,
|
||
label: label
|
||
};
|
||
|
||
if (App.isOnline) {
|
||
try {
|
||
const response = await apiCall('ajax/pwa_api.php', data);
|
||
if (response.success) {
|
||
const carrier = App.carriers.find(c => c.id == App.editCarrierId);
|
||
if (carrier) {
|
||
carrier.total_te = totalTe;
|
||
carrier.label = label;
|
||
}
|
||
renderEditor();
|
||
showToast('Hutschiene aktualisiert', 'success');
|
||
} else {
|
||
showToast(response.error || 'Fehler', 'error');
|
||
}
|
||
} catch (err) {
|
||
showToast('Netzwerkfehler - wird offline gespeichert', 'warning');
|
||
queueOfflineAction(data);
|
||
}
|
||
} else {
|
||
queueOfflineAction(data);
|
||
const carrier = App.carriers.find(c => c.id == App.editCarrierId);
|
||
if (carrier) {
|
||
carrier.total_te = totalTe;
|
||
carrier.label = label;
|
||
}
|
||
renderEditor();
|
||
showToast('Wird synchronisiert...', 'warning');
|
||
}
|
||
} else {
|
||
// Neu anlegen
|
||
const data = {
|
||
action: 'create_carrier',
|
||
panel_id: App.currentPanelId,
|
||
total_te: totalTe,
|
||
label: label
|
||
};
|
||
|
||
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');
|
||
} else {
|
||
showToast(response.error || 'Fehler beim Anlegen', 'error');
|
||
}
|
||
} catch (err) {
|
||
showToast('Netzwerkfehler - wird offline gespeichert', 'warning');
|
||
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');
|
||
}
|
||
}
|
||
|
||
App.editCarrierId = null;
|
||
}
|
||
|
||
function handleDeleteCarrierConfirm() {
|
||
const carrierId = App.editCarrierId;
|
||
if (!carrierId) return;
|
||
|
||
const carrier = App.carriers.find(c => c.id == carrierId);
|
||
const eqCount = App.equipment.filter(e => e.fk_carrier == carrierId).length;
|
||
const msg = eqCount > 0
|
||
? `"${carrier?.label || 'Hutschiene'}" mit ${eqCount} Automat${eqCount > 1 ? 'en' : ''} wirklich löschen?`
|
||
: `"${carrier?.label || 'Hutschiene'}" wirklich löschen?`;
|
||
|
||
$('#confirm-title').text('Hutschiene löschen?');
|
||
$('#confirm-message').text(msg);
|
||
App.confirmCallback = () => deleteCarrier(carrierId);
|
||
|
||
closeModal('add-carrier');
|
||
openModal('confirm');
|
||
}
|
||
|
||
async function deleteCarrier(carrierId) {
|
||
const data = {
|
||
action: 'delete_carrier',
|
||
carrier_id: carrierId
|
||
};
|
||
|
||
if (App.isOnline) {
|
||
try {
|
||
const response = await apiCall('ajax/pwa_api.php', data);
|
||
if (response.success) {
|
||
App.equipment = App.equipment.filter(e => e.fk_carrier != carrierId);
|
||
App.carriers = App.carriers.filter(c => c.id != carrierId);
|
||
renderEditor();
|
||
showToast('Hutschiene gelöscht', 'success');
|
||
} else {
|
||
showToast(response.error || 'Fehler', 'error');
|
||
}
|
||
} catch (err) {
|
||
showToast('Netzwerkfehler - wird offline gespeichert', 'warning');
|
||
queueOfflineAction(data);
|
||
App.equipment = App.equipment.filter(e => e.fk_carrier != carrierId);
|
||
App.carriers = App.carriers.filter(c => c.id != carrierId);
|
||
renderEditor();
|
||
}
|
||
} else {
|
||
queueOfflineAction(data);
|
||
App.equipment = App.equipment.filter(e => e.fk_carrier != carrierId);
|
||
App.carriers = App.carriers.filter(c => c.id != carrierId);
|
||
renderEditor();
|
||
showToast('Wird synchronisiert...', 'warning');
|
||
}
|
||
|
||
App.editCarrierId = null;
|
||
}
|
||
|
||
// ============================================
|
||
// EQUIPMENT (AUTOMAT) ACTIONS
|
||
// ============================================
|
||
|
||
/**
|
||
* Maximale zusammenhängende Lücke auf einem Carrier berechnen
|
||
*/
|
||
function getMaxGap(carrierId) {
|
||
const carrier = App.carriers.find(c => c.id == carrierId);
|
||
if (!carrier) return 0;
|
||
const totalTe = parseInt(carrier.total_te) || 12;
|
||
const carrierEquipment = App.equipment.filter(e => e.fk_carrier == carrierId);
|
||
|
||
// Belegte Slots ermitteln (1-basiert)
|
||
const occupied = {};
|
||
carrierEquipment.forEach(eq => {
|
||
const pos = parseInt(eq.position_te) || 1;
|
||
const w = parseInt(eq.width_te) || 1;
|
||
for (let s = pos; s < pos + w; s++) {
|
||
occupied[s] = true;
|
||
}
|
||
});
|
||
|
||
// Maximale zusammenhängende Lücke
|
||
let maxGap = 0, currentGap = 0;
|
||
for (let s = 1; s <= totalTe; s++) {
|
||
if (!occupied[s]) {
|
||
currentGap++;
|
||
if (currentGap > maxGap) maxGap = currentGap;
|
||
} else {
|
||
currentGap = 0;
|
||
}
|
||
}
|
||
return maxGap;
|
||
}
|
||
|
||
function handleAddEquipment() {
|
||
const carrierId = $(this).data('carrier-id');
|
||
App.currentCarrierId = carrierId;
|
||
App.selectedTypeId = null;
|
||
App.editEquipmentId = null;
|
||
|
||
// Add-Modus: Titel, Typ-Grid freigeben
|
||
$('#equipment-modal-title').text('Automat hinzufügen');
|
||
$('#btn-save-equipment').text('Speichern');
|
||
$('#btn-delete-equipment').addClass('hidden');
|
||
$('#type-grid .type-btn').removeClass('selected');
|
||
|
||
// Typ-Buttons nach verfügbarem Platz filtern
|
||
const maxGap = getMaxGap(carrierId);
|
||
$('#type-grid .type-btn').each(function() {
|
||
const w = parseInt($(this).data('width')) || 1;
|
||
if (w > maxGap) {
|
||
$(this).addClass('disabled').prop('disabled', true);
|
||
} else {
|
||
$(this).removeClass('disabled').prop('disabled', false);
|
||
}
|
||
});
|
||
|
||
// Schritt 1 zeigen
|
||
showEquipmentStep('type');
|
||
openModal('add-equipment');
|
||
}
|
||
|
||
async 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);
|
||
|
||
// Titel für Schritt 2
|
||
$('#eq-fields-title').text(type?.label_short || type?.label || 'Werte');
|
||
|
||
// Felder vom Server laden
|
||
await loadTypeFields(App.selectedTypeId, App.editEquipmentId);
|
||
|
||
// Zu Schritt 2 wechseln
|
||
showEquipmentStep('fields');
|
||
}
|
||
|
||
/**
|
||
* Wechselt zwischen Schritt 1 (Typ) und Schritt 2 (Felder)
|
||
*/
|
||
function showEquipmentStep(step) {
|
||
if (step === 'type') {
|
||
$('#eq-step-type').removeClass('hidden');
|
||
$('#eq-step-fields').addClass('hidden');
|
||
} else {
|
||
$('#eq-step-type').addClass('hidden');
|
||
$('#eq-step-fields').removeClass('hidden');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Lädt Felder für einen Equipment-Typ vom Server
|
||
*/
|
||
async function loadTypeFields(typeId, equipmentId) {
|
||
$('#eq-dynamic-fields').html('<div class="loading-container"><div class="spinner small"></div></div>');
|
||
|
||
if (!App.isOnline) {
|
||
// Offline: Gecachte Felder verwenden falls vorhanden
|
||
const cached = App.cachedTypeFields && App.cachedTypeFields[typeId];
|
||
if (cached) {
|
||
renderDynamicFields(cached);
|
||
} else {
|
||
$('#eq-dynamic-fields').html('<div class="list-empty small">Offline - Felder nicht verfügbar</div>');
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const data = { action: 'get_type_fields', type_id: typeId };
|
||
if (equipmentId) data.equipment_id = equipmentId;
|
||
|
||
const response = await apiCall('ajax/pwa_api.php', data);
|
||
if (response.success) {
|
||
// Felder cachen für Offline
|
||
if (!App.cachedTypeFields) App.cachedTypeFields = {};
|
||
App.cachedTypeFields[typeId] = response.fields;
|
||
|
||
renderDynamicFields(response.fields);
|
||
} else {
|
||
$('#eq-dynamic-fields').html('');
|
||
}
|
||
} catch (err) {
|
||
$('#eq-dynamic-fields').html('');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Rendert dynamische Felder basierend auf field_type aus der DB
|
||
*/
|
||
function renderDynamicFields(fields) {
|
||
if (!fields || !fields.length) {
|
||
$('#eq-dynamic-fields').html('');
|
||
$('#equipment-label').focus();
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
fields.forEach(field => {
|
||
const req = field.required ? ' <span class="field-required">*</span>' : '';
|
||
const val = field.value || '';
|
||
|
||
html += `<div class="form-group">`;
|
||
html += `<label>${escapeHtml(field.label)}${req}</label>`;
|
||
|
||
switch (field.type) {
|
||
case 'select':
|
||
html += `<select name="eq_field_${field.code}" class="form-select">`;
|
||
html += `<option value="">--</option>`;
|
||
if (field.options) {
|
||
field.options.split('|').forEach(opt => {
|
||
const selected = (opt === val) ? ' selected' : '';
|
||
html += `<option value="${escapeHtml(opt)}"${selected}>${escapeHtml(opt)}</option>`;
|
||
});
|
||
}
|
||
html += `</select>`;
|
||
break;
|
||
|
||
case 'number':
|
||
html += `<input type="number" name="eq_field_${field.code}" class="form-input" value="${escapeHtml(val)}">`;
|
||
break;
|
||
|
||
case 'checkbox':
|
||
const checked = val === '1' || val === 'true' ? ' checked' : '';
|
||
html += `<label class="checkbox-label"><input type="checkbox" name="eq_field_${field.code}" value="1"${checked}> ${escapeHtml(field.label)}</label>`;
|
||
break;
|
||
|
||
case 'textarea':
|
||
html += `<textarea name="eq_field_${field.code}" class="form-input" rows="3">${escapeHtml(val)}</textarea>`;
|
||
break;
|
||
|
||
default: // text
|
||
html += `<input type="text" name="eq_field_${field.code}" class="form-input" value="${escapeHtml(val)}">`;
|
||
}
|
||
|
||
html += `</div>`;
|
||
});
|
||
|
||
$('#eq-dynamic-fields').html(html);
|
||
}
|
||
|
||
/**
|
||
* Sammelt Feldwerte aus den dynamischen Formularfeldern
|
||
*/
|
||
function collectFieldValues() {
|
||
const fieldValues = {};
|
||
$('#eq-dynamic-fields [name^="eq_field_"]').each(function() {
|
||
const code = $(this).attr('name').replace('eq_field_', '');
|
||
if ($(this).is(':checkbox')) {
|
||
fieldValues[code] = $(this).is(':checked') ? '1' : '0';
|
||
} else {
|
||
const val = $(this).val();
|
||
if (val) fieldValues[code] = val;
|
||
}
|
||
});
|
||
return fieldValues;
|
||
}
|
||
|
||
async function handleSaveEquipment() {
|
||
if (!App.selectedTypeId) {
|
||
showToast('Bitte Typ wählen', 'error');
|
||
return;
|
||
}
|
||
|
||
// Pflichtfelder prüfen
|
||
let valid = true;
|
||
$('#eq-dynamic-fields select[name], #eq-dynamic-fields input[name]').each(function() {
|
||
const $field = $(this);
|
||
const $group = $field.closest('.form-group');
|
||
if ($group.find('.field-required').length && !$field.val()) {
|
||
$field.addClass('field-error');
|
||
valid = false;
|
||
} else {
|
||
$field.removeClass('field-error');
|
||
}
|
||
});
|
||
if (!valid) {
|
||
showToast('Pflichtfelder ausfüllen', 'error');
|
||
return;
|
||
}
|
||
|
||
const type = App.equipmentTypes.find(t => t.id == App.selectedTypeId);
|
||
const label = $('#equipment-label').val().trim();
|
||
const fieldValues = collectFieldValues();
|
||
|
||
if (App.editEquipmentId) {
|
||
await saveEquipmentUpdate(label, fieldValues);
|
||
} else {
|
||
await saveEquipmentCreate(type, label, fieldValues);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Neuen Automaten anlegen
|
||
*/
|
||
async function saveEquipmentCreate(type, label, fieldValues) {
|
||
// 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: response.label || label,
|
||
position_te: nextPos,
|
||
width_te: type?.width_te || 1,
|
||
field_values: fieldValues,
|
||
block_label: response.block_label || '',
|
||
block_color: response.block_color || type?.color || ''
|
||
});
|
||
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');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Bestehenden Automaten aktualisieren
|
||
*/
|
||
async function saveEquipmentUpdate(label, fieldValues) {
|
||
const data = {
|
||
action: 'update_equipment',
|
||
equipment_id: App.editEquipmentId,
|
||
label: label,
|
||
field_values: JSON.stringify(fieldValues)
|
||
};
|
||
|
||
closeModal('add-equipment');
|
||
|
||
if (App.isOnline) {
|
||
try {
|
||
const response = await apiCall('ajax/pwa_api.php', data);
|
||
if (response.success) {
|
||
// Lokale Daten aktualisieren
|
||
const eq = App.equipment.find(e => e.id == App.editEquipmentId);
|
||
if (eq) {
|
||
eq.label = label;
|
||
eq.field_values = fieldValues;
|
||
eq.block_label = response.block_label || '';
|
||
eq.block_color = response.block_color || eq.block_color;
|
||
}
|
||
renderEditor();
|
||
showToast('Automat aktualisiert', 'success');
|
||
} else {
|
||
showToast(response.error || 'Fehler beim Aktualisieren', 'error');
|
||
}
|
||
} catch (err) {
|
||
showToast('Netzwerkfehler - wird offline gespeichert', 'warning');
|
||
queueOfflineAction(data);
|
||
// Optimistic UI
|
||
const eq = App.equipment.find(e => e.id == App.editEquipmentId);
|
||
if (eq) {
|
||
eq.label = label;
|
||
eq.field_values = fieldValues;
|
||
}
|
||
renderEditor();
|
||
}
|
||
} else {
|
||
queueOfflineAction(data);
|
||
const eq = App.equipment.find(e => e.id == App.editEquipmentId);
|
||
if (eq) {
|
||
eq.label = label;
|
||
eq.field_values = fieldValues;
|
||
}
|
||
renderEditor();
|
||
showToast('Wird synchronisiert...', 'warning');
|
||
}
|
||
|
||
App.editEquipmentId = null;
|
||
}
|
||
|
||
async function handleEquipmentClick() {
|
||
const eqId = $(this).data('equipment-id');
|
||
const eq = App.equipment.find(e => e.id == eqId);
|
||
if (!eq) return;
|
||
|
||
const type = App.equipmentTypes.find(t => t.id == eq.fk_equipment_type);
|
||
|
||
// Edit-Modus setzen
|
||
App.editEquipmentId = eqId;
|
||
App.currentCarrierId = eq.fk_carrier;
|
||
App.selectedTypeId = eq.fk_equipment_type;
|
||
|
||
// Titel + Buttons anpassen
|
||
$('#eq-fields-title').text(type?.label_short || type?.label || 'Bearbeiten');
|
||
$('#btn-save-equipment').text('Aktualisieren');
|
||
$('#btn-delete-equipment').removeClass('hidden');
|
||
|
||
// Label befüllen
|
||
$('#equipment-label').val(eq.label || '');
|
||
|
||
// Felder vom Server laden (mit bestehenden Werten)
|
||
await loadTypeFields(eq.fk_equipment_type, eqId);
|
||
|
||
// Direkt Schritt 2 zeigen (kein Typ-Wechsel beim Edit)
|
||
showEquipmentStep('fields');
|
||
openModal('add-equipment');
|
||
}
|
||
|
||
/**
|
||
* Bestätigungsdialog vor Equipment-Löschung
|
||
*/
|
||
function handleDeleteEquipmentConfirm() {
|
||
const eqId = App.editEquipmentId;
|
||
if (!eqId) return;
|
||
|
||
const eq = App.equipment.find(e => e.id == eqId);
|
||
const type = App.equipmentTypes.find(t => t.id == eq?.fk_equipment_type);
|
||
const typeName = type?.label_short || type?.ref || 'Automat';
|
||
const eqLabel = eq?.label ? ` "${eq.label}"` : '';
|
||
|
||
$('#confirm-title').text('Automat löschen?');
|
||
$('#confirm-message').text(`${typeName}${eqLabel} wirklich löschen?`);
|
||
|
||
// Callback für OK-Button
|
||
App.confirmCallback = () => deleteEquipment(eqId);
|
||
|
||
closeModal('add-equipment');
|
||
openModal('confirm');
|
||
}
|
||
|
||
/**
|
||
* Equipment löschen (nach Bestätigung)
|
||
*/
|
||
async function deleteEquipment(eqId) {
|
||
const data = {
|
||
action: 'delete_equipment',
|
||
equipment_id: eqId
|
||
};
|
||
|
||
if (App.isOnline) {
|
||
try {
|
||
const response = await apiCall('ajax/pwa_api.php', data);
|
||
if (response.success) {
|
||
App.equipment = App.equipment.filter(e => e.id != eqId);
|
||
// Zugehörige Abgänge entfernen
|
||
App.outputs = App.outputs.filter(o => o.fk_source != eqId);
|
||
renderEditor();
|
||
showToast('Automat gelöscht', 'success');
|
||
} else {
|
||
showToast(response.error || 'Fehler beim Löschen', 'error');
|
||
}
|
||
} catch (err) {
|
||
showToast('Netzwerkfehler - wird offline gespeichert', 'warning');
|
||
queueOfflineAction(data);
|
||
App.equipment = App.equipment.filter(e => e.id != eqId);
|
||
App.outputs = App.outputs.filter(o => o.fk_source != eqId);
|
||
renderEditor();
|
||
}
|
||
} else {
|
||
queueOfflineAction(data);
|
||
App.equipment = App.equipment.filter(e => e.id != eqId);
|
||
App.outputs = App.outputs.filter(o => o.fk_source != eqId);
|
||
renderEditor();
|
||
showToast('Wird synchronisiert...', 'warning');
|
||
}
|
||
|
||
App.editEquipmentId = null;
|
||
}
|
||
|
||
/**
|
||
* Output-Terminal HTML erzeugen (Pfeil + Labels)
|
||
* @param {object} out - Connection-Objekt
|
||
* @param {string} phaseColor - Farbe der Phase
|
||
* @param {string} dir - 'up' oder 'down'
|
||
*/
|
||
function renderOutputLabel(out, phaseColor, dir) {
|
||
// Pfeil statt Punkt - Pfeil immer am Automaten (zwischen Block und Label)
|
||
const arrowClass = dir === 'up' ? 'terminal-arrow-up' : 'terminal-arrow-down';
|
||
const arrowHtml = `<span class="terminal-arrow ${arrowClass}" style="--arrow-color:${phaseColor}"></span>`;
|
||
|
||
let labelHtml = '';
|
||
if (out.output_label) {
|
||
let cableInfo = '';
|
||
if (out.medium_type) cableInfo = out.medium_type;
|
||
if (out.medium_spec) cableInfo += ' ' + out.medium_spec;
|
||
if (out.medium_length) cableInfo += ' (' + out.medium_length + ')';
|
||
labelHtml = `<span class="terminal-label">${escapeHtml(out.output_label)}`;
|
||
if (cableInfo) labelHtml += `<br><small class="cable-info">${escapeHtml(cableInfo.trim())}</small>`;
|
||
labelHtml += `</span>`;
|
||
} else {
|
||
labelHtml = `<span class="terminal-phase">${escapeHtml(out.connection_type || '')}</span>`;
|
||
}
|
||
|
||
// Oben: Label zuerst, dann Pfeil (Pfeil zeigt zum Automaten darunter)
|
||
// Unten: Pfeil zuerst, dann Label (Pfeil zeigt zum Automaten darüber)
|
||
if (dir === 'up') {
|
||
return labelHtml + arrowHtml;
|
||
}
|
||
return arrowHtml + labelHtml;
|
||
}
|
||
|
||
// ============================================
|
||
// CONNECTION (TERMINAL) ACTIONS
|
||
// ============================================
|
||
|
||
// Phasen-Optionen wie auf der Website
|
||
const INPUT_PHASES = ['L1', 'L2', 'L3', '3P', '3P+N', 'PE'];
|
||
const OUTPUT_PHASES = ['L1N', 'L2N', 'L3N', 'N', '3P+N', 'PE', 'DATA'];
|
||
|
||
/**
|
||
* Phasenfarbe ermitteln (DIN VDE Farben)
|
||
*/
|
||
function getPhaseColor(type) {
|
||
const colors = {
|
||
'L1': '#8B4513', 'L2': '#1a1a1a', 'L3': '#666',
|
||
'N': '#0066cc', 'PE': '#27ae60',
|
||
'L1N': '#8B4513', 'L2N': '#1a1a1a', 'L3N': '#666',
|
||
'3P': '#e74c3c', '3P+N': '#e74c3c', 'DATA': '#9b59b6'
|
||
};
|
||
return colors[type] || '#888';
|
||
}
|
||
|
||
/**
|
||
* Abgangsseite-Button setzen
|
||
*/
|
||
function setSideButton(side) {
|
||
$('.side-btn').removeClass('selected');
|
||
$(`.side-btn[data-side="${side}"]`).addClass('selected');
|
||
}
|
||
|
||
/**
|
||
* Gewählte Abgangsseite auslesen
|
||
*/
|
||
function getSelectedSide() {
|
||
return $('.side-btn.selected').data('side') || 'bottom';
|
||
}
|
||
|
||
/**
|
||
* Typ-Select je nach Richtung befüllen (wie Website)
|
||
*/
|
||
function renderTypeSelect(direction, selectedType) {
|
||
const phases = direction === 'input' ? INPUT_PHASES : OUTPUT_PHASES;
|
||
let html = '<option value="">-- Kein Typ --</option>';
|
||
phases.forEach(p => {
|
||
const sel = (p === selectedType) ? ' selected' : '';
|
||
html += `<option value="${p}"${sel}>${p}</option>`;
|
||
});
|
||
$('#conn-type').html(html);
|
||
}
|
||
|
||
/**
|
||
* Klick auf Terminal-Zelle (Input oder Output)
|
||
*/
|
||
function handleTerminalClick(e) {
|
||
e.stopPropagation();
|
||
const $point = $(this);
|
||
const eqId = $point.data('equipment-id');
|
||
const direction = $point.data('direction');
|
||
const connId = $point.data('connection-id');
|
||
|
||
App.connectionEquipmentId = eqId;
|
||
App.connectionDirection = direction;
|
||
|
||
// Typ-Select befüllen
|
||
renderTypeSelect(direction, '');
|
||
|
||
if (connId) {
|
||
// Bearbeiten
|
||
App.editConnectionId = connId;
|
||
$('#connection-modal-title').text('Verbindung bearbeiten');
|
||
$('#btn-delete-connection').removeClass('hidden');
|
||
|
||
const conn = direction === 'input'
|
||
? App.inputs.find(i => i.id == connId)
|
||
: App.outputs.find(o => o.id == connId);
|
||
|
||
if (conn) {
|
||
renderTypeSelect(direction, conn.connection_type);
|
||
$('#conn-color').val(conn.color || '#3498db');
|
||
$('#conn-label').val(conn.output_label || '');
|
||
$('#conn-medium-type').val(conn.medium_type || '');
|
||
$('#conn-medium-spec').val(conn.medium_spec || '');
|
||
$('#conn-medium-length').val(conn.medium_length || '');
|
||
setSideButton(conn.is_top ? 'top' : 'bottom');
|
||
}
|
||
} else {
|
||
// Neu anlegen
|
||
App.editConnectionId = null;
|
||
$('#connection-modal-title').text(direction === 'input' ? 'Einspeisung' : 'Abgang');
|
||
$('#btn-delete-connection').addClass('hidden');
|
||
$('#conn-color').val('#3498db');
|
||
$('#conn-label').val('');
|
||
$('#conn-medium-type').val('');
|
||
$('#conn-medium-spec').val('');
|
||
$('#conn-medium-length').val('');
|
||
setSideButton('bottom');
|
||
}
|
||
|
||
// Medium-Felder nur bei Abgang zeigen
|
||
$('#conn-output-fields').toggle(direction === 'output');
|
||
|
||
openModal('connection');
|
||
}
|
||
|
||
/**
|
||
* Connection speichern (Neu oder Update)
|
||
*/
|
||
async function handleSaveConnection() {
|
||
const connectionType = $('#conn-type').val() || '';
|
||
const color = $('#conn-color').val() || '#3498db';
|
||
const outputLabel = $('#conn-label').val().trim();
|
||
const isOutput = App.connectionDirection === 'output';
|
||
const mediumType = isOutput ? ($('#conn-medium-type').val().trim() || '') : '';
|
||
const mediumSpec = isOutput ? ($('#conn-medium-spec').val().trim() || '') : '';
|
||
const mediumLength = isOutput ? ($('#conn-medium-length').val().trim() || '') : '';
|
||
const isTop = isOutput && getSelectedSide() === 'top';
|
||
// source_terminal_id wie Website: t1=oben, t2=unten
|
||
const sourceTerminalId = isOutput ? (isTop ? 't1' : 't2') : '';
|
||
const sourceTerminal = isOutput ? (isTop ? 'top' : 'output') : '';
|
||
|
||
closeModal('connection');
|
||
|
||
if (App.editConnectionId) {
|
||
// Update
|
||
const data = {
|
||
action: 'update_connection',
|
||
connection_id: App.editConnectionId,
|
||
connection_type: connectionType,
|
||
color: color,
|
||
output_label: outputLabel,
|
||
medium_type: mediumType,
|
||
medium_spec: mediumSpec,
|
||
medium_length: mediumLength,
|
||
source_terminal: sourceTerminal,
|
||
source_terminal_id: sourceTerminalId
|
||
};
|
||
|
||
const updateLocal = (conn) => {
|
||
if (!conn) return;
|
||
conn.connection_type = connectionType;
|
||
conn.color = color;
|
||
conn.output_label = outputLabel;
|
||
conn.medium_type = mediumType;
|
||
conn.medium_spec = mediumSpec;
|
||
conn.medium_length = mediumLength;
|
||
if (isOutput) {
|
||
conn.is_top = isTop;
|
||
conn.source_terminal_id = sourceTerminalId;
|
||
}
|
||
};
|
||
|
||
if (App.isOnline) {
|
||
try {
|
||
const response = await apiCall('ajax/pwa_api.php', data);
|
||
if (response.success) {
|
||
const list = App.connectionDirection === 'input' ? App.inputs : App.outputs;
|
||
updateLocal(list.find(c => c.id == App.editConnectionId));
|
||
renderEditor();
|
||
showToast('Verbindung aktualisiert', 'success');
|
||
} else {
|
||
showToast(response.error || 'Fehler', 'error');
|
||
}
|
||
} catch (err) {
|
||
queueOfflineAction(data);
|
||
showToast('Wird synchronisiert...', 'warning');
|
||
}
|
||
} else {
|
||
queueOfflineAction(data);
|
||
const list = App.connectionDirection === 'input' ? App.inputs : App.outputs;
|
||
updateLocal(list.find(c => c.id == App.editConnectionId));
|
||
renderEditor();
|
||
showToast('Wird synchronisiert...', 'warning');
|
||
}
|
||
} else {
|
||
// Neu anlegen
|
||
const data = {
|
||
action: 'create_connection',
|
||
equipment_id: App.connectionEquipmentId,
|
||
direction: App.connectionDirection,
|
||
connection_type: connectionType,
|
||
color: color,
|
||
output_label: outputLabel,
|
||
medium_type: mediumType,
|
||
medium_spec: mediumSpec,
|
||
medium_length: mediumLength,
|
||
source_terminal: sourceTerminal,
|
||
source_terminal_id: sourceTerminalId
|
||
};
|
||
|
||
const newConnBase = {
|
||
connection_type: connectionType,
|
||
color: color,
|
||
output_label: outputLabel,
|
||
medium_type: mediumType,
|
||
medium_spec: mediumSpec,
|
||
medium_length: mediumLength,
|
||
is_top: isTop,
|
||
source_terminal_id: sourceTerminalId
|
||
};
|
||
|
||
if (App.isOnline) {
|
||
try {
|
||
const response = await apiCall('ajax/pwa_api.php', data);
|
||
if (response.success) {
|
||
const newConn = Object.assign({ id: response.connection_id }, newConnBase);
|
||
if (App.connectionDirection === 'input') {
|
||
newConn.fk_target = App.connectionEquipmentId;
|
||
App.inputs.push(newConn);
|
||
} else {
|
||
newConn.fk_source = App.connectionEquipmentId;
|
||
App.outputs.push(newConn);
|
||
}
|
||
renderEditor();
|
||
showToast('Verbindung angelegt', 'success');
|
||
} else {
|
||
showToast(response.error || 'Fehler', 'error');
|
||
}
|
||
} catch (err) {
|
||
queueOfflineAction(data);
|
||
showToast('Wird synchronisiert...', 'warning');
|
||
}
|
||
} else {
|
||
queueOfflineAction(data);
|
||
const newConn = Object.assign({ id: 'temp_' + Date.now() }, newConnBase);
|
||
if (App.connectionDirection === 'input') {
|
||
newConn.fk_target = App.connectionEquipmentId;
|
||
App.inputs.push(newConn);
|
||
} else {
|
||
newConn.fk_source = App.connectionEquipmentId;
|
||
App.outputs.push(newConn);
|
||
}
|
||
renderEditor();
|
||
showToast('Wird synchronisiert...', 'warning');
|
||
}
|
||
}
|
||
|
||
App.editConnectionId = null;
|
||
}
|
||
|
||
/**
|
||
* Connection löschen (mit Bestätigung)
|
||
*/
|
||
function handleDeleteConnectionConfirm() {
|
||
const connId = App.editConnectionId;
|
||
if (!connId) return;
|
||
|
||
$('#confirm-title').text('Verbindung löschen?');
|
||
$('#confirm-message').text('Diese Verbindung wirklich löschen?');
|
||
|
||
App.confirmCallback = () => deleteConnection(connId);
|
||
|
||
closeModal('connection');
|
||
openModal('confirm');
|
||
}
|
||
|
||
async function deleteConnection(connId) {
|
||
const data = {
|
||
action: 'delete_connection',
|
||
connection_id: connId
|
||
};
|
||
|
||
if (App.isOnline) {
|
||
try {
|
||
const response = await apiCall('ajax/pwa_api.php', data);
|
||
if (response.success) {
|
||
App.outputs = App.outputs.filter(o => o.id != connId);
|
||
App.inputs = App.inputs.filter(i => i.id != connId);
|
||
renderEditor();
|
||
showToast('Verbindung gelöscht', 'success');
|
||
} else {
|
||
showToast(response.error || 'Fehler', 'error');
|
||
}
|
||
} catch (err) {
|
||
queueOfflineAction(data);
|
||
App.outputs = App.outputs.filter(o => o.id != connId);
|
||
App.inputs = App.inputs.filter(i => i.id != connId);
|
||
renderEditor();
|
||
}
|
||
} else {
|
||
queueOfflineAction(data);
|
||
App.outputs = App.outputs.filter(o => o.id != connId);
|
||
App.inputs = App.inputs.filter(i => i.id != connId);
|
||
renderEditor();
|
||
showToast('Wird synchronisiert...', 'warning');
|
||
}
|
||
|
||
App.editConnectionId = null;
|
||
}
|
||
|
||
// ============================================
|
||
// 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);
|