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.