PageSourceSearch

https://app.medi-one.ai/modules/crmIntegration.js

js medi-one.ai collected 2026-10-05 14:15:27 UTC 102,955 bytes, 2,105 lines download raw bytes

1// CRM Integration Module
2import UI from './ui.js?v=20260820-dd1-imperr1-bann1-impauth1';
3import { auth, db } from '../firebase-init.js';
4import { Language } from './language.js';
5import { collection, doc, getDoc, setDoc, deleteDoc, query, where, getDocs, serverTimestamp, writeBatch, Timestamp } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js';
6import config from './config.js';
7import { usersApi } from './usersApi.js';
8
9// Translation helper
10function t(text) {
11    return Language.translate(text);
12}
13
14// API Configuration
15const API_BASE_URL = config.API_BASE_URL;
16
17export class CRMIntegration {
18    static async configure() {
19        try {
20            // Ensure user is authenticated
21            if (!auth.currentUser) {
22                throw new Error('User not authenticated');
23            }
24
25            const mainContent = document.getElementById('mainContent');
26            if (!mainContent) {
27                throw new Error('Main content container not found');
28            }
29
30            mainContent.innerHTML = `
31                <div class="crm-integration container-fluid">
32                    <header class="d-flex justify-content-between align-items-center mb-4">
33                        <h1>${t('CRM-Integration')}</h1>
34                        <div class="d-flex align-items-center gap-3">
35                            <button id="syncNowBtn" class="btn btn-primary">
36                                <i class="fas fa-sync me-2"></i>${t('Jetzt synchronisieren')}
37                            </button>
38                        </div>
39                    </header>
40
41                    <div class="row">
42                        <!-- Pipedrive Integration -->
43                        <div class="col-md-4 mb-4">
44                            <div class="card">
45                                <div class="card-header d-flex justify-content-between align-items-center">
46                                    <h5 class="mb-0">
47                                        <i class="fas fa-building text-primary me-2"></i>
48                                        Pipedrive
49                                    </h5>
50                                    <div class="d-flex align-items-center gap-2">
51                                        <div id="pipedriveStatus">
52                                            <span class="badge bg-secondary">${t('Nicht verbunden')}</span>
53                                        </div>
54                                        <button class="btn btn-link btn-sm p-0 text-muted" id="pipedriveSettingsBtn" title="${t('Pipedrive Einstellungen')}">
55                                            <i class="fas fa-cog"></i>
56                                        </button>
57                                    </div>
58                                </div>
59                                <div class="card-body">
60                                    <p class="card-text">
61                                        ${t('Verbinden Sie Pipedrive mit Ihrem API Key, um Ihre Kontakte zu synchronisieren.')}
62                                    </p>
63                                    <div class="d-grid gap-2">
64                                        <button class="btn btn-outline-primary" id="pipedriveConnectBtn">
65                                            <i class="fas fa-plug me-2"></i>${t('Mit Pipedrive verbinden')}
66                                        </button>
67                                        <button class="btn btn-danger d-none" id="pipedriveDisconnectBtn">
68                                            <i class="fas fa-unlink me-2"></i>${t('Verbindung trennen')}
69                                        </button>
70                                        <button class="btn btn-outline-secondary" id="pipedriveImportBtn">
71                                            <i class="fas fa-file-import me-2"></i>${t('Kontakte importieren')}
72                                        </button>
73                                    </div>
74                                </div>
75                            </div>
76                        </div>
77
78                
79
80                        <!-- Close.com Integration -->
81                        <div class="col-md-4 mb-4">
82                            <div class="card">
83                                <div class="card-header d-flex justify-content-between align-items-center">
84                                    <h5 class="mb-0">
85                                        <i class="fas fa-building text-success me-2"></i>
86                                        Close.com
87                                    </h5>
88                                    <div class="d-flex align-items-center gap-2">
89                                        <div id="closeStatus">
90                                            <span class="badge bg-secondary">${t('Nicht verbunden')}</span>
91                                        </div>
92                                        <button class="btn btn-link btn-sm p-0 text-muted" id="closeSettingsBtn" title="${t('Close.com Einstellungen')}">
93                                            <i class="fas fa-cog"></i>
94                                        </button>
95                                    </div>
96                                </div>
97                                <div class="card-body">
98                                    <p class="card-text">
99                                        ${t('Verbinden Sie Close.com mit Ihrem API Key, um Ihre Kontakte zu synchronisieren.')}
100                                    </p>
101                                    <div class="d-grid gap-2">
102                                        <button class="btn btn-outline-success" id="closeConnectBtn">
103                                            <i class="fas fa-plug me-2"></i>${t('Mit Close.com verbinden')}
104                                        </button>
105                                        <button class="btn btn-danger d-none" id="closeDisconnectBtn">
106                                            <i class="fas fa-unlink me-2"></i>${t('Verbindung trennen')}
107                                        </button>
108                                        <button class="btn btn-outline-secondary" id="closeImportBtn">
109                                            <i class="fas fa-file-import me-2"></i>${t('Kontakte importieren')}
110                                        </button>
111                                    </div>
112                                </div>
113                            </div>
114                        </div>
115
116
117                        <!-- Platz für weitere CRM-Integrationen -->
118                        <div class="col-md-4 mb-4">
119                            <div class="card h-100 disabled-card">
120                                <div class="card-body">
121                                    <div class="coming-soon-badge">${t('Demnächst verfügbar')}</div>
122                                    <h5 class="card-title text-muted">
123                                        <i class="fab fa-hubspot text-muted me-2"></i>
124                                        HubSpot
125                                    </h5>
126                                    <p class="card-text text-muted">${t('Integration mit HubSpot CRM wird bald verfügbar sein.')}</p>
127                                </div>
128                            </div>
129                        </div>
130
131                          <!-- Salesforce Integration -->
132                        <div class="col-md-4 mb-4">
133                            <div class="card">
134                                <div class="card-header d-flex justify-content-between align-items-center">
135                                    <h5 class="mb-0">
136                                        <i class="fab fa-salesforce text-primary me-2"></i>
137                                        Salesforce
138                                    </h5>
139                                    <div class="d-flex align-items-center gap-2">
140                                        <div id="salesforceStatus">
141                                            <span class="badge bg-secondary">${t('Nicht verbunden')}</span>
142                                        </div>
143                                        <button class="btn btn-link btn-sm p-0 text-muted" id="salesforceSettingsBtn" title="${t('Salesforce Einstellungen')}">
144                                            <i class="fas fa-cog"></i>
145                                        </button>
146                                    </div>
147                                </div>
148                                <div class="card-body">
149                                    <p class="card-text">
150                                        ${t('Verbinden Sie Salesforce mit OAuth2, um Ihre Kontakte zu synchronisieren.')}
151                                    </p>
152                                    <div class="d-grid gap-2">
153                                        <button class="btn btn-outline-primary" id="salesforceConnectBtn">
154                                            <i class="fas fa-plug me-2"></i>${t('Mit Salesforce verbinden')}
155                                        </button>
156                                        <button class="btn btn-danger d-none" id="salesforceDisconnectBtn">
157                                            <i class="fas fa-unlink me-2"></i>${t('Verbindung trennen')}
157
158                                        </button>
159                                        <button class="btn btn-outline-secondary" id="salesforceImportBtn">
160                                            <i class="fas fa-file-import me-2"></i>${t('Kontakte importieren')}
161                                        </button>
162                                    </div>
163                                </div>
164                            </div>
165                        </div>
166                        </div>
167                    </div>
168                </div>
169            `;
170
171            // Bind event listeners and check status
172            await this.bindEvents();
173            await this.checkPipedriveStatus();
174            await this.checkCloseStatus();
175            await this.checkSalesforceStatus();
176
177        } catch (error) {
178            console.error('Error configuring CRM integration:', error);
179            UI.showError(t('Fehler beim Konfigurieren der CRM-Integration:') + ' ' + error.message);
180        }
181    }
182
183    static async init() {
184        try {
185            // Force token refresh on page load
186            await auth.currentUser.getIdToken(true);
187            
188            // Initialize CRM integration
189            await this.configure();
190            
191            // Bind event listeners and check status
192            await this.bindEvents();
193            await this.checkPipedriveStatus();
194            await this.checkCloseStatus();
195            await this.checkSalesforceStatus();
196
197        } catch (error) {
198            console.error('Error configuring CRM integration:', error);
199            UI.showError(t('Fehler beim Konfigurieren der CRM-Integration'));
200        }
201    }
202
203    static async bindEvents() {
204        const connectBtn = document.getElementById('pipedriveConnectBtn');
205        const disconnectBtn = document.getElementById('pipedriveDisconnectBtn');
206        const settingsBtn = document.getElementById('pipedriveSettingsBtn');
207        const importBtn = document.getElementById('pipedriveImportBtn');
208        const syncNowBtn = document.getElementById('syncNowBtn');
209
210        const closeConnectBtn = document.getElementById('closeConnectBtn');
211        const closeDisconnectBtn = document.getElementById('closeDisconnectBtn');
212        const closeSettingsBtn = document.getElementById('closeSettingsBtn');
213        const closeImportBtn = document.getElementById('closeImportBtn');
214
215        const salesforceConnectBtn = document.getElementById('salesforceConnectBtn');
216        const salesforceDisconnectBtn = document.getElementById('salesforceDisconnectBtn');
217        const salesforceImportBtn = document.getElementById('salesforceImportBtn');
218        const salesforceSettingsBtn = document.getElementById('salesforceSettingsBtn');
219
220        if (connectBtn) {
221            connectBtn.addEventListener('click', () => this.connectPipedrive());
222        }
223
224        if (disconnectBtn) {
225            disconnectBtn.addEventListener('click', () => this.disconnectPipedrive());
226        }
227
228        if (settingsBtn) {
229            settingsBtn.addEventListener('click', async (e) => {
230                e.preventDefault();
231                await this.showPipedriveSettings();
232            });
233        }
234
235        if (importBtn) {
236            importBtn.addEventListener('click', () => this.importPipedriveContacts());
237        }
238
239        if (syncNowBtn) {
240            syncNowBtn.addEventListener('click', () => this.syncNow());
241        }
242
243        if (closeConnectBtn) {
244            closeConnectBtn.addEventListener('click', () => this.connectClose());
245        }
246
247        if (closeDisconnectBtn) {
248            closeDisconnectBtn.addEventListener('click', () => this.disconnectClose());
249        }
250
251        if (closeSettingsBtn) {
252            closeSettingsBtn.addEventListener('click', async (e) => {
253                e.preventDefault();
254                await this.showCloseSettings();
255            });
256        }
257
258        if (closeImportBtn) {
259            closeImportBtn.addEventListener('click', () => this.importCloseContacts());
260        }
261
262        if (salesforceConnectBtn) {
263            salesforceConnectBtn.addEventListener('click', () => this.connectToSalesforce());
264        }
265
266        if (salesforceDisconnectBtn) {
267            salesforceDisconnectBtn.addEventListener('click', () => this.disconnectSalesforce());
268        }
269
270        if (salesforceImportBtn) {
271            salesforceImportBtn.addEventListener('click', () => this.importSalesforceContacts());
272        }
273
274        if (salesforceSettingsBtn) {
275            salesforceSettingsBtn.addEventListener('click', async (e) => {
276                e.preventDefault();
277                await this.showSalesforceSettings();
278            });
279        }
280    }
281
282    static async checkPipedriveStatus() {
283        try {
284            const idToken = await auth.currentUser.getIdToken();
285            const response = await fetch('/api2/pipedrive/status', {
286                headers: {
287                    'Authorization': `Bearer ${idToken}`
288                }
289            });
290
291            if (!response.ok) {
292                throw new Error('Failed to check status');
293            }
294
295            const { connected } = await response.json();
296            
297            // Update UI elements
298            const statusBadge = document.getElementById('pipedriveStatus');
299            const connectBtn = document.getElementById('pipedriveConnectBtn');
300            const disconnectBtn = document.getElementById('pipedriveDisconnectBtn');
301            const importBtn = document.getElementById('pipedriveImportBtn');
302
303            if (statusBadge) {
304                statusBadge.innerHTML = connected ? 
305                    `<span class="badge bg-success">${t('Verbunden')}</span>` : 
306                    `<span class="badge bg-secondary">${t('Nicht verbunden')}</span>`;
307            }
308
309            if (connectBtn) connectBtn.classList.toggle('d-none', connected);
310            if (disconnectBtn) disconnectBtn.classList.toggle('d-none', !connected);
311            if (importBtn) importBtn.disabled = !connected;
312
313            return connected;
314        } catch (error) {
315            console.error('Error checking Pipedrive status:', error);
316            UI.showError(t('Fehler beim Prüfen des Pipedrive-Status:') + ' ' + error.message);
317            return false;
318        }
319    }
320
321    static async connectPipedrive() {
322        const modalDiv = document.createElement('div');
323        modalDiv.className = 'modal fade';
324        modalDiv.id = 'pipedriveApiKeyModal';
325        modalDiv.setAttribute('tabindex', '-1');
326        modalDiv.innerHTML = `
327            <div class="modal-dialog">
328                <div class="modal-content">
329                    <div class="modal-header">
330                        <h5 class="modal-title">Pipedrive API Key</h5>
331                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
332                    </div>
333                    <div class="modal-body">
334                        <p>${t('Bitte geben Sie Ihren Pipedrive API-Key ein.')}</p>
335                        <div class="alert alert-info">
336                            <h6 class="alert-heading">${t('So finden Sie Ihren API-Key:')}</h6>
337                            <ol class="mb-0">
338                                <li>${t('Loggen Sie sich in Ihren Pipedrive-Account ein')}</li>
339                                <li>${t('Klicken Sie oben rechts auf Ihr Profilbild')}</li>
340                                <li>${t('Wählen Sie "Einstellungen"')}</li>
341                                <li>${t('Scrollen Sie links zu "Persönlich"')}</li>
342                                <li>${t('Klicken Sie auf "API"')}</li>
343                                <li>${t('Generieren Sie einen neuen API-Key oder kopieren Sie einen bestehenden')}</li>
344                            </ol>
345                        </div>
346                        <div class="form-group">
347                            <input type="text" class="form-control" id="pipedrive-api-key" placeholder="API Key eingeben">
348                        </div>
349                    </div>
350                    <div class="modal-footer">
351                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">${t('Abbrechen')}</button>
352                        <button type="button" class="btn btn-primary" id="save-pipedrive-key">${t('Speichern')}</button>
353                    </div>
354                </div>
355            </div>
356        `;
357        document.body.appendChild(modalDiv);
358
359        const modal = new bootstrap.Modal(modalDiv);
360        modal.show();
361
362        return new Promise((resolve, reject) => {
363            const saveButton = modalDiv.querySelector('#save-pipedrive-key');
364            const input = modalDiv.querySelector('#pipedrive-api-key');
365            const closeButton = modalDiv.querySelector('.btn-close');
366            const cancelButton = modalDiv.querySelector('.btn-secondary');
367
368            // Bessere Fehlerbehandlung für das Close-Event
369            const handleClose = () => {
370                modalDiv.remove();
371                // Nur reject, wenn noch keine Auflösung erfolgt ist
372                reject(new Error('Cancelled'));
373            };
374
375            saveButton.addEventListener('click', async () => {
376                const apiKey = input.value.trim();
377                if (apiKey) {
378                    try {
379                        const idToken = await auth.currentUser.getIdToken();
380                        const response = await fetch('/api2/pipedrive/connect', {
381                            method: 'POST',
382                            headers: {
383                                'Authorization': `Bearer ${idToken}`,
384                                'Content-Type': 'application/json'
385                            },
386                            body: JSON.stringify({ apiKey })
387                        });
388
389                        if (!response.ok) {
390                            const error = await response.json();
391                            throw new Error(error.error || 'Failed to connect');
392                        }
393
394                        modal.hide();
395                        UI.showSuccess(t('Erfolgreich mit Pipedrive verbunden'));
396                        await this.checkPipedriveStatus();
397                        resolve(true);
398                    } catch (error) {
399                        console.error('Error connecting to Pipedrive:', error);
400                        UI.showError(t('Fehler bei der Pipedrive-Verbindung:') + ' ' + error.message);
401                        reject(error);
402                    }
403                } else {
404                    UI.showError(t('Bitte geben Sie einen API-Key ein'));
405                }
406            });
407
408            // Verbesserte Event-Listener für Abbrechen und Schließen
409            if (closeButton) closeButton.addEventListener('click', handleClose);
410            if (cancelButton) cancelButton.addEventListener('click', handleClose);
411
412            modalDiv.addEventListener('hidden.bs.modal', () => {
413                modalDiv.remove();
414                // Verwende try-catch, um doppelte rejects zu vermeiden
415                try {
416                    reject(new Error('Cancelled'));
417                } catch (e) {
418                    // Ignoriere Fehler, die auftreten, wenn Promise bereits aufgelöst wurde
419                }
420            });
421        }).catch((error) => {
422            console.error('Error connecting to Pipedrive:', error);
423            if (error.message !== 'Cancelled') {
424                UI.showError(t('Fehler bei der Pipedrive-Verbindung:') + ' ' + error.message);
425            }
426            return false;
427        });
428    }
429
430    static async disconnectPipedrive() {
431        if (!await UI.confirm('Möchten Sie die Verbindung zu Pipedrive wirklich trennen?')) {
432            return;
433        }
434
435        try {
436            const idToken = await auth.currentUser.getIdToken();
437            const response = await fetch('/api2/pipedrive/disconnect', {
438                method: 'POST',
439                headers: {
440                    'Authorization': `Bearer ${idToken}`
441                }
442            });
443
444            if (!response.ok) {
445                throw new Error('Failed to disconnect');
446            }
447
448            UI.showSuccess(t('Pipedrive-Verbindung getrennt'));
449            await this.checkPipedriveStatus();
450        } catch (error) {
451            console.error('Error disconnecting Pipedrive:', error);
452            UI.showError(t('Fehler beim Trennen der Pipedrive-Verbindung:') + ' ' + error.message);
453        }
454    }
455
456    static async showPipedriveSettings() {
457        try {
458            // Welle 4: Backend-API statt direkter Firestore-Read
459            const userData = (await usersApi.me()) || {};
460            const pipedriveSettings = userData?.pipedrive || {};
461
462            const modalDiv = document.createElement('div');
463            modalDiv.className = 'modal fade';
464            modalDiv.id = 'pipedriveSettingsModal';
465            modalDiv.setAttribute('tabindex', '-1');
466            modalDiv.innerHTML = `
467                <div class="modal-dialog">
468                    <div class="modal-content">
469                        <div class="modal-header">
470                            <h5 class="modal-title">${t('Pipedrive Einstellungen')}</h5>
471                            <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
472                        </div>
473                        <div class="modal-body">
474                            <form id="pipedriveSettingsForm">
475                                <div class="mb-3">
476                                    <label class="form-label">Synchronisationsintervall</label>
477                                    <select class="form-select" name="syncInterval">
478                                        <option value="15">Alle 15 Minuten</option>
479                                        <option value="30">Alle 30 Minuten</option>
480                                        <option value="60">
480Stündlich</option>
481                                    </select>
482                                </div>
483                                <div class="mb-3">
484                                    <label class="form-label">Kontakte importieren als</label>
485                                    <select class="form-select" name="importAs">
486                                        <option value="leads">Leads</option>
487                                        <option value="contacts">Kontakte</option>
488                                    </select>
489                                </div>
490                            </form>
491                        </div>
492                        <div class="modal-footer">
493                            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">${t('Schließen')}</button>
494                            <button type="button" class="btn btn-primary" id="savePipedriveSettings">${t('Speichern')}</button>
495                        </div>
496                    </div>
497                </div>
498            `;
499            
500            document.body.appendChild(modalDiv);
501            const modal = new bootstrap.Modal(modalDiv);
502
503            // Set current values
504            const form = document.getElementById('pipedriveSettingsForm');
505            if (form) {
506                form.syncInterval.value = pipedriveSettings.syncInterval || '30';
507                form.importAs.value = pipedriveSettings.importAs || 'contacts';
508            }
509
510            // Save settings
511            document.getElementById('savePipedriveSettings').addEventListener('click', async () => {
512                try {
513                    const settings = {
514                        syncInterval: form.syncInterval.value,
515                        importAs: form.importAs.value
516                    };
517
518                    await setDoc(doc(db, 'users', auth.currentUser.uid), {
519                        pipedrive: {
520                            ...pipedriveSettings,
521                            ...settings
522                        }
523                    }, { merge: true });
524
525                    modal.hide();
526                    UI.showSuccess(t('Einstellungen gespeichert'));
527                } catch (error) {
528                    console.error('Error saving settings:', error);
529                    UI.showError(t('Fehler beim Speichern der Einstellungen'));
530                }
531            });
532
533            // Clean up when modal is hidden
534            modalDiv.addEventListener('hidden.bs.modal', () => {
535                modalDiv.remove();
536            });
537
538            modal.show();
539
540        } catch (error) {
541            console.error('Error showing Pipedrive settings:', error);
542            UI.showError(t('Fehler beim Anzeigen der Pipedrive-Einstellungen'));
543        }
544    }
545
546    static async showPipedriveApiKeyDialog() {
547        const apiKeyDialog = document.createElement('div');
548        apiKeyDialog.className = 'modal';
549        apiKeyDialog.innerHTML = `
550            <div class="modal-content">
551                <h2>Pipedrive API Key</h2>
552                <p>${t('Bitte geben Sie Ihren Pipedrive API-Key ein.')}</p>
553                <p class="info-text">
554                    ${t('So finden Sie Ihren API-Key:')}<br>
555                    1. ${t('Loggen Sie sich in Ihren Pipedrive-Account ein')}<br>
556                    2. ${t('Klicken Sie oben rechts auf Ihr Profilbild')}<br>
557                    3. ${t('Wählen Sie "Einstellungen"')}<br>
558                    4. ${t('Scrollen Sie links zu "Persönlich"')}<br>
559                    5. ${t('Klicken Sie auf "API"')}<br>
560                    6. ${t('Generieren Sie einen neuen API-Key oder kopieren Sie einen bestehenden')}
561                </p>
562                <input type="text" id="pipedrive-api-key" placeholder="API Key eingeben">
563                <div class="button-group">
564                    <button id="save-pipedrive-key">${t('Speichern')}</button>
565                    <button id="cancel-pipedrive-key">${t('Abbrechen')}</button>
566                </div>
567            </div>
568        `;
569        document.body.appendChild(apiKeyDialog);
570    }
571
572    static async importPipedriveContacts() {
573        try {
574            // Zuerst prüfen, ob Pipedrive verbunden ist
575            const isConnected = await this.checkPipedriveStatus();
576            
577            if (!isConnected) {
578                UI.showError(t('Keine aktive Verbindung zu Pipedrive. Bitte verbinden Sie Ihr Konto zuerst.'));
579                return;
580            }
581            
582            UI.showInfo(t('Lade Lead-Status von Pipedrive...'));
583            
584            const user = auth.currentUser;
585            if (!user) {
586                throw new Error('Nicht eingeloggt');
587            }
588
589            // Get lead statuses from Pipedrive
590            const idToken = await auth.currentUser.getIdToken();
591            const statusResponse = await fetch('/api2/pipedrive/person-fields', {
592                headers: {
593                    'Authorization': `Bearer ${idToken}`
594                }
595            });
596
597            if (!statusResponse.ok) {
598                throw new Error(t('Fehler beim Abrufen der Lead-Status von Pipedrive'));
599            }
600
601            const { data: fields } = await statusResponse.json();
602            const statusField = fields.find(field => field.field_type === 'status');
603            const statuses = statusField?.options || [];
604
605            // Remove existing modal if it exists
606            const existingModal = document.getElementById('pipedriveFieldMappingModal');
607            if (existingModal) {
608                existingModal.remove();
609            }
610
611            // Create modal container
612            const modalDiv = document.createElement('div');
613            modalDiv.className = 'modal fade';
614            modalDiv.id = 'pipedriveFieldMappingModal';
615            modalDiv.setAttribute('tabindex', '-1');
616            modalDiv.setAttribute('aria-labelledby', 'pipedriveFieldMappingModalLabel');
617            modalDiv.setAttribute('aria-hidden', 'true');
618            
619            modalDiv.innerHTML = `
620                <div class="modal-dialog modal-lg">
621                    <div class="modal-content">
622                        <div class="modal-header">
623                            <h5 class="modal-title" id="pipedriveFieldMappingModalLabel">Felder zuordnen</h5>
624                            <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
625                        </div>
626                        <div class="modal-body">
627                            <p class="text-muted">Ordnen Sie die Felder aus Pipedrive den entsprechenden Feldern in voiceOne zu.</p>
628                            <form id="pipedriveFieldMappingForm">
629                                <div class="mb-3">
630                                    <label class="form-label">
630Name</label>
631                                    <select class="form-select" name="name">
632                                        <option value="">Nicht importieren</option>
633                                        <option value="name" selected>Name</option>
634                                        <option value="first_name">Vorname</option>
635                                        <option value="last_name">Nachname</option>
636                                    </select>
637                                </div>
638                                <div class="mb-3">
639                                    <label class="form-label">E-Mail</label>
640                                    <select class="form-select" name="email">
641                                        <option value="">Nicht importieren</option>
642                                        <option value="email" selected>Primäre E-Mail</option>
643                                        <option value="secondary_email">Sekundäre E-Mail</option>
644                                    </select>
645                                </div>
646                                <div class="mb-3">
647                                    <label class="form-label">Telefon</label>
648                                    <select class="form-select" name="phone_number">
649                                        <option value="">Nicht importieren</option>
650                                        <option value="phone" selected>Primäre Telefonnummer</option>
651                                        <option value="mobile">Mobiltelefon</option>
652                                        <option value="other_phone">Andere Telefonnummer</option>
653                                    </select>
654                                </div>
655                                <div class="mb-3">
656                                    <label class="form-label">Firma</label>
657                                    <select class="form-select" name="company">
658                                        <option value="">Nicht importieren</option>
659                                        <option value="org_name" selected>Firmenname</option>
660                                        <option value="organization">Organisation</option>
661                                    </select>
662                                </div>
663                                <div class="mb-3">
664                                    <label class="form-label">Straße</label>
665                                    <select class="form-select" name="street">
666                                        <option value="">Nicht importieren</option>
667                                        <option value="address" selected>Straße</option>
668                                        <option value="org_address">Firmenadresse</option>
669                                    </select>
670                                </div>
671                                <div class="mb-3">
672                                    <label class="form-label">PLZ</label>
673                                    <select class="form-select" name="postalCode">
674                                        <option value="">Nicht importieren</option>
675                                        <option value="postal_code" selected>PLZ</option>
676                                        <option value="org_postal_code">Firmen-PLZ</option>
677                                    </select>
678                                </div>
679                                <div class="mb-3">
680                                    <label class="form-label">Ort</label>
681                                    <select class="form-select" name="city">
682                                        <option value="">Nicht importieren</option>
683                                        <option value="city" selected>Ort</option>
684                                        <option value="org_city">Firmenort</option>
685                                    </select>
686                                </div>
687                            </form>
688                        </div>
689                        <div class="modal-footer">
690                            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">${t('Abbrechen')}</button>
691                            <button type="button" class="btn btn-primary" id="continueToLeadStatus">Weiter</button>
692                        </div>
693                    </div>
694                </div>
695            `;
696            
697            document.body.appendChild(modalDiv);
698            const fieldMappingModal = new bootstrap.Modal(modalDiv);
699
700            // Show lead status selection modal after field mapping
701            document.getElementById('continueToLeadStatus').addEventListener('click', async () => {
702                const fieldMapping = {};
703                const form = document.getElementById('pipedriveFieldMappingForm');
704                const formData = new FormData(form);
705                formData.forEach((value, key) => {
706                    if (value) fieldMapping[key] = value;
707                });
708                
709                fieldMappingModal.hide();
710                
711                // Store field mapping for later use
712                localStorage.setItem('pipedriveFieldMapping', JSON.stringify(fieldMapping));
713                
714                // Get stages from Pipedrive
715                const stagesResponse = await fetch('/api2/pipedrive/stages', {
716                    headers: {
717                        'Authorization': `Bearer ${idToken}`
718                    }
719                });
720
721                if (!stagesResponse.ok) {
722                    throw new Error(t('Fehler beim Abrufen der Stages von Pipedrive'));
723                }
724
725                const { data: stages } = await stagesResponse.json();
726                
727                // Show lead status modal
728                const leadStatusModalDiv = document.createElement('div');
729                leadStatusModalDiv.innerHTML = `
730                    <div class="modal fade" id="pipedriveLeadStatusModal" tabindex="-1" aria-labelledby="pipedriveLeadStatusModalLabel" aria-hidden="true">
731                        <div class="modal-dialog">
732                            <div class="modal-content">
733                                <div class="modal-header">
734                                    <h5 class="modal-title" id="pipedriveLeadStatusModalLabel">Lead-Status auswählen</h5>
735                                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
736                                </div>
737                                <div class="modal-body">
738                                    <p class="text-muted">${t('Wählen Sie die Lead-Status aus, die Sie importieren möchten.')}</p>
739                                    <div class="alert alert-info">
740                                        <strong>Hinweis:</strong> Es werden nur Kontakte mit den ausgewählten Lead-Status importiert.
741                                    </div>
742                                    <form id="pipedriveLeadStatusForm">
743                                        ${stages.map(stage => `
744                                            <div class="form-check">
745                                                <input class="form-check-input" type="checkbox" value="${stage.id}" id="stage-${stage.id}" checked>
746                                                <label class="form-check-label" for="stage-${stage.id}">
747                                                    ${stage.name}
748                                                </label>
749                                            </div>
750                                        `).join('')}
751                                    </form>
752                                </div>
753                                <div class="modal-footer">
754                                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">${t('Abbrechen')}</button>
755                                    <button type="button" class="btn btn-primary" id="startPipedriveImport">${t('Importieren')}</button>
756                                </div>
757                            </div>
758                        </div>
759                    </div>
760                `;
761                
762                document.body.appendChild(leadStatusModalDiv);
763                const leadStatusModal = new bootstrap.Modal(leadStatusModalDiv.querySelector('#pipedriveLeadStatusModal'));
764                
765                // Handle import button click
766                document.getElementById('startPipedriveImport').addEventListener('click', async () => {
767                    const selectedStatuses = Array.from(document.querySelectorAll('#pipedriveLeadStatusForm input:checked'))
768                        .map(checkbox => checkbox.value);
769                    
770                    leadStatusModal.hide();
771                    await this.performPipedriveImport(selectedStatuses, fieldMapping);
772                });
773
774                // Clean up when modal is hidden
775                leadStatusModalDiv.querySelector('#pipedriveLeadStatusModal').addEventListener('hidden.bs.modal', () => {
776                    leadStatusModalDiv.remove();
777                });
778
779                leadStatusModal.show();
780            });
781
782            // Clean up when modal is hidden
783            modalDiv.addEventListener('hidden.bs.modal', () => {
784                modalDiv.remove();
785            });
786
787            // Show the field mapping modal
788            fieldMappingModal.show();
789
790        } catch (error) {
791            console.error('Error in Pipedrive import:', error);
792            UI.showError(t('Fehler beim Import:') + ' ' + error.message);
793        }
794    }
795
796    static async performPipedriveImport(selectedStatuses, fieldMapping) {
797        try {
798            console.log('Starting Pipedrive import with selected statuses:', selectedStatuses);
799            console.log('Field mapping:', fieldMapping);
800
801            // Welle 4: Backend-API statt direkter Firestore-Read
802            const __userData_801 = await usersApi.me();
803            if (!__userData_801) {
804                throw new Error('Benutzerprofil nicht gefunden');
805            }
806            const userDoc = { exists: () => !!__userData_801, data: () => __userData_801 };
807            
808            const userData = userDoc.data();
809            const tenantId = userData.tenantID || userData.tenant_id;
810            if (!tenantId) {
811                throw new Error('Keine Tenant-ID gefunden');
812            }
813
814            const idToken = await auth.currentUser.getIdToken();
815            let importedContacts = 0;
816            let skippedContacts = 0;
817
818            // Get contacts from Pipedrive
819            const response = await fetch('/api2/pipedrive/persons', {
820                headers: {
821                    'Authorization': `Bearer ${idToken}`
822                }
823            });
824
825            if (!response.ok) {
826                throw new Error(t('Fehler beim Abrufen der Kontakte von Pipedrive'));
827            }
828
829            const { data: contacts } = await response.json();
830            console.log('Retrieved contacts from Pipedrive:', contacts);
831            
832            // Filter contacts by selected statuses
833            const filteredContacts = contacts.filter(contact => {
834                console.log('Checking contact:', contact);
835                console.log('Contact stage_id:', contact.stage_id);
836                return selectedStatuses.includes(contact.stage_id?.toString());
837            });
838            
839            console.log('Filtered contacts:', filteredContacts);
840            
841            // Prepare batch write
842            const batch = writeBatch(db);
843            const contactsRef = collection(db, 'contacts');
844            
845            // Check for existing contacts to avoid duplicates
846            const existingContacts = await getDocs(query(contactsRef, 
847                where('tenantID', '==', tenantId),
848                where('uid', '==', auth.currentUser.uid)
849            ));
850            const existingEmails = new Set();
851            const existingPhones = new Set();
852            const existingSourceIds = new Set();
853            
854            existingContacts.forEach(doc => {
855                const contact = doc.data();
856                if (contact.email) existingEmails.add(contact.email.toLowerCase());
857                if (contact.phone) existingPhones.add(contact.phone.replace(/\D/g, ''));
858                if (contact.source === 'pipedrive' && contact.source_id) {
859                    existingSourceIds.add(contact.source_id.toString());
860                }
861            });
862
863            // Process each contact
864            for (const contact of filteredContacts) {
865                console.log('Processing contact:', contact);
866                const mappedContact = {
867                    name: '',
868                    email: '',
869                    phone_number: '',  
870                    company: '',
871                    street: '',
872                    postalCode: '',
873                    city: '',
874                    source: 'pipedrive',
875                    source_id: contact.id,  // Füge source_id hinzu
876                    tenantID: tenantId,
877                    userId: auth.currentUser.uid,
878                    lead_status: ''
879                };
880
881                // Map fields according to user's mapping
882                console.log('Field mapping:', fieldMapping);
883                console.log('Raw contact:', contact);
884                
885                Object.entries(fieldMapping).forEach(([targetField, sourceField]) => {
886                    console.log(`Mapping ${sourceField} to ${targetField}:`, contact[sourceField]);
887                    
888                    if (sourceField && contact[sourceField] !== undefined) {
889                        // Handle phone number formatting
890                        if (targetField === 'phone_number') {
891                            const phoneValue = contact[sourceField];
892                            console.log('Phone value before formatting:', phoneValue);
893                            if (phoneValue) {
894                                mappedContact[targetField] = phoneValue.replace(/\D/g, '');
895                                console.log('Phone value after formatting:', mappedContact[targetField]);
896                            }
897                        } else {
898                            mappedContact[targetField] = contact[sourceField];
899                        }
900                    }
901                });
902
903                console.log('Final mapped contact:', mappedContact);
904
905                // Normalize phone number for comparison
906                const normalizedPhone = mappedContact.phone_number ? mappedContact.phone_number.replace(/\D/g, '') : '';
907                
908                // Check for duplicates using email, phone, or source_id
909                const isDuplicate = (
910                    (mappedContact.email && existingEmails.has(mappedContact.email.toLowerCase())) ||
911                    (normalizedPhone && existingPhones.has(normalizedPhone)) ||
912                    (mappedContact.source_id && existingSourceIds.has(mappedContact.source_id.toString()))
913                );
914
915                // Skip if duplicate or no identifying information
916                if (isDuplicate || (!mappedContact.email && !mappedContact.phone_number && !mappedContact.name)) {
917                    console.log('Skipping contact - Duplicate or no identifying information:', mappedContact);
918                    skippedContacts++;
919                    continue;
920                }
921
922                // Add to batch
923                const newContactRef = doc(contactsRef);
924                batch.set(newContactRef, mappedContact);
925                
926                // Update tracking sets
927                if (mappedContact.email) existingEmails.add(mappedContact.email.toLowerCase());
928                if (normalizedPhone) existingPhones.add(normalizedPhone);
929                existingSourceIds.add(mappedContact.source_id.toString());
930                
931                importedContacts++;
932            }
933
934            // Commit batch
935            if (importedContacts > 0) {
936                await batch.commit();
937            }
938
939            console.log(`Import finished. Imported: ${importedContacts}, Skipped: ${skippedContacts}`);
940            UI.showSuccess(t('{{count}} neue Kontakte importiert. {{skipped}} Kontakte übersprungen.').replace('{{count}}', importedContacts).replace('{{skipped}}', skippedContacts));
941        } catch (error) {
942            console.error('Error importing Pipedrive contacts:', error);
943            UI.showError(t('Fehler beim Importieren der Pipedrive-Kontakte:') + ' ' + error.message);
944        }
945    }
946
947    static async syncNow() {
948        try {
949            // Implement sync now logic
950            UI.showSuccess(t('Daten werden synchronisiert...'));
951        } catch (error) {
952            console.error('Error syncing data:', error);
953            UI.showError(t('Fehler beim Synchronisieren der Daten:') + ' ' + error.message);
954        }
955    }
956
957    static async connectClose() {
958        const modalDiv = document.createElement('div');
959        modalDiv.className = 'modal fade';
960        modalDiv.id = 'closeApiKeyModal';
961        modalDiv.setAttribute('tabindex', '-1');
962        modalDiv.innerHTML = `
963            <div class="modal-dialog">
964                <div class="modal-content">
965                    <div class="modal-header">
966                        <h5 class="modal-title">Close.com API Key</h5>
967                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
968                    </div>
969                    <div class="modal-body">
970                        <p>${t('Bitte geben Sie Ihren Close.com API-Key ein.')}</p>
971                        <div class="alert alert-info">
972                            <h6 class="alert-heading">${t('So finden Sie Ihren API-Key:')}</h6>
973                            <ol class="mb-0">
974                                <li>${t('Loggen Sie sich in Ihren Close.com-Account ein')}</li>
975                                <li>${t('Klicken Sie auf Ihr Profilbild')}</li>
976                                <li>${t('Wählen Sie "Settings & Integrations"')}</li>
977                                <li>${t('Klicken Sie auf "Developer Settings"')}</li>
978                                <li>${t('Klicken Sie auf "Create API Key"')}</li>
979                                <li>${t('Kopieren Sie den generierten API-Key')}</li>
980                            </ol>
981                        </div>
982                        <div class="form-group">
983                            <input type="text" class="form-control" id="close-api-key" placeholder="API Key eingeben">
984                        </div>
985                    </div>
986                    <div class="modal-footer">
987                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">${t('Abbrechen')}</button>
988                        <button type="button" class="btn btn-primary" id="save-close-key">${t('Speichern')}</button>
989                    </div>
990                </div>
991            </div>
992        `;
993        document.body.appendChild(modalDiv);
994
995        const modal = new bootstrap.Modal(modalDiv);
996        modal.show();
997
998        return new Promise((resolve, reject) => {
999            const saveButton = modalDiv.querySelector('#save-close-key');
1000            const input = modalDiv.querySelector('#close-api-key');
1001
1002            saveButton.addEventListener('click', async () => {
1003                const apiKey = input.value.trim();
1004                if (apiKey) {
1005                    try {
1006                        const idToken = await auth.currentUser.getIdToken();
1007                        const response = await fetch('/api3/close/connect', {
1008                            method: 'POST',
1009                            headers: {
1010                                'Authorization': `Bearer ${idToken}`,
1011                                'Content-Type': 'application/json'
1012                            },
1013                            body: JSON.stringify({ apiKey })
1014                        });
1015
1016                        if (!response.ok) {
1017                            const error = await response.json();
1018                            throw new Error(error.error || 'Failed to connect');
1019                        }
1020
1021                        modal.hide();
1022                        UI.showSuccess(t('Erfolgreich mit Close.com verbunden'));
1023                        await this.checkCloseStatus();
1024                        resolve();
1025                    } catch (error) {
1026                        console.error('Error connecting to Close.com:', error);
1027                        UI.showError(t('Fehler bei der Close.com-Verbindung:') + ' ' + error.message);
1028                        reject(error);
1029                    }
1030                }
1031            });
1032
1033            modalDiv.addEventListener('hidden.bs.modal', () => {
1034                modalDiv.remove();
1035                reject(new Error('Cancelled'));
1036            });
1037        }).catch((error) => {
1038            console.error('Error connecting to Close.com:', error);
1039            if (error.message !== 'Cancelled') {
1040                UI.showError('Fehler bei der Close.com-Verbindung: ' + error.message);
1041            }
1042        });
1043    }
1044
1045    static async disconnectClose() {
1046        if (!await UI.confirm('Möchten Sie die Verbindung zu Close.com wirklich trennen?')) {
1047            return;
1048        }
1049
1050        try {
1051            const idToken = await auth.currentUser.getIdToken();
1052            const response = await fetch('/api3/close/disconnect', {
1053                method: 'POST',
1054                headers: {
1055                    'Authorization': `Bearer ${idToken}`
1056                }
1057            });
1058
1059            if (!response.ok) {
1060                throw new Error('Failed to disconnect');
1061            }
1062
1063            UI.showSuccess(t('Close.com-Verbindung getrennt'));
1064            await this.checkCloseStatus();
1065        } catch (error) {
1066            console.error('Error disconnecting Close.com:', error);
1067            UI.showError(t('Fehler beim Trennen der Close.com-Verbindung:') + ' ' + error.message);
1068        }
1069    }
1070
1071    static async showCloseSettings() {
1072        try {
1073            // Welle 4: Backend-API statt direkter Firestore-Read
1074            const userData = (await usersApi.me()) || {};
1075            const closeSettings = userData?.close || {};
1076
1077            const modalDiv = document.createElement('div');
1078            modalDiv.className = 'modal fade';
1079            modalDiv.id = 'closeSettingsModal';
1080            modalDiv.setAttribute('tabindex', '-1');
1081            modalDiv.innerHTML = `
1082                <div class="modal-dialog">
1083                    <div class="modal-content">
1084                        <div class="modal-header">
1085                            <h5 class="modal-title">${t('Close.com Einstellungen')}</h5>
1086                            <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
1087                        </div>
1088                        <div class="modal-body">
1089                            <form id="closeSettingsForm">
1090                                <div class="mb-3">
1091                                    <label class="form-label">Synchronisationsintervall</label>
1092                                    <select class="form-select" name="syncInterval">
1093                                        <option value="15">Alle 15 Minuten</option>
1094                                        <option value="30">Alle 30 Minuten</option>
1095                                        <option value="60">
1095Stündlich</option>
1096                                    </select>
1097                                </div>
1098                                <div class="mb-3">
1099                                    <label class="form-label">Kontakte importieren als</label>
1100                                    <select class="form-select" name="importAs">
1101                                        <option value="leads">Leads</option>
1102                                        <option value="contacts">Kontakte</option>
1103                                    </select>
1104                                </div>
1105                            </form>
1106                        </div>
1107                        <div class="modal-footer">
1108                            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">${t('Schließen')}</button>
1109                            <button type="button" class="btn btn-primary" id="saveCloseSettings">${t('Speichern')}</button>
1110                        </div>
1111                    </div>
1112                </div>
1113            `;
1114            
1115            document.body.appendChild(modalDiv);
1116            const modal = new bootstrap.Modal(modalDiv);
1117
1118            // Set current values
1119            const form = document.getElementById('closeSettingsForm');
1120            if (form) {
1121                form.syncInterval.value = closeSettings.syncInterval || '30';
1122                form.importAs.value = closeSettings.importAs || 'contacts';
1123            }
1124
1125            // Save settings
1126            document.getElementById('saveCloseSettings').addEventListener('click', async () => {
1127                try {
1128                    const settings = {
1129                        syncInterval: form.syncInterval.value,
1130                        importAs: form.importAs.value
1131                    };
1132
1133                    await setDoc(doc(db, 'users', auth.currentUser.uid), {
1134                        close: {
1135                            ...closeSettings,
1136                            ...settings
1137                        }
1138                    }, { merge: true });
1139
1140                    modal.hide();
1141                    UI.showSuccess(t('Einstellungen gespeichert'));
1142                } catch (error) {
1143                    console.error('Error saving settings:', error);
1144                    UI.showError(t('Fehler beim Speichern der Einstellungen'));
1145                }
1146            });
1147
1148            // Clean up when modal is hidden
1149            modalDiv.addEventListener('hidden.bs.modal', () => {
1150                modalDiv.remove();
1151            });
1152
1153            modal.show();
1154
1155        } catch (error) {
1156            console.error('Error showing Close.com settings:', error);
1157            UI.showError(t('Fehler beim Anzeigen der Close.com-Einstellungen'));
1158        }
1159    }
1160
1161    static async checkCloseStatus() {
1162        try {
1163            const idToken = await auth.currentUser.getIdToken();
1164            const response = await fetch('/api3/close/status', {
1165                headers: {
1166                    'Authorization': `Bearer ${idToken}`
1167                }
1168            });
1169
1170            if (!response.ok) {
1171                throw new Error('Failed to check status');
1172            }
1173
1174            const { connected } = await response.json();
1175            
1176            // Update UI elements
1177            const statusBadge = document.getElementById('closeStatus');
1178            const connectBtn = document.getElementById('closeConnectBtn');
1179            const disconnectBtn = document.getElementById('closeDisconnectBtn');
1180            const importBtn = document.getElementById('closeImportBtn');
1181
1182            if (statusBadge) {
1183                statusBadge.innerHTML = connected ? 
1184                    `<span class="badge bg-success">${t('Verbunden')}</span>` : 
1185                    `<span class="badge bg-secondary">${t('Nicht verbunden')}</span>`;
1186            }
1187
1188            if (connectBtn) connectBtn.classList.toggle('d-none', 
1188connected);
1189            if (disconnectBtn) disconnectBtn.classList.toggle('d-none', !connected);
1190            if (importBtn) importBtn.disabled = !connected;
1191
1192            return connected;
1193        } catch (error) {
1194            console.error('Error checking Close.com status:', error);
1195            UI.showError(t('Fehler beim Prüfen des Close.com-Status:') + ' ' + error.message);
1196            return false;
1197        }
1198    }
1199
1200    static async importCloseContacts() {
1201        try {
1202            UI.showInfo(t('Lade Lead-Status von Close.com...'));
1203            
1204            const user = auth.currentUser;
1205            if (!user) {
1206                throw new Error('Nicht eingeloggt');
1207            }
1208
1209            // Get lead statuses from Close.com
1210            const idToken = await auth.currentUser.getIdToken();
1211            const statusResponse = await fetch('/api3/close/lead-statuses', {
1212                headers: {
1213                    'Authorization': `Bearer ${idToken}`
1214                }
1215            });
1216
1217            if (!statusResponse.ok) {
1218                throw new Error(t('Fehler beim Abrufen der Lead-Status von Close.com'));
1219            }
1220
1221            const { statuses } = await statusResponse.json();
1222            
1223            // Show field mapping modal first
1224            const modalDiv = document.createElement('div');
1225            modalDiv.innerHTML = `
1226                <div class="modal fade" id="closeFieldMappingModal" tabindex="-1" aria-labelledby="closeFieldMappingModalLabel" aria-hidden="true">
1227                    <div class="modal-dialog modal-lg">
1228                        <div class="modal-content">
1229                            <div class="modal-header">
1230                                <h5 class="modal-title" id="closeFieldMappingModalLabel">Felder zuordnen</h5>
1231                                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
1232                            </div>
1233                            <div class="modal-body">
1234                                <p class="text-muted">Ordnen Sie die Felder aus Close.com den entsprechenden Feldern in voiceOne zu.</p>
1235                                <form id="closeFieldMappingForm">
1236                                    <div class="mb-3">
1237                                        <label class="form-label">Name</label>
1238                                        <select class="form-select" name="name">
1239                                            <option value="">Nicht importieren</option>
1240                                            <option value="name" selected>Name</option>
1241                                            <option value="title">Titel/Position</option>
1242                                            <option value="created_by">Erstellt von</option>
1243                                            <option value="lead_id">Lead ID</option>
1244                                        </select>
1245                                    </div>
1246                                    <div class="mb-3">
1247                                        <label class="form-label">E-Mail</label>
1248                                        <select class="form-select" name="email">
1249                                            <option value="">Nicht importieren</option>
1250                                            <option value="emails" selected>E-Mail-Adressen</option>
1251                                            <option value="email_work">Geschäftliche E-Mail</option>
1252                                            <option value="email_personal">Private E-Mail</option>
1253                                        </select>
1254                                    </div>
1255                                    <div class="mb-3">
1256                                        <label class="form-label">Telefon</label>
1257                                        <select class="form-select" name="phone">
1258                                            <option value="">Nicht importieren</option>
1259                                            <option value="phones" selected>Alle Telefonnummern</option>
1260                                            <option value="phone_work">Geschäftlich</option>
1261                                            <option value="phone_mobile">Mobil</option>
1262                                            <option value="phone_home">Privat</option>
1263                                            <option value="phone_other">Andere</option>
1264                                        </select>
1265                                    </div>
1266                                    <div class="mb-3">
1267                                        <label class="form-label">Firma</label>
1268                                        <select class="form-select" name="company">
1269                                            <option value="">Nicht importieren</option>
1270                                            <option value="company_name" selected>
1270Firmenname</option>
1271                                            <option value="organization_name">Organisationsname</option>
1272                                        </select>
1273                                    </div>
1274                                    <div class="mb-3">
1275                                        <label class="form-label">Straße</label>
1276                                        <select class="form-select" name="street">
1277                                            <option value="">Nicht importieren</option>
1278                                            <option value="address_street" selected>Straße (Geschäftlich)</option>
1279                                            <option value="address_street_home">Straße (Privat)</option>
1280                                        </select>
1281                                    </div>
1282                                    <div class="mb-3">
1283                                        <label class="form-label">PLZ</label>
1284                                        <select class="form-select" name="postalCode">
1285                                            <option value="">Nicht importieren</option>
1286                                            <option value="address_postal_code" selected>PLZ (Geschäftlich)</option>
1287                                            <option value="address_postal_code_home">PLZ (Privat)</option>
1288                                        </select>
1289                                    </div>
1290                                    <div class="mb-3">
1291                                        <label class="form-label">Ort</label>
1292                                        <select class="form-select" name="city">
1293                                            <option value="">Nicht importieren</option>
1294                                            <option value="address_city" selected>Ort (Geschäftlich)</option>
1295                                            <option value="address_city_home">Ort (Privat)</option>
1296                                        </select>
1297                                    </div>
1298                                    <div class="mb-3">
1299                                        <label class="form-label">Notizen</label>
1300                                        <select class="form-select" name="notes">
1301                                            <option value="">Nicht importieren</option>
1302                                            <option value="notes" selected>Notizen</option>
1303                                            <option value="description">Beschreibung</option>
1304                                            <option value="background">Hintergrund</option>
1305                                        </select>
1306                                    </div>
1307                                    <div class="mb-3">
1308                                        <label class="form-label">Lead-Status</label>
1309                                        <select class="form-select" name="lead_status">
1310                                            <option value="">Nicht importieren</option>
1311                                            <option value="status_label">Status-Bezeichnung</option>
1312                                        </select>
1313                                    </div>
1314                                </form>
1315                            </div>
1316                            <div class="modal-footer">
1317                                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">${t('Abbrechen')}</button>
1318                                <button type="button" class="btn btn-primary" id="continueToLeadStatus">Weiter</button>
1319                            </div>
1320                        </div>
1321                    </div>
1322                </div>
1323            `;
1324            
1325            document.body.appendChild(modalDiv);
1326            const fieldMappingModal = new bootstrap.Modal(modalDiv.querySelector('#closeFieldMappingModal'));
1327            
1328            // Show lead status selection modal after field mapping
1329            const showLeadStatusModal = () => {
1330                const fieldMapping = {};
1331                const form = document.getElementById('closeFieldMappingForm');
1332                const formData = new FormData(form);
1333                formData.forEach((value, key) => {
1334                    if (value) fieldMapping[key] = value;
1335                });
1336                
1337                fieldMappingModal.hide();
1338                
1339                // Store field mapping for later use
1340                localStorage.setItem('closeFieldMapping', JSON.stringify(fieldMapping));
1341                
1342                // Show lead status modal
1343                const leadStatusModalDiv = document.createElement('div');
1344                leadStatusModalDiv.innerHTML = `
1345                    <div class="modal fade" id="closeLeadStatusModal" tabindex="-1" aria-labelledby="closeLeadStatusModalLabel" aria-hidden="true">
1346                        <div class="modal-dialog">
1347                            <div class="modal-content">
1348                                <div class="modal-header">
1349                                    <h5 class="modal-title" id="closeLeadStatusModalLabel">Lead-Status auswählen</h5>
1350                                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
1351                                </div>
1352                                <div class="modal-body">
1353                                    <form id="closeLeadStatusForm">
1354                                        <div class="mb-3">
1355                                            <label for="leadStatus" class="form-label">Lead-Status auswählen</label>
1356                                            <select class="form-select" id="leadStatus" multiple>
1357                                                <option value="all" selected>Alle Kontakte</option>
1358                                                ${statuses.map(status => `
1359                                                    <option value="${status.id}">${status.label}</option>
1360                                                `).join('')}
1361                                            </select>
1362                                            <div class="form-text">Mehrfachauswahl mit Strg/Cmd + Klick möglich</div>
1363                                        </div>
1364                                    </form>
1365                                </div>
1366                                <div class="modal-footer">
1367                                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">${t('Abbrechen')}</button>
1368                                    <button type="button" class="btn btn-primary" id="startCloseImport">${t('Importieren')}</button>
1369                                </div>
1370                            </div>
1371                        </div>
1372                    </div>
1373                `;
1374                
1375                document.body.appendChild(leadStatusModalDiv);
1376                const leadStatusModal = new bootstrap.Modal(leadStatusModalDiv.querySelector('#closeLeadStatusModal'));
1377                
1378                // Handle import button click
1379                document.getElementById('startCloseImport').addEventListener('click', async () => {
1380                    const selectedStatuses = Array.from(document.getElementById('leadStatus').selectedOptions)
1381                        .map(option => option.value);
1382                    
1383                    leadStatusModal.hide();
1384                    await CRMIntegration.performCloseImport(selectedStatuses, fieldMapping);
1385                });
1386                
1387                leadStatusModalDiv.querySelector('#closeLeadStatusModal').addEventListener('hidden.bs.modal', () => {
1388                    leadStatusModalDiv.remove();
1389                });
1390                
1391                leadStatusModal.show();
1392            };
1393            
1394            // Handle continue button click
1395            document.getElementById('continueToLeadStatus').addEventListener('click', showLeadStatusModal);
1396            
1397            // Clean up when modal is hidden
1398            modalDiv.querySelector('#closeFieldMappingModal').addEventListener('hidden.bs.modal', () => {
1399                modalDiv.remove();
1400            });
1401            
1402            fieldMappingModal.show();
1403        } catch (error) {
1404            console.error('Error in importCloseContacts:', error);
1405            UI.showError('Fehler beim Import-Dialog: ' + error.message);
1406        }
1407    }
1408
1409    static async performCloseImport(selectedStatuses, fieldMapping) {
1410        try {
1411            UI.showInfo('Importiere Kontakte von Close.com...');
1412            
1413            const user = auth.currentUser;
1414            if (!user) {
1415                throw new Error('Nicht eingeloggt');
1416            }
1417
1418            // Welle 4: Backend-API statt direkter Firestore-Read
1419            const __userData_1417 = await usersApi.me();
1420            if (!__userData_1417) {
1421                throw new Error('Benutzerprofil nicht gefunden');
1422            }
1423            const userDoc = { exists: () => !!__userData_1417, data: () => __userData_1417 };
1424
1425            const userData = userDoc.data();
1426            const tenantId = userData.tenantID || userData.tenant_id;
1427            if (!tenantId) {
1428                throw new Error('Keine Tenant-ID gefunden');
1429            }
1430
1431            const idToken = await auth.currentUser.getIdToken();
1432            let importedContacts = 0;
1433            let skippedContacts = 0;
1434
1435            // Import contacts for each selected status
1436            for (const statusId of selectedStatuses) {
1437                console.log(`Importing contacts for status: ${statusId}`);
1438                
1439                // Fetch leads with the selected status
1440                const response = await fetch(`/api3/close/leads?status_id=${statusId}`, {
1441                    headers: {
1442                        'Authorization': `Bearer ${idToken}`
1443                    }
1444                });
1445
1446                if (!response.ok) {
1447                    throw new Error(`Fehler beim Abrufen der Kontakte: ${response.statusText}`);
1448                }
1449
1450                const { data: leads } = await response.json();
1451                console.log(`Found ${leads.length} leads with status ${statusId}`);
1452
1453                // Process each lead
1454                for (const lead of leads) {
1455                    try {
1456                        console.log('Processing lead:', lead);
1457                        
1458                        const contact = {
1459                            name: '',
1460                            email: '',
1461                            phone_number: '',  
1462                            company: '',
1463                            street: '',
1464                            postalCode: '',
1465                            city: '',
1466                            source: 'close',
1467                            tenantID: tenantId.toString(),
1468                            userId: user.uid,
1469                            lead_status: ''
1470                        };
1471
1472                        // Map the name field
1473                        if (fieldMapping.name === 'name' && lead.display_name) {
1474                            contact.name = lead.display_name;
1475                        }
1476
1477                        // Get contact information
1478                        if (lead.contacts && lead.contacts.length > 0) {
1479                            const leadContact = lead.contacts[0];
1480                            console.log('Processing lead contact:', leadContact);
1481
1482                            // Handle emails
1483                            if (leadContact.emails && leadContact.emails.length > 0) {
1484                                contact.email = leadContact.emails[0].email;
1485                                console.log('Found email:', contact.email);
1486                            }
1487
1488                            // Handle phones based on mapping
1489                            if (leadContact.phones && leadContact.phones.length > 0) {
1490                                console.log('Available phone numbers:', leadContact.phones);
1491                                
1492                                if (fieldMapping.phone === 'phones') {
1493                                    // Use first available phone number
1494                                    contact.phone_number = leadContact.phones[0].phone;
1495                                } else if (fieldMapping.phone === 'phone_work') {
1496                                    const workPhone = leadContact.phones.find(p => p.type === 'office');
1497                                    contact.phone_number = workPhone ? workPhone.phone : '';
1498                                } else if (fieldMapping.phone === 'phone_mobile') {
1499                                    const mobilePhone = leadContact.phones.find(p => p.type === 'mobile');
1500                                    contact.phone_number = mobilePhone ? mobilePhone.phone : '';
1501                                } else if (fieldMapping.phone === 'phone_home') {
1502                                    const homePhone = leadContact.phones.find(p => p.type === 'home');
1503                                    contact.phone_number = homePhone ? homePhone.phone : '';
1504                                }
1505                                
1506                                console.log('Selected phone number:', contact.phone_number);
1507                            }
1508
1509                            // Get address information
1510                            if (leadContact.addresses && leadContact.addresses.length > 0) {
1511                                const address = leadContact.addresses[0];
1512                                console.log('Processing address:', address);
1513                                
1514                                if (fieldMapping.street === 'address_street') {
1515                                    contact.street = address.street1 || '';
1516                                }
1517                                if (fieldMapping.postalCode === 'address_postal_code') {
1518                                    contact.postalCode = address.postal_code || '';
1519                                }
1520                                if (fieldMapping.city === 'address_city') {
1521                                    contact.city = address.city || '';
1522                                }
1523                            }
1524                        }
1525
1526                        contact.company = lead.company_name || '';
1527
1528                        // Map lead status
1529                        if (fieldMapping.lead_status === 'status_id') {
1530                            contact.lead_status = lead.status_id || '';
1531                        } else if (fieldMapping.lead_status === 'status_label') {
1532                            contact.lead_status = lead.status_label || '';
1533                        }
1534
1535                        console.log('Final contact data:', contact);
1536
1537                        // Check if contact already exists
1538                        let existingContact = null;
1539                        
1540                        // Check by email if available
1541                        if (contact.email) {
1542                            const emailQuery = query(
1543                                collection(db, 'contacts'),
1544                                where('tenantID', '==', tenantId.toString()),
1545                                where('email', '==', contact.email)
1546                            );
1547                            const emailQuerySnapshot = await getDocs(emailQuery);
1548                            if (!emailQuerySnapshot.empty) {
1549                                existingContact = emailQuerySnapshot.docs[0];
1550                            }
1551                        }
1552
1553                        // If not found by email, check by phone if available
1554                        if (!existingContact && contact.phone_number) {  
1555                            const phoneQuery = query(
1556                                collection(db, 'contacts'),
1557                                where('tenantID', '==', tenantId.toString()),
1558                                where('phone_number', '==', contact.phone_number)  
1559                            );
1560                            const phoneQuerySnapshot = await getDocs(phoneQuery);
1561                            if (!phoneQuerySnapshot.empty) {
1562                                existingContact = phoneQuerySnapshot.docs[0];
1563                            }
1564                        }
1565
1566                        if (existingContact) {
1567                            console.log('Contact already exists:', existingContact.data());
1568                            skippedContacts++;
1569                            continue;
1570                        }
1571
1572                        // Save the contact to Firestore
1573                        const contactRef = doc(collection(db, 'contacts'));
1574                        await setDoc(contactRef, {
1575                            ...contact,
1576                            createdAt: serverTimestamp()
1577                        });
1578
1579                        importedContacts++;
1580                    } catch (error) {
1581                        console.error('Error processing lead:', error);
1582                        // Continue with next lead even if one fails
1583                    }
1584                }
1585            }
1586
1587            UI.showSuccess(`${importedContacts} Kontakte importiert, ${skippedContacts} übersprungen (bereits vorhanden)`);
1588            
1589            // Reload contacts to show the newly imported ones
1590            const contactsInstance = new Contacts();
1591            await contactsInstance.loadContacts();
1592        } catch (error) {
1593            console.error('Import error:', error);
1594            UI.showError(`Fehler beim Import: ${error.message}`);
1595        }
1596    }
1597
1598    static async connectToSalesforce() {
1599        try {
1600            const userId = auth.currentUser.uid;
1601            // Welle 4: Backend-API statt direkter Firestore-Read
1602            const __userData_1599 = await usersApi.me();
1603            if (!__userData_1599) {
1604                throw new Error('User document not found');
1605            }
1606            const userDoc = { exists: () => !!__userData_1599, data: () => __userData_1599 };
1607
1608            const salesforceData = userDoc.data()?.salesforce;
1609            if (!salesforceData?.domain || !salesforceData?.clientId || !salesforceData?.clientSecret) {
1610                UI.showError('Bitte konfigurieren Sie zuerst Ihre Salesforce-Einstellungen');
1611                await this.showSalesforceSettings();
1612                return;
1613            }
1614
1615            // Start OAuth flow with PKCE
1616            const domain = salesforceData.domain;
1617            const clientId = salesforceData.clientId;
1618            const redirectUri = 'https://app.voice-one.ai/salesforce/oauth/callback';
1619            
1620            // Generate PKCE values
1621            const verifier = this.generateCodeVerifier();
1622            const challenge = await this.generateCodeChallenge(verifier);
1623            
1624            // Store values in localStorage
1625            const state = Math.random().toString(36).substring(7);
1626            localStorage.setItem('salesforceState', state);
1627            localStorage.setItem('salesforceCodeVerifier', verifier);
1628            
1629            // Build authorization URL with PKCE parameters
1630            const authUrl = new URL(`https://${domain}/services/oauth2/authorize`);
1631            authUrl.searchParams.append('response_type', 'code');
1632            authUrl.searchParams.append('client_id', clientId);
1633            authUrl.searchParams.append('redirect_uri', redirectUri);
1634            authUrl.searchParams.append('state', state);
1635            authUrl.searchParams.append('scope', 'api refresh_token');
1636            authUrl.searchParams.append('code_challenge', challenge);
1637            authUrl.searchParams.append('code_challenge_method', 'S256');
1638            
1639            console.log('Auth URL:', authUrl.toString());
1640            
1641            // Calculate popup dimensions
1642            const width = 600;
1643            const height = 700;
1644            const left = (window.screen.width / 2) - (width / 2);
1645            const top = (window.screen.height / 2) - (height / 2);
1646
1647            // Open popup window
1648            const popup = window.open(
1649                authUrl.toString(),
1650                'SalesforceAuth',
1651                `width=${width},height=${height}
1651,left=${left},top=${top},toolbar=no,menubar=no,location=no,status=no`
1652            );
1653
1654            // Add message listener for popup communication
1655            window.addEventListener('message', async (event) => {
1656                if (event.origin === 'https://app.voice-one.ai' && event.data.type === 'salesforceCallback') {
1657                    if (popup) popup.close();
1658                    
1659                    // Get stored values from localStorage
1660                    const storedState = localStorage.getItem('salesforceState');
1661                    const storedCodeVerifier = localStorage.getItem('salesforceCodeVerifier');
1662                    
1663                    console.log('Callback received');
1664                    console.log('Stored State:', storedState);
1665                    console.log('Received State:', event.data.state);
1666                    console.log('Stored Code Verifier:', storedCodeVerifier);
1667                    
1668                    await this.handleSalesforceCallback(event.data.code, event.data.state, storedCodeVerifier);
1669                    
1670                    // Clean up localStorage
1671                    localStorage.removeItem('salesforceState');
1672                    localStorage.removeItem('salesforceCodeVerifier');
1673                }
1674            }, { once: true });
1675
1676        } catch (error) {
1677            console.error('Error connecting to Salesforce:', error);
1678            UI.showError('Error connecting to Salesforce: ' + error.message);
1679            this.updateSalesforceStatus(false);
1680        }
1681    }
1682
1683    static async disconnectSalesforce() {
1684        // Implement Salesforce disconnection logic
1685    }
1686
1687    static async importSalesforceContacts() {
1688        try {
1689            UI.showInfo('Kontakte werden von Salesforce importiert...');
1690            
1691            const user = auth.currentUser;
1692            if (!user) {
1693                throw new Error('Nicht eingeloggt');
1694            }
1695
1696            // Welle 4: Backend-API statt direkter Firestore-Read
1697            const userData = await usersApi.me();
1698            if (!userData) {
1699                throw new Error('Benutzerprofil nicht gefunden');
1700            }
1701            const tenantId = userData.tenantID || userData.tenant_id;
1702            if (!tenantId) {
1703                throw new Error('Keine Tenant-ID gefunden');
1704            }
1705
1706            const salesforceData = userData.salesforce;
1707            if (!salesforceData?.access_token || !salesforceData?.instance_url) {
1708                throw new Error('Keine Salesforce-Verbindung gefunden');
1709            }
1710
1711            // Get existing contacts with Salesforce IDs
1712            const existingContactsQuery = query(
1713                collection(db, 'contacts'),
1714                where('tenantID', '==', tenantId.toString()),
1715                where('salesforce_id', '!=', null)
1716            );
1717            const existingContactsSnapshot = await getDocs(existingContactsQuery);
1718            const existingEmails = new Set();
1719            const existingPhones = new Set();
1720            const existingSalesforceIds = new Set();
1721            
1722            existingContactsSnapshot.forEach(doc => {
1723                const contact = doc.data();
1724                if (contact.email) existingEmails.add(contact.email.toLowerCase());
1725                if (contact.phone) existingPhones.add(contact.phone.replace(/\D/g, ''));
1726                if (contact.source === 'salesforce' && contact.salesforce_id) {
1727                    existingSalesforceIds.add(contact.salesforce_id.toString());
1728                }
1729            });
1730
1731            // Fetch contacts from Salesforce
1732            const response = await fetch(`${config.API_BASE_URL}/salesforce/contacts`, {
1733                method: 'GET',
1734                headers: {
1735                    'Authorization': `Bearer ${salesforceData.access_token}`,
1736                    'Instance-URL': salesforceData.instance_url || salesforceData.instanceUrl,
1737                    'Content-Type': 'application/json',
1738                    'Accept': 'application/json'
1739                },
1740                mode: 'cors',
1741                credentials: 'include'
1742            });
1743
1744            if (!response.ok) {
1745                const contentType = response.headers.get('content-type');
1746                let errorData;
1747                
1748                if (contentType && contentType.includes('application/json')) {
1749                    errorData = await response.json();
1750                } else {
1751                    const text = await response.text();
1752                    console.error('Non-JSON error response:', text);
1753                    errorData = {
1754                        error: 'server_error',
1755                        error_description: 'Server returned an invali
1755d response'
1756                    };
1757                }
1758                
1759                console.error('Salesforce API error:', errorData);
1760                throw new Error(errorData.error_description || 'Fehler beim Abrufen der Kontakte von Salesforce');
1761            }
1762
1763            const responseData = await response.json();
1764            const contacts = Array.isArray(responseData) ? responseData : responseData.contacts || [];
1765
1766            // Use batched writes for better performance
1767            const batch = writeBatch(db);
1768            const contactsRef = collection(db, 'contacts');
1769            
1770            let importCount = 0;
1771            let skipCount = 0;
1772
1773            for (const contact of contacts) {
1774                // Skip if contact already exists
1775                if (existingSalesforceIds.has(contact.Id.toString())) {
1776                    skipCount++;
1777                    continue;
1778                }
1779
1780                const contactData = {
1781                    name: `${contact.FirstName || ''} ${contact.LastName || ''}`.trim(),
1782                    email: contact.Email || '',
1783                    phone_number: contact.Phone || '',
1784                    company: contact.Account?.Name || '',
1785                    notes: contact.Description || '',
1786                    street: contact.MailingStreet || '',
1787                    postal_code: contact.MailingPostalCode || '',
1788                    city: contact.MailingCity || '',
1789                    country: contact.MailingCountry || '',
1790                    lead_status: 'Neu',
1791                    industry: 'Sonstige',
1792                    aufgabe: 'Anruf angelegt',
1793                    marketing_status: 'keine',
1794                    tenantID: tenantId.toString(),
1795                    uid: user.uid,
1796                    customer_id: Date.now() + importCount,
1797                    createdAt: Timestamp.now(),
1798                    updatedAt: Timestamp.now(),
1799                    created_time: Timestamp.now(),
1800                    salesforce_id: contact.Id
1801                };
1802
1803                const newContactRef = doc(contactsRef);
1804                batch.set(newContactRef, contactData);
1805                importCount++;
1806            }
1807
1808            if (importCount > 0) {
1809                await batch.commit();
1810            }
1811            
1812            UI.showSuccess(`${importCount} neue Kontakte importiert. ${skipCount} Kontakte übersprungen.`);
1813        } catch (error) {
1814            console.error('Error importing Salesforce contacts:', error);
1815            UI.showError('Fehler beim Importieren der Salesforce-Kontakte: ' + error.message);
1816        }
1817    }
1818
1819    static async showSalesforceSettings() {
1820        // Create modal element if it doesn't exist
1821        let modalElement = document.getElementById('salesforceSettingsModal');
1822        if (!modalElement) {
1823            modalElement = document.createElement('div');
1824            modalElement.id = 'salesforceSettingsModal';
1825            modalElement.className = 'modal fade';
1826            document.body.appendChild(modalElement);
1827        }
1828
1829        modalElement.innerHTML = `
1830            <div class="modal-dialog">
1831                <div class="modal-content">
1832                    <div class="modal-header">
1833                        <h5 class="modal-title">Salesforce Einstellungen</h5>
1834                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
1835                    </div>
1836                    <div class="modal-body">
1837                        <form id="salesforceSettingsForm">
1838                            <div class="mb-3">
1839                                <label for="salesforceDomain" class="form-label">Salesforce Domain</label>
1840                                <input type="text" class="form-control" id="salesforceDomain" required>
1841                                <small class="form-text text-muted">Ihre Salesforce Domain finden Sie unter:<br>
1842                                Setup → Quick Find Box → "My Domain" eingeben → My Domain auswählen<br>
1843                                Beispiel: mycompany.my.salesforce.com</small>
1844                            </div>
1845                            <div class="mb-3">
1846                                <label for="salesforceClientId" class="form-label">Client ID (Consumer Key)</label>
1847                                <input type="text" class="form-control" id="salesforceClientId" required>
1848                                <small class="form-text text-muted">Die Client ID finden Sie in Salesforce unter:<br>
1849                                1. Setup → ${t('Anwendungs-Manager')}<br>
1850                                2. ${t('Klicken Sie auf "Neue verbundende Anwendung"')}<br>
1851                                3. ${t('Erstellen Sie eine neue Anwendung "voiceOne"')}<br>
1852                                4. Aktivieren Sie "OAuth-Einstellungen"<br>
1853                                5. Als Callback-URL: https://app.voice-one.ai/salesforce/oauth/callback<br>
1854                                6. OAuth-Scopes auswählen:<br>
1855                                   - Access and manage your data (api)<br>
1856                                   - Perform requests at any time (refresh_token)<br>
1857                                7. Die Client ID finden Sie dann als "Consumer Key"</small>
1858                            </div>
1859                            <div class="mb-3">
1860                                <label for="salesforceClientSecret" class="form-label">Client Secret (Consumer Secret)</label>
1861                                <input type="password" class="form-control" id="salesforceClientSecret" required>
1862                                <small class="form-text text-muted">Das Client Secret finden Sie direkt unter der Consumer Key als "Consumer Secret"</small>
1863                            </div>
1864                        </form>
1865                    </div>
1866                    <div class="modal-footer">
1867                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">${t('Abbrechen')}</button>
1868                        <button type="button" class="btn btn-primary" id="saveSalesforceSettings">${t('Speichern')}</button>
1869                    </div>
1870                </div>
1871            </div>
1872        `;
1873        
1874        // Initialize Bootstrap modal
1875        const modal = new bootstrap.Modal(modalElement);
1876
1877        // Load existing settings
1878        const userId = auth.currentUser.uid;
1879        const userDocRef = doc(db, 'users', userId);
1880        const userDoc = await getDoc(userDocRef);
1881        const salesforceData = userDoc.data()?.salesforce || {};
1882
1883        if (salesforceData) {
1884            const domainInput = document.getElementById('salesforceDomain');
1885            const clientIdInput = document.getElementById('salesforceClientId');
1886            const clientSecretInput = document.getElementById('salesforceClientSecret');
1887
1888            if (domainInput) domainInput.value = salesforceData.domain || '';
1889            if (clientIdInput) clientIdInput.value = salesforceData.clientId || '';
1890            if (clientSecretInput) clientSecretInput.value = salesforceData.clientSecret || '';
1891        }
1892
1893        // Save settings
1894        const saveButton = document.getElementById('saveSalesforceSettings');
1895        if (saveButton) {
1896            saveButton.addEventListener('click', async () => {
1897                const domain = document.getElementById('salesforceDomain')?.value || '';
1898                const clientId = document.getElementById('salesforceClientId')?.value || '';
1899                const clientSecret = document.getElementById('salesforceClientSecret')?.value || '';
1900
1901                if (!domain || !clientId || !clientSecret) {
1902                    UI.showError('Bitte füllen Sie alle Felder aus');
1903                    return;
1904                }
1905
1906                try {
1907                    await setDoc(userDocRef, {
1908                        salesforce: {
1909                            domain,
1910                            clientId,
1911                            clientSecret,
1912                            lastUpdated: serverTimestamp()
1913                        }
1914                    }, { merge: true });
1915
1916                    modal.hide();
1917                    UI.showSuccess('Salesforce-Einstellungen gespeichert');
1918                    
1919                    // Try to connect immediately
1920                    await this.connectToSalesforce();
1921                    
1922                } catch (error) {
1923                    console.error('Error saving Salesforce settings:', error);
1924                    UI.showError('Fehler beim Speichern der Einstellungen: ' + error.message);
1925                }
1926            });
1927        }
1928    }
1929
1930    static async handleSalesforceCallback(code, state, codeVerifier) {
1931        try {
1932            // Verify state
1933            const savedState = localStorage.getItem('salesforceState');
1934            if (!state || state !== savedState) {
1935                throw new Error('Invalid state parameter');
1936            }
1937            
1938            // Verify code verifier
1939            if (!codeVerifier) {
1940                throw new Error('Code verifier not found');
1941            }
1942            
1943            const userId = auth.currentUser.uid;
1944            const userDocRef = doc(db, 'users', userId);
1945            const userDoc = await getDoc(userDocRef);
1946            
1947            if (!userDoc.exists()) {
1948                throw new Error('User document not found');
1949            }
1950
1951            const salesforceData = userDoc.data()?.salesforce;
1952            if (!salesforceData?.domain || !salesforceData?.clientId || !salesforceData?.clientSecret) {
1953                throw new Error('Salesforce settings not found');
1954            }
1955
1956            const redirectUri = 'https://app.voice-one.ai/salesforce/oauth/callback';
1957            
1958            // Log token request details
1959            console.log('Token Request:');
1960            console.log('Domain:', salesforceData.domain);
1961            console.log('Code:', code);
1962            console.log('Code Verifier:', codeVerifier);
1963            console.log('Redirect URI:', redirectUri);
1964
1965            // Exchange code for token with PKCE
1966            const formData = new URLSearchParams({
1967                domain: salesforceData.domain,
1968                grant_type: 'authorization_code',
1969                code: code,
1970                client_id: salesforceData.clientId,
1971                client_secret: salesforceData.clientSecret,
1972                redirect_uri: redirectUri,
1973                code_verifier: codeVerifier
1974            });
1975
1976            const response = await fetch('/salesforce/token', {
1977                method: 'POST',
1978                headers: {
1979                    'Content-Type': 'application/x-www-form-urlencoded',
1980                },
1981                body: formData,
1982            });
1983
1984            if (!response.ok) {
1985                const errorData = await response.json();
1986                console.error('Token exchange error:', errorData);
1987                throw new Error(errorData.error_description || 'Token exchange failed');
1988            }
1989
1990            const tokenData = await response.json();
1991
1992            // Store tokens in Firestore with encryption
1993            await setDoc(userDocRef, {
1994                salesforce: {
1995                    ...salesforceData,
1996                    connected: true,
1997                    access_token: tokenData.access_token,
1998                    refresh_token: tokenData.refresh_token,
1999                    instance_url: tokenData.instance_url,
2000                    last_token_update: serverTimestamp()
2001                }
2002            }, { merge: true });
2003
2004            // Update UI status
2005            await this.updateSalesforceStatus(true);
2006
2007            // Show success message
2008            UI.showSuccess('Erfolgreich mit Salesforce verbunden');
2009
2010            return true;
2011        } catch (error) {
2012            console.error('Error in Salesforce callback:', error);
2013            UI.showError('Fehler bei der Salesforce-Verbindung: ' + error.message);
2014            this.updateSalesforceStatus(false);
2015            return false;
2016        }
2017    }
2018
2019    static async updateSalesforceStatus(connected) {
2020        try {
2021            const statusBadge = document.getElementById('salesforceStatus');
2022            const connectBtn = document.getElementById('salesforceConnectBtn');
2023            const disconnectBtn = document.getElementById('salesforceDisconnectBtn');
2024            const importBtn = document.getElementById('salesforceImportBtn');
2025
2026            if (statusBadge) {
2027                statusBadge.innerHTML = connected ? 
2028                    `<span class="badge bg-success">${t('Verbunden')}</span>` : 
2029                    `<span class="badge bg-secondary">${t('Nicht verbunden')}</span>`;
2030            }
2031
2032            if (connectBtn) connectBtn.classList.toggle('d-none', connected);
2033            if (disconnectBtn) disconnectBtn.classList.toggle('d-none', !connected);
2034            if (importBtn) importBtn.disabled = !connected;
2035
2036            // Update connection status in Firestore
2037            if (auth.currentUser) {
2038                const userDocRef = doc(db, 'users', auth.currentUser.uid);
2039                await setDoc(userDocRef, {
2040                    salesforce: {
2041                        connected: connected,
2042                        lastStatusUpdate: serverTimestamp()
2043                    }
2044                }, { merge: true });
2045            }
2046        } catch (error) {
2047            console.error('Error updating Salesforce status:', error);
2048            UI.showError('Fehler beim Aktualisieren des Salesforce-Status');
2049        }
2050    }
2051
2052    static async checkSalesforceStatus() {
2053        try {
2054            const userId = auth.currentUser.uid;
2055            const userDocRef = doc(db, 'users', userId);
2056            const userDoc = await getDoc(userDocRef);
2057            
2058            if (userDoc.exists()) {
2059                const salesforceData = userDoc.data()?.salesforce;
2060                this.updateSalesforceStatus(salesforceData?.connected || false);
2061            }
2062        } catch (error) {
2063            console.error('Error checking Salesforce status:', error);
2064        }
2065    }
2066
2067    static generateRandomString(length) {
2068        const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._';
2069        let result = '';
2070        const randomValues = new Uint8Array(length);
2071        window.crypto.getRandomValues(randomValues);
2072        for (let i = 0; i < length; i++) {
2073            result += charset[randomValues[i] % charset.length];
2074        }
2075        return result;
2076    }
2077
2078    static generateCodeVerifier() {
2079        // Generate random string between 43 and 128 characters
2080        const verifier = this.generateRandomString(64); // Using 64 as a good length
2081        console.log('Generated verifier:', verifier);
2082        return verifier;
2083    }
2084
2085    static async generateCodeChallenge(verifier) {
2086        const encoder = new TextEncoder();
2087        const data = encoder.encode(verifier);
2088        const hash = await window.crypto.subtle.digest('SHA-256', data);
2089        const challenge = this.base64urlEncode(hash);
2090        console.log('Generated challenge:', challenge);
2091        return challenge;
2092    }
2093
2094    static base64urlEncode(arrayBuffer) {
2095        const bytes = new Uint8Array(arrayBuffer);
2096        let str = '';
2097        for (let i = 0; i < bytes.byteLength; i++) {
2098            str += String.fromCharCode(bytes[i]);
2099        }
2100        return btoa(str)
2101            .replace(/\+/g, '-')
2102            .replace(/\//g, '_')
2103            .replace(/=/g, '');
2104    }
2105}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.