/** * BadProjekt-Pass Widget * WordPress Integration */ (function() { 'use strict'; // Konfiguration aus data-Attributen const script = document.currentScript || document.querySelector('script[data-tenant]'); const config = { tenantId: script?.dataset.tenant || '', locale: script?.dataset.locale || 'de-DE', appBaseUrl: script?.dataset.appBaseUrl || window.location.origin, apiUrl: script?.dataset.apiUrl || window.location.origin + '/api' }; // State let projectToken = localStorage.getItem('bp_project_token'); let projectCode = localStorage.getItem('bp_project_code'); let projectId = localStorage.getItem('bp_project_id'); // Session Tracking let sessionId = sessionStorage.getItem('bp_session_id') || generateSessionId(); if (!sessionStorage.getItem('bp_session_id')) { sessionStorage.setItem('bp_session_id', sessionId); } // Scroll-Tracking let maxScrollDepth = 0; let pageStartTime = Date.now(); /** * API Client */ const api = { async request(url, options = {}) { const headers = { 'Content-Type': 'application/json', ...options.headers }; if (projectToken) { headers['Authorization'] = `Bearer ${projectToken}`; } const fullUrl = config.apiUrl + url; console.log('[API] Request:', { method: options.method || 'GET', url: fullUrl, hasToken: !!projectToken, body: options.body ? JSON.parse(options.body) : null }); try { const response = await fetch(fullUrl, { ...options, headers }); console.log('[API] Response Status:', response.status, response.statusText); // Prüfe ob Token erneuert wurde (aus Header) const renewedTokenHeader = response.headers.get('X-Token-Renewed'); if (renewedTokenHeader) { console.log('[API] Token wurde erneuert, aktualisiere...'); projectToken = renewedTokenHeader; localStorage.setItem('bp_project_token', projectToken); console.log('[API] Token aktualisiert'); } if (!response.ok) { // Versuche Error-Body zu lesen let errorBody = null; try { errorBody = await response.text(); console.error('[API] Error Response Body:', errorBody); } catch (e) { // Ignoriere Fehler beim Lesen des Error-Body } throw new Error(`HTTP ${response.status}: ${errorBody || response.statusText}`); } const jsonData = await response.json(); console.log('[API] Response Data:', jsonData); // Prüfe ob Token erneuert wurde (aus Body - Fallback) if (jsonData.data && jsonData.data.token) { console.log('[API] Token wurde erneuert (aus Body), aktualisiere...'); projectToken = jsonData.data.token; localStorage.setItem('bp_project_token', projectToken); console.log('[API] Token aktualisiert'); } return jsonData; } catch (error) { console.error('[API] Request Error:', error); console.error('[API] Error Details:', { message: error.message, stack: error.stack, name: error.name }); throw error; } }, async createProject(data) { return this.request('/public/projects', { method: 'POST', body: JSON.stringify(data) }); }, async restoreProject(identifier, password, tenantId) { return this.request('/public/projects/restore', { method: 'POST', body: JSON.stringify({ identifier: identifier, password: password, tenant_id: tenantId }) }); }, async requestPasswordReset(identifier, tenantId) { return this.request('/public/password/reset-request', { method: 'POST', body: JSON.stringify({ identifier: identifier, tenant_id: tenantId }) }); }, async resetPassword(resetToken, newPassword) { return this.request('/public/password/reset', { method: 'POST', body: JSON.stringify({ reset_token: resetToken, password: newPassword }) }); }, async addProduct(projectId, productData) { return this.request(`/projects/${projectId}/products`, { method: 'POST', body: JSON.stringify(productData) }); }, async toggleMerkliste(projectId, merklisteData) { return this.request(`/projects/${projectId}/merkliste`, { method: 'POST', body: JSON.stringify(merklisteData) }); }, async saveConfiguration(projectId, configData) { return this.request(`/projects/${projectId}/configurations`, { method: 'POST', body: JSON.stringify(configData) }); }, async trackDownload(projectId, downloadData) { return this.request(`/projects/${projectId}/downloads`, { method: 'POST', body: JSON.stringify(downloadData) }); }, async checkMerklisteStatus(projectId, externalId, kategorie) { return this.request(`/projects/${projectId}/merkliste/check?external_id=${encodeURIComponent(externalId)}&kategorie=${encodeURIComponent(kategorie)}`, { method: 'GET' }); } }; /** * Toast-System für Fehlermeldungen */ function showToast(message, type = 'error') { const toast = document.createElement('div'); toast.className = `bp-toast bp-toast-${type}`; toast.textContent = message; // Styles Object.assign(toast.style, { position: 'fixed', bottom: '20px', right: '20px', background: type === 'error' ? '#f44336' : '#4CAF50', color: 'white', padding: '12px 20px', borderRadius: '4px', boxShadow: '0 4px 12px rgba(0,0,0,0.15)', zIndex: '10000', fontSize: '14px', maxWidth: '300px', animation: 'slideInRight 0.3s ease-out' }); // Animation CSS hinzufügen (falls noch nicht vorhanden) if (!document.getElementById('bp-toast-styles')) { const style = document.createElement('style'); style.id = 'bp-toast-styles'; style.textContent = ` @keyframes slideInRight { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOutRight { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } `; document.head.appendChild(style); } document.body.appendChild(toast); // Nach 3 Sekunden entfernen setTimeout(() => { toast.style.animation = 'slideOutRight 0.3s ease-out'; setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, 300); }, 3000); } /** * UTM Parameter auslesen */ function getUtmParams() { const params = new URLSearchParams(window.location.search); return { utm_source: params.get('utm_source'), utm_medium: params.get('utm_medium'), utm_campaign: params.get('utm_campaign'), utm_content: params.get('utm_content'), utm_term: params.get('utm_term') }; } /** * Generiert Session-ID */ function generateSessionId() { return 'bp_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); } /** * Ermittelt Herkunft (Referrer oder Direktaufruf) */ function getSource() { const referrer = document.referrer; if (!referrer || referrer === '' || referrer === window.location.href) { return 'Direkt Aufruf - Webseite'; } try { const referrerUrl = new URL(referrer); const currentUrl = new URL(window.location.href); // Wenn Referrer von derselben Domain kommt, als "Interne Navigation" markieren if (referrerUrl.hostname === currentUrl.hostname) { return 'Interne Navigation'; } // Externe Referrer zurückgeben return referrer; } catch (e) { // Falls URL-Parsing fehlschlägt, Referrer trotzdem zurückgeben return referrer; } } /** * Trackt Seitenbesuch und Scrolltiefe */ function trackPageVisit() { // Reset für neue Seite maxScrollDepth = 0; pageStartTime = Date.now(); if (!projectId) { return; } // Scrolltiefe tracken function updateScrollDepth() { const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const documentHeight = document.documentElement.scrollHeight - window.innerHeight; const scrollPercent = documentHeight > 0 ? Math.round((scrollTop / documentHeight) * 100) : 0; if (scrollPercent > maxScrollDepth) { maxScrollDepth = scrollPercent; } } // Scroll-Event-Listener let scrollTimeout; window.addEventListener('scroll', () => { clearTimeout(scrollTimeout); scrollTimeout = setTimeout(updateScrollDepth, 100); }, { passive: true }); // Initiale Scrolltiefe updateScrollDepth(); // Beim Verlassen der Seite: Daten senden function sendSessionData() { if (!projectId) { return; } const timeOnPage = Math.round((Date.now() - pageStartTime) / 1000); // Sende Tracking-Daten (auch wenn scroll_depth 0 ist) const data = JSON.stringify({ session_id: sessionId, page_url: window.location.href, page_title: document.title, scroll_depth: maxScrollDepth, time_on_page: timeOnPage }); // Verwende fetch mit keepalive (sendBeacon unterstützt keine Authorization Header) fetch(config.apiUrl + `/projects/${projectId}/session-tracking`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': projectToken ? `Bearer ${projectToken}` : '' }, body: data, keepalive: true // Wichtig für zuverlässiges Tracking beim Seitenwechsel }).catch(() => {}); // Fehler ignorieren } // Beim Verlassen der Seite window.addEventListener('beforeunload', sendSessionData); // Auch beim Seitenwechsel (falls möglich) window.addEventListener('pagehide', sendSessionData); } /** * Popup erstellen */ function createPopup() { // Prüfe ob Popup nicht angezeigt werden soll (permanent) if (localStorage.getItem('bp_popup_dont_show') === 'true') { return null; } // Prüfe ob Popup bereits in dieser Session gezeigt wurde if (sessionStorage.getItem('bp_popup_shown') === 'true') { return null; } const popup = document.createElement('div'); popup.id = 'bp-popup'; popup.className = 'bp-popup'; popup.innerHTML = `

Erstelle deinen kostenlosen BadProjekt-Pass

Speichere eine Lieblingsseiten, lade Inspirationen hoch und erhalte deinen persönlichen QR-Code für die Ausstellung. Ohne Verpflichtung, jederzeit löschbar.

Merkliste
Checklisten & Broschüren sichern
Eigene Bad-Fotos hochladen
QR-Code für Beratung vor Ort
Bitte wählen Sie ein sicheres Passwort (mindestens 8 Zeichen)
`; document.body.appendChild(popup); // Tab-Switching const tabButtons = popup.querySelectorAll('.bp-tab-btn'); const identifierInput = popup.querySelector('#bp-identifier'); const formLabel = popup.querySelector('.bp-form-label'); tabButtons.forEach(btn => { btn.addEventListener('click', () => { tabButtons.forEach(b => b.classList.remove('active')); btn.classList.add('active'); const tab = btn.dataset.tab; if (tab === 'whatsapp') { identifierInput.type = 'tel'; identifierInput.placeholder = 'z. B. +49 123 456789'; formLabel.textContent = 'WhatsApp-Nummer'; } else { identifierInput.type = 'text'; identifierInput.placeholder = 'z. B. max.mustermann@mail.de'; formLabel.textContent = 'E-Mail-Adresse'; } }); }); // Close Button - Markiere Popup als gezeigt const closePopup = () => { sessionStorage.setItem('bp_popup_shown', 'true'); popup.remove(); }; popup.querySelector('.bp-popup-close').addEventListener('click', closePopup); popup.querySelector('.bp-popup-overlay').addEventListener('click', closePopup); // "Nicht wieder anzeigen" Button popup.querySelector('#bp-dont-show-btn').addEventListener('click', () => { localStorage.setItem('bp_popup_dont_show', 'true'); popup.remove(); }); // Wiederherstellungs-Button const restoreBtn = popup.querySelector('#bp-restore-btn'); const restoreInput = popup.querySelector('#bp-restore-identifier'); const restoreGroup = popup.querySelector('.bp-restore-group'); // Zeige Wiederherstellungs-Feld wenn "Bereits eine ID vorhanden?" Link geklickt wird const showRestoreLink = document.createElement('a'); showRestoreLink.href = '#'; showRestoreLink.className = 'bp-show-restore'; showRestoreLink.textContent = 'Bereits eine ID vorhanden?'; showRestoreLink.style.cssText = 'display: block; margin-top: 10px; color: #666; text-decoration: underline; font-size: 14px;'; showRestoreLink.addEventListener('click', (e) => { e.preventDefault(); restoreGroup.style.display = 'block'; showRestoreLink.style.display = 'none'; // Setze required nur wenn Feld sichtbar ist const restorePasswordInput = popup.querySelector('#bp-restore-password'); if (restorePasswordInput) { restorePasswordInput.setAttribute('required', 'required'); } }); popup.querySelector('.bp-form-group').appendChild(showRestoreLink); // Passwort vergessen Link const forgotPasswordLink = popup.querySelector('#bp-forgot-password-link'); if (forgotPasswordLink) { forgotPasswordLink.addEventListener('click', (e) => { e.preventDefault(); showPasswordResetDialog(popup); }); } restoreBtn.addEventListener('click', async () => { const identifier = restoreInput.value.trim(); const password = popup.querySelector('#bp-restore-password').value; if (!identifier) { showToast('Bitte geben Sie einen Projekt-Code, E-Mail oder Telefonnummer ein', 'error'); return; } if (!password) { showToast('Bitte geben Sie Ihr Passwort ein', 'error'); return; } try { const result = await api.restoreProject(identifier, password, config.tenantId); if (result.success && result.data) { projectToken = result.data.token; projectCode = result.data.project_code; projectId = result.data.project.id; localStorage.setItem('bp_project_token', projectToken); localStorage.setItem('bp_project_code', projectCode); localStorage.setItem('bp_project_id', projectId); sessionStorage.setItem('bp_popup_shown', 'true'); popup.remove(); showToast('Projekt erfolgreich wiederhergestellt!', 'success'); // Mache Buttons sichtbar document.querySelectorAll('.bp-add-to-merkliste, [data-bp-merkliste-id], .bp-add-to-project, [data-bp-product-id], .bp-download-link, [data-bp-download-id]').forEach(element => { element.classList.remove('hidden'); }); // Initialisiere Buttons erneut initMerklisteButtons(); initProductButtons(); initDownloadLinks(); // Starte Session-Tracking trackPageVisit(); } } catch (error) { showToast(error.message || 'Projekt nicht gefunden. Bitte prüfen Sie Ihre Eingabe.', 'error'); } }); // Form Submit popup.querySelector('#bp-popup-form').addEventListener('submit', async (e) => { e.preventDefault(); // Entferne required vom restore_password Feld, da es nicht Teil des Hauptformulars ist const restorePasswordInput = popup.querySelector('#bp-restore-password'); if (restorePasswordInput) { restorePasswordInput.removeAttribute('required'); } const formData = new FormData(e.target); const identifier = formData.get('identifier'); const password = formData.get('password'); const consent = formData.get('consent_tracking') === '1'; const consentNewsletter = formData.get('consent_newsletter') === '1'; // Passwort-Validierung if (!password || password.length < 8) { showToast('Passwort muss mindestens 8 Zeichen lang sein', 'error'); return; } const activeTab = popup.querySelector('.bp-tab-btn.active').dataset.tab; const isEmail = activeTab === 'email' || identifier.includes('@'); const projectData = { tenant_id: config.tenantId, password: password, // Passwort hinzufügen consent_tracking: consent ? 1 : 0, consent_newsletter: consentNewsletter ? 1 : 0, source: getSource() }; if (isEmail) { projectData.email = identifier; } else { projectData.phone = identifier; } if (consent) { Object.assign(projectData, getUtmParams()); } try { const result = await api.createProject(projectData); if (result.success && result.data) { projectToken = result.data.token; projectCode = result.data.project_code; projectId = result.data.project.id; localStorage.setItem('bp_project_token', projectToken); localStorage.setItem('bp_project_code', projectCode); localStorage.setItem('bp_project_id', projectId); sessionStorage.setItem('bp_popup_shown', 'true'); popup.remove(); showToast('Projektpass erfolgreich erstellt!', 'success'); // Mache Buttons sichtbar nach Projekt-Erstellung document.querySelectorAll('.bp-add-to-merkliste, [data-bp-merkliste-id], .bp-add-to-project, [data-bp-product-id], .bp-download-link, [data-bp-download-id]').forEach(element => { element.classList.remove('hidden'); }); // Initialisiere Buttons erneut mit neuem Projekt initMerklisteButtons(); initProductButtons(); initDownloadLinks(); // Starte Session-Tracking für neues Projekt trackPageVisit(); } } catch (error) { showToast('Fehler beim Erstellen des Projektpasses', 'error'); } }); return popup; } /** * Zeigt Passwort-Reset-Dialog */ function showPasswordResetDialog(parentPopup) { const dialog = document.createElement('div'); dialog.className = 'bp-popup'; dialog.innerHTML = `

Passwort zurücksetzen

Geben Sie Ihre E-Mail-Adresse oder Telefonnummer ein. Wir senden Ihnen einen Link zum Zurücksetzen des Passworts.

`; document.body.appendChild(dialog); // Close Button dialog.querySelector('.bp-popup-close').addEventListener('click', () => { dialog.remove(); }); dialog.querySelector('.bp-popup-overlay').addEventListener('click', () => { dialog.remove(); }); // Reset-Request Form dialog.querySelector('#bp-reset-request-form').addEventListener('submit', async (e) => { e.preventDefault(); const identifier = dialog.querySelector('#bp-reset-identifier').value.trim(); if (!identifier) { showToast('Bitte geben Sie eine E-Mail-Adresse oder Telefonnummer ein', 'error'); return; } try { const result = await api.requestPasswordReset(identifier, config.tenantId); if (result.success) { // Zeige Token-Eingabe (für Entwicklung - in Produktion sollte Token per E-Mail/SMS kommen) if (result.data && result.data.reset_token) { dialog.querySelector('#bp-reset-token').value = result.data.reset_token; dialog.querySelector('#bp-reset-token-section').style.display = 'block'; showToast('Reset-Link wurde gesendet. Bitte prüfen Sie Ihre E-Mail/SMS.', 'success'); } else { showToast('Reset-Link wurde gesendet. Bitte prüfen Sie Ihre E-Mail/SMS.', 'success'); } } } catch (error) { showToast(error.message || 'Fehler beim Anfordern des Reset-Links', 'error'); } }); // Reset Password Button dialog.querySelector('#bp-reset-password-btn').addEventListener('click', async () => { const resetToken = dialog.querySelector('#bp-reset-token').value.trim(); const newPassword = dialog.querySelector('#bp-new-password').value; if (!resetToken) { showToast('Bitte geben Sie den Reset-Token ein', 'error'); return; } if (!newPassword || newPassword.length < 8) { showToast('Passwort muss mindestens 8 Zeichen lang sein', 'error'); return; } try { const result = await api.resetPassword(resetToken, newPassword); if (result.success) { showToast('Passwort erfolgreich zurückgesetzt!', 'success'); dialog.remove(); if (parentPopup) { parentPopup.remove(); } } } catch (error) { showToast(error.message || 'Fehler beim Zurücksetzen des Passworts', 'error'); } }); } /** * Erfolgsmeldung */ function showSuccessMessage() { const message = document.createElement('div'); message.className = 'bp-success-message'; message.textContent = 'Projektpass erstellt!'; document.body.appendChild(message); setTimeout(() => message.remove(), 3000); } /** * "Mein Projekt" Button */ function initProjectButton() { const buttons = document.querySelectorAll('[id="bp-pass-entry-button"], [data-bp-entry="true"]'); buttons.forEach(button => { button.addEventListener('click', () => { if (projectCode) { window.open(`${config.appBaseUrl}/p/${projectCode}`, '_blank'); } else { createPopup(); } }); }); } /** * Prüft Button-Status und aktualisiert visuell (für Merkliste-Buttons) */ async function updateMerklisteButtonStatus(button) { if (!projectId) { return; } const externalId = button.dataset.bpMerklisteId || button.dataset.bpProductId || ''; const kategorie = button.dataset.bpMerklisteKategorie || button.dataset.bpMerklisteKategorie || 'Produkt'; if (!externalId) { console.warn('updateMerklisteButtonStatus: externalId fehlt', button); return; } try { const status = await api.checkMerklisteStatus(projectId, externalId, kategorie); if (status && status.success && status.data?.in_merkliste) { button.classList.add('merkliste-true'); button.textContent = button.dataset.bpMerklisteTextAdded || button.dataset.bpProductTextAdded || '✓ In Merkliste'; } else { button.classList.remove('merkliste-true'); button.textContent = button.dataset.bpMerklisteTextAdd || button.dataset.bpProductTextAdd || 'Zur Merkliste'; } } catch (error) { console.warn('updateMerklisteButtonStatus Fehler:', error); // Bei Fehler: Status basierend auf aktueller Klasse setzen (Fallback) if (!button.classList.contains('merkliste-true')) { button.classList.remove('merkliste-true'); button.textContent = button.dataset.bpMerklisteTextAdd || button.dataset.bpProductTextAdd || 'Zur Merkliste'; } } } /** * Merkliste-Buttons (neue allgemeine Funktion mit Toggle) */ function initMerklisteButtons() { const buttons = document.querySelectorAll('.bp-add-to-merkliste, [data-bp-merkliste-id]'); console.log('[embed.js] initMerklisteButtons: Gefunden', buttons.length, 'Buttons'); buttons.forEach((button, index) => { console.log(`[embed.js] Initialisiere Button ${index + 1}:`, { externalId: button.dataset.bpMerklisteId || button.dataset.bpProductId, kategorie: button.dataset.bpMerklisteKategorie || 'Produkt', hasMerklisteTrue: button.classList.contains('merkliste-true') }); // Entferne alte Event-Listener (verhindere Duplikate) const newButton = button.cloneNode(true); button.parentNode.replaceChild(newButton, button); // Entferne "hidden" Klasse nur wenn Projekt vorhanden if (projectId) { newButton.classList.remove('hidden'); // Prüfe initialen Status updateMerklisteButtonStatus(newButton); } newButton.addEventListener('click', async (e) => { console.log('[embed.js] Merkliste-Button geklickt!', { externalId: newButton.dataset.bpMerklisteId, kategorie: newButton.dataset.bpMerklisteKategorie, projectId: projectId }); e.preventDefault(); e.stopPropagation(); if (!projectId) { console.log('[embed.js] Kein projectId - öffne Popup'); createPopup(); return; } const kategorie = newButton.dataset.bpMerklisteKategorie || 'Produkt'; if (!kategorie) { showToast('Kategorie fehlt! Bitte data-bp-merkliste-kategorie setzen.', 'error'); return; } const externalId = newButton.dataset.bpMerklisteId || ''; if (!externalId) { showToast('ID fehlt! Bitte data-bp-merkliste-id setzen.', 'error'); return; } // Prüfe aktuellen Status VOR dem Toggle (optimistisches Update) const isCurrentlyInMerkliste = newButton.classList.contains('merkliste-true'); // Optimistisches Update (sofort visuell ändern) if (isCurrentlyInMerkliste) { newButton.classList.remove('merkliste-true'); newButton.textContent = newButton.dataset.bpMerklisteTextAdd || 'Zur Merkliste'; } else { newButton.classList.add('merkliste-true'); newButton.textContent = newButton.dataset.bpMerklisteTextAdded || '✓ In Merkliste'; } // Deaktiviere Button während Request newButton.disabled = true; const merklisteData = { external_id: externalId, titel: newButton.dataset.bpMerklisteTitel || newButton.dataset.bpMerklisteName || '', kategorie: kategorie, bild: newButton.dataset.bpMerklisteBild || newButton.dataset.bpMerklisteImage || '', marke: newButton.dataset.bpMerklisteMarke || newButton.dataset.bpMerklisteBrand || null, serie: newButton.dataset.bpMerklisteSerie || newButton.dataset.bpMerklisteSeries || null, sku: newButton.dataset.bpMerklisteSku || null, source_url: window.location.href, source_system: 'wordpress' }; try { console.log('[Merkliste] Toggle Start:', { externalId, kategorie, isCurrentlyInMerkliste, projectId, merklisteData }); const result = await api.toggleMerkliste(projectId, merklisteData); console.log('[Merkliste] Toggle Response:', result); console.log('[Merkliste] Response Structure:', { success: result?.success, data: result?.data, action: result?.data?.action, message: result?.message }); if (!result) { throw new Error('Keine Response vom Server erhalten'); } if (!result.success) { throw new Error(result.message || result.error || 'Fehler beim Toggle'); } if (!result.data) { console.warn('[Merkliste] Response hat kein data-Objekt:', result); throw new Error('Ungültige Response-Struktur'); } // Korrigiere Status basierend auf Response const action = result.data.action; console.log('[Merkliste] Action:', action); if (action === 'removed') { newButton.classList.remove('merkliste-true'); newButton.textContent = newButton.dataset.bpMerklisteTextAdd || 'Zur Merkliste'; showToast('Aus Merkliste entfernt', 'success'); console.log('[Merkliste] Button-Status auf "entfernt" gesetzt'); } else if (action === 'added' || action === 'already_exists') { newButton.classList.add('merkliste-true'); newButton.textContent = newButton.dataset.bpMerklisteTextAdded || '✓ In Merkliste'; if (action === 'already_exists') { showToast('Bereits in Merkliste', 'info'); } else { showToast('Zur Merkliste hinzugefügt', 'success'); } console.log('[Merkliste] Button-Status auf "hinzugefügt" gesetzt'); } else { console.warn('[Merkliste] Unbekannte Action:', action); // Fallback: Prüfe Status vom Server setTimeout(() => { updateMerklisteButtonStatus(newButton); }, 500); } // Prüfe Status nochmal (sicherstellen) setTimeout(() => { console.log('[Merkliste] Prüfe Button-Status erneut...'); updateMerklisteButtonStatus(newButton); }, 500); } catch (error) { console.error('[Merkliste] Fehler beim Toggle:', error); console.error('[Merkliste] Error Details:', { message: error.message, stack: error.stack, name: error.name }); // Revert optimistisches Update bei Fehler if (isCurrentlyInMerkliste) { newButton.classList.add('merkliste-true'); newButton.textContent = newButton.dataset.bpMerklisteTextAdded || '✓ In Merkliste'; console.log('[Merkliste] Revert: Button zurück auf "in Merkliste"'); } else { newButton.classList.remove('merkliste-true'); newButton.textContent = newButton.dataset.bpMerklisteTextAdd || 'Zur Merkliste'; console.log('[Merkliste] Revert: Button zurück auf "nicht in Merkliste"'); } showToast(error.message || 'Fehler beim Aktualisieren der Merkliste', 'error'); } finally { newButton.disabled = false; } }); }); } /** * Produkt-Buttons (alte Funktion für Rückwärtskompatibilität) */ function initProductButtons() { const buttons = document.querySelectorAll('.bp-add-to-project, [data-bp-product-id]'); buttons.forEach(button => { // Entferne alte Event-Listener (verhindere Duplikate) const newButton = button.cloneNode(true); button.parentNode.replaceChild(newButton, button); // Entferne "hidden" Klasse nur wenn Projekt vorhanden if (projectId) { newButton.classList.remove('hidden'); const externalId = newButton.dataset.bpProductId || ''; if (externalId) { updateMerklisteButtonStatus(newButton); } } newButton.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); if (!projectId) { createPopup(); return; } // Konvertiere alte Struktur zu neuer Merkliste-Struktur const externalId = newButton.dataset.bpProductId || ''; const merklisteData = { external_id: externalId, titel: newButton.dataset.bpProductName || '', kategorie: 'Produkt', bild: newButton.dataset.bpProductImage || '', marke: newButton.dataset.bpProductBrand || null, serie: newButton.dataset.bpProductSeries || null, sku: newButton.dataset.bpProductSku || null, source_url: window.location.href, source_system: 'wordpress' }; // Prüfe aktuellen Status VOR dem Toggle (optimistisches Update) const isCurrentlyInMerkliste = newButton.classList.contains('merkliste-true'); // Optimistisches Update (sofort visuell ändern) if (isCurrentlyInMerkliste) { newButton.classList.remove('merkliste-true'); newButton.textContent = newButton.dataset.bpProductTextAdd || 'Zum BadProjekt-Pass hinzufügen'; } else { newButton.classList.add('merkliste-true'); newButton.textContent = newButton.dataset.bpProductTextAdded || '✓ Hinzugefügt'; } // Deaktiviere Button während Request newButton.disabled = true; try { console.log('[Merkliste-Product] Toggle Start:', { externalId, isCurrentlyInMerkliste, projectId, merklisteData }); const result = await api.toggleMerkliste(projectId, merklisteData); console.log('[Merkliste-Product] Toggle Response:', result); console.log('[Merkliste-Product] Response Structure:', { success: result?.success, data: result?.data, action: result?.data?.action, message: result?.message }); if (!result) { throw new Error('Keine Response vom Server erhalten'); } if (!result.success) { throw new Error(result.message || result.error || 'Fehler beim Toggle'); } if (!result.data) { console.warn('[Merkliste-Product] Response hat kein data-Objekt:', result); throw new Error('Ungültige Response-Struktur'); } // Korrigiere Status basierend auf Response const action = result.data.action; console.log('[Merkliste-Product] Action:', action); if (action === 'removed') { newButton.classList.remove('merkliste-true'); newButton.textContent = newButton.dataset.bpProductTextAdd || 'Zum BadProjekt-Pass hinzufügen'; showToast('Aus Merkliste entfernt', 'success'); console.log('[Merkliste-Product] Button-Status auf "entfernt" gesetzt'); } else if (action === 'added' || action === 'already_exists') { newButton.classList.add('merkliste-true'); newButton.textContent = newButton.dataset.bpProductTextAdded || '✓ Hinzugefügt'; if (action === 'already_exists') { showToast('Bereits in Merkliste', 'info'); } else { showToast('Zur Merkliste hinzugefügt', 'success'); } console.log('[Merkliste-Product] Button-Status auf "hinzugefügt" gesetzt'); } else { console.warn('[Merkliste-Product] Unbekannte Action:', action); // Fallback: Prüfe Status vom Server setTimeout(() => { updateMerklisteButtonStatus(newButton); }, 500); } // Prüfe Status nochmal (sicherstellen) setTimeout(() => { console.log('[Merkliste-Product] Prüfe Button-Status erneut...'); updateMerklisteButtonStatus(newButton); }, 500); } catch (error) { console.error('[Merkliste-Product] Fehler beim Toggle:', error); console.error('[Merkliste-Product] Error Details:', { message: error.message, stack: error.stack, name: error.name }); // Revert optimistisches Update bei Fehler if (isCurrentlyInMerkliste) { newButton.classList.add('merkliste-true'); newButton.textContent = newButton.dataset.bpProductTextAdded || '✓ Hinzugefügt'; console.log('[Merkliste-Product] Revert: Button zurück auf "in Merkliste"'); } else { newButton.classList.remove('merkliste-true'); newButton.textContent = newButton.dataset.bpProductTextAdd || 'Zum BadProjekt-Pass hinzufügen'; console.log('[Merkliste-Product] Revert: Button zurück auf "nicht in Merkliste"'); } showToast(error.message || 'Fehler beim Aktualisieren der Merkliste', 'error'); } finally { newButton.disabled = false; } }); }); } /** * Download-Links */ function initDownloadLinks() { const links = document.querySelectorAll('.bp-download-link, [data-bp-download-id]'); links.forEach(link => { // Entferne alte Event-Listener (falls vorhanden) durch Klonen const newLink = link.cloneNode(true); link.parentNode?.replaceChild(newLink, link); const linkToUse = newLink; // Entferne "hidden" Klasse nur wenn Projekt vorhanden if (projectId) { linkToUse.classList.remove('hidden'); } linkToUse.addEventListener('click', async (e) => { // Prüfe ob Link in neuem Tab geöffnet werden soll const openInNewTab = linkToUse.target === '_blank' || linkToUse.getAttribute('target') === '_blank'; const downloadUrl = linkToUse.href; if (!projectId) { // Wenn kein Projekt vorhanden, lasse Standard-Verhalten zu return; // Standard-Navigation läuft (inkl. target="_blank") } // Für target="_blank": Lasse Standard-Verhalten zu, tracke nur im Hintergrund // Das verhindert Popup-Blocker-Probleme if (openInNewTab) { // Lassen Sie den Link normal öffnen (kein preventDefault) // Tracking im Hintergrund (nicht-blockierend) const downloadData = { download_id: linkToUse.dataset.bpDownloadId || '', title: linkToUse.dataset.bpDownloadTitle || linkToUse.textContent, category: linkToUse.dataset.bpDownloadCategory || '', file_url: downloadUrl, thumbnail_url: linkToUse.dataset.bpDownloadThumbnail || null, source_page: window.location.href, source_system: 'wordpress' }; // Starte Tracking im Hintergrund (nicht-blockierend) api.trackDownload(projectId, downloadData).catch(error => { console.warn('Download tracking failed:', error); }); // Lassen Sie den Link normal öffnen (Standard-Verhalten) return; // Kein preventDefault, Link öffnet sich normal in neuem Tab } // Für gleichen Tab: Verhindere Navigation, tracke, dann navigiere e.preventDefault(); const downloadData = { download_id: linkToUse.dataset.bpDownloadId || '', title: linkToUse.dataset.bpDownloadTitle || linkToUse.textContent, category: linkToUse.dataset.bpDownloadCategory || '', file_url: downloadUrl, thumbnail_url: linkToUse.dataset.bpDownloadThumbnail || null, source_page: window.location.href, source_system: 'wordpress' }; try { // Sende Tracking-Request (mit Timeout, damit Download nicht zu lange wartet) const result = await Promise.race([ api.trackDownload(projectId, downloadData), new Promise((resolve) => setTimeout(() => resolve({}), 2000)) // Max 2 Sekunden warten ]); // Optional: Toast für Update (nur wenn nicht Timeout) if (result && result.data?.action === 'updated') { // Leise aktualisiert, kein Toast nötig } } catch (error) { // Fehler ignorieren, Download läuft trotzdem console.warn('Download tracking failed:', error); } // Öffne im gleichen Tab nach Tracking window.location.href = downloadUrl; }); }); } /** * 3D-Konfigurator-Seite erkennen und Daten erfassen */ function detectConfigurationPage() { if (!projectId) { return; // Kein Projekt vorhanden } // Suche nach Data-Attributen (Body oder Container-Element) let configElement = document.body; // Prüfe ob Container-Element mit Data-Attributen existiert const configContainer = document.querySelector('[data-bp-config-id]'); if (configContainer) { configElement = configContainer; } // Prüfe ob Konfigurationsseite (mindestens ID muss vorhanden sein) const configId = configElement?.dataset?.bpConfigId || document.querySelector('meta[name="bp-configuration-id"]')?.getAttribute('content') || null; if (!configId) { return; // Keine Konfigurationsseite } // Budget aus Data-Attribut oder Meta-Tag const budget = configElement?.dataset?.bpConfigBudget ? parseFloat(configElement.dataset.bpConfigBudget) : (document.querySelector('meta[name="bp-budget"]') ? parseFloat(document.querySelector('meta[name="bp-budget"]').getAttribute('content')) : null); // Sammle alle Konfigurationsdaten aus Data-Attributen const configData = {}; if (!configElement) { return; } // Einfache Felder if (configElement.dataset.bpConfigId) configData.id = configElement.dataset.bpConfigId; if (configElement.dataset.bpConfigErstelltAm) configData.erstellt_am = configElement.dataset.bpConfigErstelltAm; if (configElement.dataset.bpConfigBadplanungsId) configData.badplanungs_id = configElement.dataset.bpConfigBadplanungsId; if (configElement.dataset.bpConfigAngebotPdfLink) configData.angebot_pdf_link = configElement.dataset.bpConfigAngebotPdfLink; if (configElement.dataset.bpConfigBadplanungsApiLink) configData.badplanungs_api_link = configElement.dataset.bpConfigBadplanungsApiLink; if (configElement.dataset.bpConfigBadplanungsDeeplink) configData.badplanungs_deeplink = configElement.dataset.bpConfigBadplanungsDeeplink; // Preis-Struktur (verschachtelt) if (configElement.dataset.bpConfigPreisGesamt || configElement.dataset.bpConfigPreisMontage || configElement.dataset.bpConfigPreisMaterial) { configData.preis = {}; if (configElement.dataset.bpConfigPreisGesamt) configData.preis.gesamt = parseFloat(configElement.dataset.bpConfigPreisGesamt); if (configElement.dataset.bpConfigPreisMontage) configData.preis.montage = parseFloat(configElement.dataset.bpConfigPreisMontage); if (configElement.dataset.bpConfigPreisMaterial) configData.preis.material = parseFloat(configElement.dataset.bpConfigPreisMaterial); } // Module (Array - komma-getrennt oder JSON) if (configElement.dataset.bpConfigModule) { try { // Versuche zuerst als JSON zu parsen configData.module = JSON.parse(configElement.dataset.bpConfigModule); } catch (e) { // Falls kein JSON, als komma-getrennte Liste behandeln const moduleNames = configElement.dataset.bpConfigModule.split(',').map(name => name.trim()).filter(name => name); configData.module = moduleNames.map(name => ({ name: name })); } } // Fallback: JSON-LD falls vorhanden (für Rückwärtskompatibilität) if (Object.keys(configData).length === 0) { const jsonLd = document.querySelector('script[type="application/ld+json"]'); if (jsonLd) { try { const ldData = JSON.parse(jsonLd.textContent); if (ldData.configurationData) { Object.assign(configData, ldData.configurationData); } } catch (e) { console.warn('Konnte JSON-LD nicht parsen:', e); } } } // Erfasse Konfiguration (Fehler werden ignoriert, damit die Seite nicht blockiert wird) api.saveConfiguration(projectId, { configuration_id: configId, budget: budget || (configData.preis?.gesamt ? parseFloat(configData.preis.gesamt) : null), configuration_data: Object.keys(configData).length > 0 ? configData : null, source_url: window.location.href }).catch(error => { // Fehler wird nur geloggt, blockiert aber nicht die Seite console.warn('Konfiguration konnte nicht gespeichert werden (möglicherweise fehlt die Migration):', error); }); } /** * Popup-Trigger */ function initPopupTriggers() { // Prüfe ob Popup nicht angezeigt werden soll (permanent) if (localStorage.getItem('bp_popup_dont_show') === 'true') { return; } // Prüfe ob Popup bereits in dieser Session gezeigt wurde if (sessionStorage.getItem('bp_popup_shown') === 'true') { return; } // Timer (8 Sekunden) setTimeout(() => { if (!projectCode && !document.getElementById('bp-popup')) { const popup = createPopup(); if (popup) { // Flag wird beim Schließen gesetzt, nicht hier } } }, 8000); // Scroll-Tiefe (50%) let scrolled = false; window.addEventListener('scroll', () => { if (scrolled || projectCode) return; const scrollPercent = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100; if (scrollPercent > 50 && !document.getElementById('bp-popup')) { scrolled = true; const popup = createPopup(); if (popup) { // Flag wird beim Schließen gesetzt, nicht hier } } }); // Exit Intent document.addEventListener('mouseleave', (e) => { if (e.clientY <= 0 && !projectCode && !document.getElementById('bp-popup')) { const popup = createPopup(); if (popup) { // Flag wird beim Schließen gesetzt, nicht hier } } }); } /** * Öffnet Popup manuell (z.B. über Button/Link) */ function openPopupManually() { // Entferne Session-Flag, damit Popup angezeigt wird sessionStorage.removeItem('bp_popup_shown'); // Prüfe auch ob Popup bereits existiert const existingPopup = document.getElementById('bp-popup'); if (existingPopup) { existingPopup.remove(); } createPopup(); } // Exponiere Funktion global für manuelles Öffnen window.bpOpenPopup = openPopupManually; // Unterstütze auch data-Attribute für automatische Event-Listener document.addEventListener('DOMContentLoaded', () => { // Suche alle Buttons/Links mit data-bp-open-popup Attribut document.querySelectorAll('[data-bp-open-popup]').forEach(element => { element.addEventListener('click', (e) => { e.preventDefault(); openPopupManually(); }); }); }); /** * Initialisierung */ function init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); return; } initProjectButton(); initMerklisteButtons(); // Neue allgemeine Merkliste-Funktion initProductButtons(); // Alte Funktion für Rückwärtskompatibilität initDownloadLinks(); initPopupTriggers(); detectConfigurationPage(); // 3D-Konfigurator-Erkennung trackPageVisit(); // Seitenbesuch und Scrolltiefe tracken // Force re-initialization nach kurzer Verzögerung (für Caching-Probleme) setTimeout(() => { // Prüfe ob Buttons sichtbar sein sollten if (projectId) { document.querySelectorAll('.bp-add-to-merkliste, [data-bp-merkliste-id], .bp-add-to-project, [data-bp-product-id], .bp-download-link, [data-bp-download-id]').forEach(element => { element.classList.remove('hidden'); }); // Initialisiere Buttons erneut initMerklisteButtons(); initProductButtons(); initDownloadLinks(); } }, 100); } // Start init(); // Export für externe Nutzung window.BadProjektPass = { api, config, getProjectCode: () => projectCode, getProjectToken: () => projectToken, openProject: () => { if (projectCode) { window.open(`${config.appBaseUrl}/p/${projectCode}`, '_blank'); } else { createPopup(); } } }; })();