1// // phase8-contacts-users-migrated 2import { usersApi } from './usersApi.js'; 3// Adapter: usersApi.me() in Firestore snapshot shape 4async function _phase8UserSnap(uid) { 5 try { 6 const me = await usersApi.me(); 7 return { exists: () => !!me, data: () => me, id: (me && me.uid) || uid }; 8 } catch (e) { 9 return { exists: () => false, data: () => null }; 10 } 11} 12import { db, auth } from '../firebase-init.js'; 13import UI from './ui.js?v=20260820-dd1-imperr1-bann1-impauth1'; 14import { Pages } from './pages.js'; 15import { ContactImportUI } from './contactImportUI.js?v=20260731-csvimport-merk1-impauth1-kteam1-kt3'; 16import { loadCustomLeadStatuses } from '../app.js'; // Hinzugefügt 17import { 18 collection, 19 query, 20 where, 21 orderBy, 22 limit, 23 getDocs, 24 doc, 25 getDoc, 26 addDoc, 27 updateDoc, 28 deleteDoc, 29 serverTimestamp, 30 writeBatch, 31 arrayUnion, // HINZUGEFÃGT 32 arrayRemove, // HINZUGEFÃGT 33 startAfter // HINZUGEFÃGT für Pagination 34} from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js'; 35// phase8c-contacts-migrated-v1 36import SmartListsApi from './smartListsApi.js'; 37import AutoCallRulesApi from './autoCallRulesApi.js'; 38import ContactsApi from './contactsApi.js'; 39import AgentsApi from './agentsApi.js'; 40import { AUFGABE_OPTIONS, DEFAULT_INDUSTRIES, DEFAULT_LEAD_STATUSES } from './constants.js'; 41import { Calendar } from './calendar.js?v=20260826-tdup2-badge1-dnd1-kalrw2'; // Calendar importieren 42import { Auth } from './auth.js'; 43import { Language } from './language.js'; 44import ContactDetail from './contactDetail.js?v=20260807-noshow2-soap1-p5f1-rrev1-bill1-intake1-alarm1-tokfix1-dat1-mod1b-nam1-merk1--kal1-kont1--kd1-prax1-nsl1-nsl2-impauth1-tdup2-kf1-kf3-kteam1-kt3'; // ContactDetail für mobile Ansicht 45import { ladeKundenfelder, renderKundenfelder, sammleKundenfelder } from './customFields.js?v=kf3'; // kundenfelder-v1 46import * as KTeam from './kontaktTeam.js?v=kteam3'; // kontakt-team-v1 47 48// phase8c helpers 49async function _c8c_authedFetch(path, opts = {}) { 50 const tok = await Auth.getIdToken(); 51 const headers = Object.assign({ 'Authorization': 'Bearer ' + tok, 'Content-Type': 'application/json' }, opts.headers || {}); 52 return fetch(path, Object.assign({}, opts, { headers })); 53} 54 55async function _c8c_userMe() { 56 try { const r = await _c8c_authedFetch('/api/users/me'); if (r.ok) return await r.json(); } catch (e) {} 57 return null; 58} 59 60async function _c8c_listMulticallTemplates() { 61 const r = await _c8c_authedFetch('/api/multicall/list'); 62 if (!r.ok) return []; 63 const j = await r.json(); 64 const all = j.multicalls || []; 65 return all.filter(d => { 66 const isVoice = (d.task || '').startsWith('Sprachbefehl:'); 67 const isTemplate = d.status === 'template' || (d.greeting && d.message && !d.phone_number); 68 return !isVoice && isTemplate; 69 }); 70} 71 72async function _c8c_listAgents() { 73 try { const a = await AgentsApi.list(); return Array.isArray(a) ? a : []; } catch (e) { return []; } 74} 75 76async function _c8c_getCalendarTypes() { 77 const r = await _c8c_authedFetch('/api/calendar/types'); 78 if (!r.ok) return []; 79 const j = await r.json(); 80 return j.types || j || []; 81} 82 83 84// Kurze Ãbersetzungsfunktion für einfacheren Zugriff 85function t(text) { 86 // Fallback für iPhone/Safari falls Language nicht verfügbar ist 87 let result = text; 88 if (typeof Language !== 'undefined' && Language.translate) { 89 result = Language.translate(text); 90 } 91 // Physio Edition: Kontakte -> Patienten 92 // praxis-modus-v1 (13.08.2026): auch bei Betriebsart 'praxis'. 93 if (document.body.classList.contains('physio-edition') 94 || document.body.classList.contains('physio-menu') 95 || document.documentElement.classList.contains('physio-menu')) { 96 result = result.replace(/Kontakte/g, 'Patienten').replace(/Kontakt(?!e)/g, 'Patient').replace(/Kontakten/g, 'Patienten'); 97 } 98 return result; 99} 100 101// Helper-Funktion für lokalisierte Datumsformatierung 102function getLocale() { 103 return Language.currentLanguage || 'de-DE'; 104} 105 106// Helper-Funktion für Bootstrap Datepicker Konfiguration 107function getDatepickerConfig() { 108 const currentLang = Language.currentLanguage || 'de-DE'; 109 const config = { 110 'de-DE': { format: 'dd.mm.yyyy', language: 'de' }, 111 'en-US': { format: 'mm/dd/yyyy', language: 'en' }, 112 'es-ES': { format: 'dd/mm/yyyy', language: 'es' }, 113 'fr-FR': { format: 'dd/mm/yyyy', language: 'fr' }, 114 'ar-SA': { format: 'dd/mm/yyyy', language: 'ar' }, 115 'it-IT': { format: 'dd/mm/yyyy', language: 'it' } // app-i18n-it-v1 116 }; 117 return config[currentLang] || config['de-DE']; 118} 119 120// Globale Variable für Ladeindikator 121let contactsLoadingIndicator = null; 122 123// CSS für den Ladeindikator hinzufügen, wenn es noch nicht existiert 124function ensureLoadingIndicatorStyles() { 125 if (!document.getElementById('loading-indicator-styles')) { 126 const style = document.createElement('style'); 127 style.id = 'loading-indicator-styles'; 128 style.textContent = ` 129 .loading-overlay { 130 position: fixed; /* Geändert zu fixed */ 131 top: 0; 132 left: 0; 133 width: 100%; 134 height: 100%; 135 background-color: rgba(255, 255, 255, 0.8); /* Leicht transparent */ 136 display: flex; 137 justify-content: center; 138 align-items: center; 139 z-index: 1056; /* Höher als Bootstrap Modals (1055) */ 140 opacity: 1; /* Start mit voller Sichtbarkeit */ 141 transition: opacity 0.3s ease; /* Weiches Ausblenden */ 142 } 143 .loading-overlay.hidden { 144 opacity: 0; 145 pointer-events: none; /* Verhindert Interaktion, wenn ausgeblendet */ 146 } 147 .spinner-container { 148 text-align: center; 149 background-color: white; 150 padding: 20px; 151 border-radius: 8px; 152 box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 153 } 154 `; 155 document.head.appendChild(style); 156 } 157} 158 159// Ladeindikator anzeigen 160function showContactsLoadingIndicator() { 161 // console.log("[showContactsLoadingIndicator] Called"); // Logging 162 ensureLoadingIndicatorStyles(); // Stelle sicher, dass Styles vorhanden sind 163 164 // Ziel ist jetzt immer document.body 165 const targetContainer = document.body; 166 167 // Prüfe, ob bereits ein Ladeindikator existiert 168 let loadingIndicator = document.getElementById('contactsLoadingIndicator'); 169 170 if (!loadingIndicator) { 171 // Erstelle den Ladeindikator, wenn er noch nicht existiert 172 // console.log("[showContactsLoadingIndicator] Creating indicator"); // Logging 173 loadingIndicator = document.createElement('div'); 174 loadingIndicator.id = 'contactsLoadingIndicator'; 175 loadingIndicator.className = 'loading-overlay'; // Startet sichtbar 176 loadingIndicator.innerHTML = ` 177 <div class="spinner-container"> 178 <div class="spinner-border text-primary" role="status">
179 <span class="visually-hidden">${t('Wird geladen...')}</span> 180 </div> 181 <div class="mt-2">${t('Kontakte werden geladen...')}</div> 182 `; 183 184 targetContainer.appendChild(loadingIndicator); 185 } else { 186 // Wenn er existiert, stelle sicher, dass er sichtbar ist 187 // console.log("[showContactsLoadingIndicator] Making existing indicator visible"); // Logging 188 loadingIndicator.classList.remove('hidden'); 189 } 190 // console.log("Loading indicator shown for contacts."); 191} 192 193// Ladeindikator ausblenden 194function hideContactsLoadingIndicator() { 195 // console.log("[hideContactsLoadingIndicator] Called"); // Logging 196 const loadingIndicator = document.getElementById('contactsLoadingIndicator'); 197 if (loadingIndicator) { 198 // console.log("[hideContactsLoadingIndicator] Hiding indicator"); // Logging 199 // Füge die 'hidden'-Klasse hinzu, um die Ausblend-Animation zu starten 200 loadingIndicator.classList.add('hidden'); 201 202 // Entferne das Element nach der Animation 203 setTimeout(() => { 204 if (loadingIndicator.parentNode) { 205 // console.log("[hideContactsLoadingIndicator] Removing indicator from DOM"); // Logging 206 loadingIndicator.parentNode.removeChild(loadingIndicator); 207 } 208 }, 300); // Wartezeit entspricht der CSS-Transition-Dauer 209 } else { 210 // console.log("[hideContactsLoadingIndicator] Indicator not found"); // Logging 211 } 212 // console.log("Loading indicator hidden for contacts."); 213} 214 215// Verbesserte Cache-Funktion mit dynamischer Ablaufzeit und Kompression 216function getCachedData(cacheKey, fetchFunction, cacheDuration = 15 * 60 * 1000) { // Erhöht auf 15 Minuten 217 try { 218 const cached = localStorage.getItem(cacheKey); 219 if (cached) { 220 try { 221 const parsed = JSON.parse(cached); 222 if (Date.now() - parsed.timestamp < cacheDuration) { 223 // console.log(`Rückgabe von Cache-Daten für Schlüssel: ${cacheKey}`); 224 return Promise.resolve(parsed.data); 225 } else { 226 // console.log(`Cache abgelaufen für Schlüssel: ${cacheKey}`); 227 // Aktualisiere die Daten im Hintergrund, während wir abgelaufene Daten zurückgeben 228 setTimeout(() => { 229 fetchFunction().then(data => { 230 try { 231 localStorage.setItem(cacheKey, JSON.stringify({ 232 timestamp: Date.now(), 233 data: data 234 })); 235 // console.log(`Cache im Hintergrund aktualisiert für: ${cacheKey}`); 236 } catch (e) { 237 console.warn('Fehler beim Aktualisieren des Cache:', e); 238 } 239 }); 240 }, 100); 241 242 // Gib trotzdem die abgelaufenen Daten zurück für schnellere Anzeige 243 if (parsed.data) { 244 return Promise.resolve(parsed.data); 245 } 246 } 247 } catch (e) { 248 console.error('Fehler beim Parsen der Cache-Daten für Schlüssel:', cacheKey, e); 249 } 250 } 251 } catch (storageError) { 252 console.warn('localStorage-Fehler, Cache wird übersprungen:', storageError); 253 return fetchFunction(); 254 } 255 256 // Daten neu laden 257 return fetchFunction().then(data => { 258 cacheData(cacheKey, data); 259 return data; 260 }); 261} 262 263// Separate Funktion zum Speichern von Daten im Cache 264function cacheData(cacheKey, data) { 265 try { 266 // Speichere mit Zeitstempel 267 localStorage.setItem(cacheKey, JSON.stringify({ 268 timestamp: Date.now(), 269 data: data 270 })); 271 } catch (storageError) { 272 console.warn('Fehler beim Speichern im Cache:', storageError); 273 274 // Intelligente Cache-Bereinigung 275 try { 276 // 1. Prüfe verfügbaren Speicher 277 const totalItems = localStorage.length; 278 279 // 2. Wenn mehr als 20 Einträge, lösche die ältesten Kontakt-Caches 280 if (totalItems > 20) { 281 const cacheEntries = []; 282 283 // Sammle alle Kontakt-Caches mit Zeitstempeln 284 for (let i = 0; i < localStorage.length; i++) {
285 const key = localStorage.key(i); 286 if (key && key.startsWith('contacts_')) { 287 try { 288 const item = JSON.parse(localStorage.getItem(key)); 289 cacheEntries.push({ 290 key: key, 291 timestamp: item.timestamp || 0 292 }); 293 } catch (e) { 294 // Ungültiger Cache-Eintrag, direkt löschen 295 localStorage.removeItem(key); 296 } 297 } 298 } 299 300 // Sortiere nach Alter (älteste zuerst) 301 cacheEntries.sort((a, b) => a.timestamp - b.timestamp); 302 303 // Lösche die ältesten 30% der Einträge 304 const deleteCount = Math.ceil(cacheEntries.length * 0.3); 305 for (let i = 0; i < deleteCount; i++) { 306 if (cacheEntries[i]) { 307 localStorage.removeItem(cacheEntries[i].key); 308 // console.log(`Gelöschter alter Cache: ${cacheEntries[i].key}`); 309 } 310 } 311 312 // Versuche erneut zu speichern 313 try { 314 localStorage.setItem(cacheKey, JSON.stringify({ 315 timestamp: Date.now(), 316 data: data 317 })); 318 // console.log('Cache nach Bereinigung erfolgreich gespeichert'); 319 } catch (retryError) { 320 console.warn('Cache konnte nach Bereinigung nicht gespeichert werden'); 321 } 322 } 323 } catch (cleanupError) { 324 console.error('Fehler bei der Cache-Bereinigung:', cleanupError); 325 } 326 } 327} 328 329// Optimierte Funktion für das Cachen gefilterter Kontakte 330function setCachedFilteredContacts(key, contacts) { 331 try { 332 // Cache ALLE Kontakte ohne Limit 333 const contactsToCache = contacts; 334 335 // Kontakte sind bereits minimal (von loadContacts), keine weitere Reduzierung nötig 336 const minimalContacts = contactsToCache; 337 338 // Speichere mit TTL (Time-To-Live) 339 const cacheData = { 340 timestamp: Date.now(), 341 expires: Date.now() + (15 * 60 * 1000), // 15 Minuten TTL 342 count: contacts.length, // Speichere Gesamtanzahl 343 data: minimalContacts 344 }; 345 346 localStorage.setItem(key, JSON.stringify(cacheData)); 347 348 // Speichere den letzten Cache-Key für schnelles Wiederherstellen 349 localStorage.setItem('last_contacts_cache_key', key); 350 } catch (error) { 351 console.warn('Cache-Fehler:', error); 352 } 353} 354 355// Neue Funktion zur regelmäÃigen Cache-Bereinigung 356function scheduleCacheCleanup() { 357 // Führe die Bereinigung alle 30 Minuten durch 358 setInterval(() => { 359 try { 360 // console.log('Starte geplante Cache-Bereinigung...'); 361 const now = Date.now(); 362 let cleanedCount = 0; 363 364 // Durchlaufe alle Cache-Einträge 365 for (let i = 0; i < localStorage.length; i++) { 366 const key = localStorage.key(i); 367 368 // Fokussiere auf Kontakt-bezogene Caches 369 if (key && (key.startsWith('contacts_') || key.startsWith('filtered_contacts_'))) { 370 try { 371 const cacheData = JSON.parse(localStorage.getItem(key)); 372 373 // Lösche abgelaufene oder alte Caches (älter als 2 Stunden) 374 if (cacheData.expires && cacheData.expires < now || 375 cacheData.timestamp && (now - cacheData.timestamp > 2 * 60 * 60 * 1000)) { 376 localStorage.removeItem(key); 377 cleanedCount++; 378 } 379 } catch (e) { 380 // Ungültiger Cache-Eintrag, direkt löschen 381 localStorage.removeItem(key); 382 cleanedCount++; 383 } 384 } 385 } 386 387 // console.log(`Cache-Bereinigung abgeschlossen. ${cleanedCount} Einträge gelöscht.`); 388 } catch (error) { 389 console.error('Fehler bei der geplanten Cache-Bereinigung:', error); 390 } 391 }, 30 * 60 * 1000); // Alle 30 Minuten 392} 393 394// Starte die Cache-Bereinigung beim Laden 395scheduleCacheCleanup(); 396 397// Lazy Loading für Detaildaten mit verbessertem Caching 398async function loadContactDetails(contactId) { 399 const cachedDetails = localStorage.getItem(`contact_details_${contactId}`); 400 if (cachedDetails) { 401 try { 402 const parsed = JSON.parse(cachedDetails); 403 // Erhöhe Cache-Zeit auf 15 Minuten 404 if (Date.now() - parsed.timestamp < 15 * 60 * 1000) { 405 // console.log(`Kontaktdetails aus Cache geladen: ${contactId}`); 406 return parsed.data; 407 } 408 } catch (e) { 409 console.error('Error parsing cached contact details:', e); 410 } 411 } 412 413 // Lade Details nur bei Bedarf 414 const response = await fetch(`/api/contacts/${contactId}`); 415 const details = await response.json(); 416 417 try { 418 localStorage.setItem(`contact_details_${contactId}`, JSON.stringify({ 419 timestamp: Date.now(), 420 data: details 421 })); 422 } catch (e) { 423 console.warn('Error caching contact details:', e); 424 } 425 426 return details; 427} 428 429// ---- EXPORTS ---- 430// Exportiere die Cache-Funktionen, damit sie von anderen Modulen verwendet werden können 431export { getCachedData, cacheData }; 432
433// kontakte-filter-v1: Ein Kontaktfeld ist nicht zwingend eine Zeichenkette. 434// In den Echtdaten liegen phone_number und email bei einigen Kontakten als 435// Objekt vor ({type, phone, country, phone_formatted}). Der frueher direkte 436// Aufruf von .toLowerCase() warf dort einen TypeError und brach das Filtern 437// fuer ALLE Kontakte des Mandanten ab ("Error filtering contacts"). 438// Objekte werden flach zu ihren Werten aufgeloest, damit die enthaltene 439// Nummer weiterhin auffindbar bleibt. 440function suchText(wert) { 441 if (wert === null || wert === undefined) return ''; 442 if (typeof wert === 'string') return wert.toLowerCase(); 443 if (typeof wert === 'number' || typeof wert === 'boolean') return String(wert).toLowerCase(); 444 if (Array.isArray(wert)) return wert.map(suchText).join(' '); 445 if (typeof wert === 'object') { 446 try { return Object.values(wert).map(suchText).join(' '); } catch (e) { return ''; } 447 } 448 return String(wert).toLowerCase(); 449} 450 451// Contacts class for managing contacts in the application 452export class Contacts { 453 constructor() { 454 this.db = db; 455 this.searchTimeout = null; 456 this.eventsBound = false; // Track if events are already bound 457 this.contacts = []; // Store all contacts 458 this.contactImportUI = new ContactImportUI(); // Initialize contact import UI 459 this.addContactModal = null; 460 this.smartListModal = null; 461 462 // Listen for language changes 463 document.addEventListener('languageChanged', () => { 464 this.translateAllModals(); 465 }); 466 this.currentPage = 1; 467 this.itemsPerPage = 50; // Anzahl pro Seite für Pagination 468 this.renderDebounceTimer = null; 469 this.lastVisible = null; // Für Firestore Pagination 470 this.hasMore = true; // Gibt es noch mehr Kontakte zu laden? 471 this.isLoadingMore = false; // Verhindert mehrfaches Laden 472 this.initialLoadSize = 200; // 200 Kontakte initial laden für bessere Suche 473 this.scrollObserver = null; // Für Infinite Scrolling 474 this.allContactsLoaded = false; // Flag ob alle Kontakte geladen sind 475 this.backgroundLoadingComplete = false; // Flag für Hintergrund-Laden 476 this.smartLists = []; 477 this.smartListEventsBound = false; 478 this.currentFilteredContacts = null; // Add this line to store filtered contacts 479 this.callSummaryCache = new Map(); // Cache for call summaries 480 this.userDoc = null; // Add this line to store user document 481 this.filterTimeout = null; // Add this line for filter timeout 482 this.leadStatuses = []; // Hinzufügen, um Lead-Status zu speichern 483 this.smartListBodyClickListener = null; // Hinzufügen für Body-Listener 484 this.activeSmartListId = null; // Hinzufügen, um aktive Smart List zu speichern 485 this.autoCallRulesModal = null; // Property für das neue Modal 486 this.autoCallRules = []; // Zum Speichern der geladenen Regeln 487 this.multiCallTemplates = []; // Zum Speichern der Vorlagen für das Dropdown 488 this.allFilteredContactsSelected = false; // Flag für "Alle gefilterten Kontakte" Auswahl 489 this.selectedFilteredContactIds = []; // Speichert IDs aller gefilterten Kontakte bei Auswahl 490 491 // Speicher für vollständige Kontaktdaten (bei Bedarf geladen) 492 this.fullContactsCache = new Map(); // Vollständige Daten einzelner Kontakte 493 494 // DEBOUNCE FIX: Verhindert mehrfaches gleichzeitiges Laden 495 this.isLoadingContacts = false; // Flag ob gerade geladen wird 496 this.pendingLoadPromise = null; // Promise für laufende Requests 497 498 // SIP-Account Check 499 this.hasSipAccount = false; // Flag ob SIP-Account vorhanden ist 500 501 // Entferne diese Aufrufe aus dem Konstruktor, sie kommen zu früh 502 // this.loadLastCachedContacts();
503 // this.scheduleBackgroundSync(); 504 } 505 506 /** 507 * Speichere den aktuellen Zustand der Kontaktliste für spätere Wiederherstellung 508 */ 509 storeContactsState() { 510 try { 511 const currentState = { 512 currentPage: this.currentPage, 513 searchTerm: document.querySelector('#searchInput')?.value || '', 514 leadStatus: document.querySelector('#leadStatusFilter')?.value || '', 515 task: document.querySelector('#aufgabeFilter')?.value || '', 516 branche: document.querySelector('#brancheFilter')?.value || '', 517 dateFrom: document.querySelector('#dateFromFilter')?.value || '', 518 dateTo: document.querySelector('#dateToFilter')?.value || '', 519 activeSmartListId: this.activeSmartListId || localStorage.getItem('activeSmartListId'), 520 hasFilters: false, // Wird unten berechnet 521 timestamp: Date.now() 522 }; 523 524 // Prüfe, ob Filter aktiv sind 525 currentState.hasFilters = !!( 526 currentState.searchTerm || 527 currentState.leadStatus || 528 currentState.task || 529 currentState.branche || 530 currentState.dateFrom || 531 currentState.dateTo 532 ); 533 534 // console.log('[storeContactsState] Speichere Kontaktlisten-Zustand:', currentState); 535 sessionStorage.setItem('contactsListState', JSON.stringify(currentState)); 536 537 // NEUE OPTIMIERUNG: Speichere den kompletten DOM-Zustand 538 const contactsContainer = document.getElementById('contactsContainer'); 539 if (contactsContainer) { 540 try { 541 // Speichere HTML und Scroll-Position 542 const domState = { 543 html: contactsContainer.innerHTML, 544 scrollTop: window.pageYOffset || document.documentElement.scrollTop, 545 timestamp: Date.now() 546 }; 547 sessionStorage.setItem('contactsListDOM', JSON.stringify(domState)); 548 549 // Speichere auch die Kontakte-Daten für mehrfache Navigation 550 if (this.contacts && this.contacts.length > 0) { 551 try { 552 sessionStorage.setItem('contactsData', JSON.stringify({ 553 contacts: this.contacts, 554 currentFilteredContacts: this.currentFilteredContacts, 555 timestamp: Date.now() 556 })); 557 } catch (e) { 558 // Falls zu groà für sessionStorage, speichere nur IDs 559 console.warn('Kontakte zu groà für sessionStorage, speichere nur Basis-Daten'); 560 } 561 } 562 563 // console.log('[storeContactsState] DOM-Zustand und Kontakte gespeichert'); 564 } catch (e) { 565 console.warn('[storeContactsState] Konnte DOM nicht speichern:', e); 566 } 567 } 568 } catch (error) { 569 console.error('Fehler beim Speichern des Kontaktlisten-Zustands:', error); 570 } 571 } 572 573 // Neue Methode zum Löschen des Kontakt-Caches 574 async clearContactsCache() { 575 try { 576 // console.log('Lösche Kontakt-Cache...'); 577 578 // Aktuelle Benutzer-ID und Tenant-ID ermitteln 579 const user = auth.currentUser; 580 if (!user) { 581 console.warn('Kein Benutzer angemeldet. Cache kann nicht gelöscht werden.'); 582 return; 583 } 584 585 // Benutzer-Dokument abrufen, um die Tenant-ID zu bekommen 586 const userDocSnap = await _phase8UserSnap(user.uid); /* phase8 (was userDocRef + getDoc) */ 587 588 if (!userDocSnap.exists()) { 589 console.warn('Benutzerprofil nicht gefunden.'); 590 return; 591 } 592 593 const userData = userDocSnap.data(); 594 const tenantId = userData.tenantID || userData.tenant_id; 595 596 if (!tenantId) { 597 console.warn('Keine Tenant-ID gefunden.'); 598 return; 599 } 600 601 // Lösche ALLE Kontakte-Caches für diesen Benutzer/Tenant (mit und ohne Suffix) 602 const mainCacheKey = `contacts_${user.uid}_${tenantId}`; 603 localStorage.removeItem(mainCacheKey); 604 localStorage.removeItem(`${mainCacheKey}_desktop`); 605 localStorage.removeItem(`${mainCacheKey}_mobile`); 606 console.log('[clearContactsCache] Alle Cache-Keys gelöscht:', mainCacheKey); 607 608 // Lösche alle gefilterten Kontaktlisten-Caches
609 const keysToRemove = []; 610 for (let i = 0; i < localStorage.length; i++) { 611 const key = localStorage.key(i); 612 if (key && (key.startsWith('filtered_contacts_') || 613 key.startsWith('contact_details_'))) { 614 keysToRemove.push(key); 615 } 616 } 617 618 // Lösche die gesammelten Schlüssel 619 keysToRemove.forEach(key => { 620 localStorage.removeItem(key); 621 }); 622 623 // Leere auch das interne contacts-Array 624 this.contacts = []; 625 this.currentFilteredContacts = null; 626 627 // console.log(`Cache gelöscht: ${mainCacheKey} und ${keysToRemove.length} weitere Einträge`); 628 } catch (error) { 629 console.error('Fehler beim Löschen des Kontakt-Caches:', error); 630 } 631 } 632 633 async init() { 634 // showContactsLoadingIndicator(); // Ladeindikator deaktiviert für bessere Performance 635 try { 636 // console.log("Contacts.init gestartet"); 637 // Ladeindikator anzeigen <-- Entfernt 638 639 // Prüfe, ob Benutzer angemeldet ist 640 const user = auth.currentUser; 641 if (!user) { 642 // console.log('Benutzer nicht angemeldet. Kontakt-Initialisierung wird übersprungen.'); 643 // hideContactsLoadingIndicator(); // Ladeindikator deaktiviert 644 return; 645 } 646 647 // Lade das User-Dokument 648 const userDocSnap = await _phase8UserSnap(user.uid); /* phase8 (was userDocRef + getDoc) */ 649 if (userDocSnap.exists()) { 650 this.userDoc = userDocSnap.data(); 651 } 652 653 // Prüfe, ob der Kontakt-Container existiert 654 const container = document.getElementById('contactsContainer'); 655 if (!container) { 656 console.log('Kontakt-Container nicht gefunden. Seite wird vermutlich nicht angezeigt.'); 657 // hideContactsLoadingIndicator(); // Ladeindikator deaktiviert 658 return; 659 } 660 661 // Setze sofort einen Platzhalter, damit etwas sichtbar ist 662 console.log('[init] Setze Platzhalter in Container'); 663 container.innerHTML = ` 664 <div class="text-center p-4"> 665 <div class="spinner-border text-primary" role="status"> 666 <span class="visually-hidden">${t('Lade Kontakte...')}</span> 667 </div> 668 <p class="mt-2">${t('Kontakte werden geladen...')}</p> 669 </div> 670 `; 671 672 // Prüfe ob wir von der Detailseite zurückkommen 673 const savedState = sessionStorage.getItem('contactsListState'); 674 // VERBESSERT: Prüfe ob DOM-Cache existiert, nicht nur Timestamp 675 const domStateStr = sessionStorage.getItem('contactsListDOM'); 676 const fromDetailPage = !!(savedState && domStateStr); // True wenn beide Caches existieren 677 678 // Lead-Status nur laden wenn noch nicht vorhanden oder bei normaler Initialisierung 679 if (!this.leadStatuses || this.leadStatuses.length === 0 || !fromDetailPage) { 680 await this.loadLeadStatuses(); // Lade Lead-Status 681 this.updateFilterDropdowns(); // Aktualisiere Filter-Dropdowns mit Lead-Status 682 } 683 684 // SIP-Account prüfen 685 await this.checkSipAccount(); 686 687 // Optimierte Ladelogik 688 if (fromDetailPage) { 689 // Wenn wir von der Detailseite zurückkommen 690 // console.log('Kehre von Detailseite zurück - prüfe gespeicherten DOM'); 691 692 // domStateStr wurde schon oben geholt 693 let domRestored = false; 694 695 if (domStateStr) { 696 try { 697 const domState = JSON.parse(domStateStr); 698 // Prüfe ob DOM-Cache noch frisch ist (max 5 Minuten alt)
699 if (domState.timestamp > Date.now() - 300000) { 700 const contactsContainer = document.getElementById('contactsContainer'); 701 if (contactsContainer) { 702 // console.log('Stelle DOM-Zustand wieder her'); 703 contactsContainer.innerHTML = domState.html; 704 domRestored = true; 705 706 // Stelle Kontakte-Daten wieder her 707 const contactsDataStr = sessionStorage.getItem('contactsData'); 708 if (contactsDataStr) { 709 try { 710 const contactsData = JSON.parse(contactsDataStr); 711 this.contacts = contactsData.contacts || []; 712 this.currentFilteredContacts = contactsData.currentFilteredContacts; 713 // console.log(`Kontakte-Daten wiederhergestellt: ${this.contacts.length} Kontakte`); 714 } catch (e) { 715 console.warn('Konnte Kontakte-Daten nicht wiederherstellen:', e); 716 // Fallback: Lade Kontakte aus localStorage Cache 717 if (!this.contacts || this.contacts.length === 0) { 718 await this.loadLastCachedContacts(); 719 } 720 } 721 } else { 722 // Keine Kontakte in SessionStorage, versuche localStorage 723 if (!this.contacts || this.contacts.length === 0) { 724 await this.loadLastCachedContacts(); 725 } 726 } 727 728 // Stelle Scroll-Position wieder her 729 setTimeout(() => { 730 window.scrollTo(0, domState.scrollTop); 731 }, 0); 732 733 // Stelle Filter-Werte wieder her 734 if (savedState) { 735 const state = JSON.parse(savedState); 736 if (state.searchTerm) document.querySelector('#searchInput').value = state.searchTerm; 737 if (state.leadStatus) document.querySelector('#leadStatusFilter').value = state.leadStatus; 738 if (state.task) document.querySelector('#aufgabeFilter').value = state.task; 739 if (state.branche) document.querySelector('#brancheFilter').value = state.branche; 740 if (state.dateFrom) document.querySelector('#dateFromFilter').value = state.dateFrom; 741 if (state.dateTo) document.querySelector('#dateToFilter').value = state.dateTo; 742 this.currentPage = state.currentPage || 1; 743 } 744 745 // Binde Event-Listener neu 746 this.bindPaginationEvents(); 747 this.setupContactEventListeners(); 748 this.updateSmartListsUI(); 749 this.bindPageControls(true); 750 } 751 } 752 } catch (e) { 753 console.warn('Konnte DOM nicht wiederherstellen:', e); 754 } 755 } 756 757 // Fallback nur wenn DOM nicht wiederhergestellt wurde 758 if (!domRestored) { 759 // console.log('DOM-Wiederherstellung fehlgeschlagen, verwende Fallback'); 760 if (this.contacts && this.contacts.length > 0) { 761 // console.log(`Verwende ${this.contacts.length}
761 Kontakte aus dem Speicher`); 762 await this.renderContacts(this.contacts); 763 } else { 764 const cachedLoaded = await this.loadLastCachedContacts(); 765 if (!cachedLoaded) { 766 await this.loadContacts(); 767 } 768 } 769 } 770 771 // Initialisierung nur wenn noch nicht vorhanden 772 if (!this.smartListModal) { 773 this.initializeModal(); // Für neuen Kontakt Modal 774 this.initializeAutoCallRulesModal(); // NEU: AutoCall Regel Modal initialisieren 775 } 776 777 // Smart Lists sind bereits geladen, überspringe initializeSmartLists 778 // console.log('Ãberspringe Smart Lists Laden - verwende vorhandene Daten'); 779 780 // Hintergrund-Sync nur nach einer Weile 781 setTimeout(() => this.scheduleBackgroundSync(), 5000); 782 } else { 783 // Normale Initialisierung - lösche alten DOM-Cache 784 sessionStorage.removeItem('contactsListDOM'); 785 sessionStorage.removeItem('contactsListState'); 786 sessionStorage.removeItem('contactsData'); 787 // console.log('Normale Initialisierung - DOM-Cache gelöscht'); 788 789 await this.loadContacts(); // Rendert Kontakte UND den Platzhalter
790 this.scheduleBackgroundSync(); 791 792 // Initialisiere Modals und Smart List Daten 793 this.initializeModal(); // Für neuen Kontakt Modal 794 this.initializeAutoCallRulesModal(); // NEU: AutoCall Regel Modal initialisieren 795 await this.initializeSmartLists(); // Lädt Smart List Daten (ohne UI Update) 796 } 797 798 // Prüfe auf gespeicherte aktive Smart List und wende sie an - NUR bei normaler Initialisierung 799 if (!fromDetailPage) { 800 const storedActiveSmartListId = localStorage.getItem('activeSmartListId'); 801 let smartListApplied = false; // Flag, ob eine Smart List angewendet wurde 802 if (storedActiveSmartListId) { 803 // console.log("Found active smart list ID in storage:", storedActiveSmartListId); 804 const exists = this.smartLists.some(list => list.id === storedActiveSmartListId); 805 if (exists) { 806 // console.log("Applying stored active smart list:", storedActiveSmartListId); 807 // Wende die Smart List an, bevor die UI für Smart Lists erstellt wird 808 await this.applySmartList(storedActiveSmartListId); 809 smartListApplied = true; // Setze das Flag 810 } else { 811 // console.log("Stored active smart list ID not found in current lists, removing from storage."); 812 localStorage.removeItem('activeSmartListId'); 813 } 814 } 815 } 816 817 // Jetzt, da der Platzhalter existiert UND die Smart List Daten geladen sind: 818 // Update Smart Lists UI *nach* potentiellem Anwenden einer gespeicherten Liste 819 // Aber NUR wenn der Platzhalter existiert 820 if (document.getElementById('smart-lists-placeholder')) { 821 this.updateSmartListsUI(); 822 } else { 823 // console.log('[init] Smart-Listen Platzhalter noch nicht vorhanden, überspringe updateSmartListsUI'); 824 } 825 826 // Binde Listener für die dynamisch gerenderten Listenelemente (Buttons, Tabelle etc.) 827 // NACHDEM loadContacts die Tabelle initial gerendert hat (oder applySmartList)
828 // Wird bereits in renderContactsImmediate aufgerufen, daher hier nur als Fallback 829 if (!fromDetailPage) { 830 this.setupContactEventListeners(); 831 } 832 833 // Initialisiere die Tooltips 834 const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); 835 tooltipTriggerList.map(function (tooltipTriggerEl) { 836 return new bootstrap.Tooltip(tooltipTriggerEl); 837 }); 838 839 // Binde Listener für statische Seitenelemente (Suche, Filter) 840 // Erzwinge das Neubinden, indem wir den Flag explizit zurücksetzen 841 // console.log("[init] Calling bindPageControls..."); 842 this.bindPageControls(true); // Force rebind parameter 843 // console.log("[init] bindPageControls completed."); 844 845 // Ladeindikator ausblenden <-- HIER GANZ AM ENDE DES TRY-BLOCKS 846 // hideContactsLoadingIndicator(); // Ladeindikator deaktiviert 847 848 } catch (error) { 849 console.error('Error initializing contacts:', error); 850 UI.showError(t('Fehler beim Laden der Kontakte')); 851 // hideContactsLoadingIndicator(); // Ladeindikator deaktiviert 852 } finally { 853 // this.setupEventDelegation(); // Entfernt, da unnötig/potenziell störend 854 // console.log("Contacts.init abgeschlossen"); 855 } 856 } 857 858 // Bindet Listener für statische Seitenelemente (Suche, Filter) - Nur einmal aufrufen 859 bindPageControls(forceRebind = false) { 860 if (this.pageControlsBound && !forceRebind) { 861 // console.log("[bindPageControls] Already bound, skipping."); 862 return; 863 } 864 865 if (forceRebind) { 866 // console.log("[bindPageControls] Force rebinding event listeners..."); 867 this.pageControlsBound = false; 868 } 869 // console.log("[bindPageControls] Binding search and filter listeners."); 870 871 // Entferne vorherigen Clear-Button-Listener falls vorhanden 872 if (this.clearButtonListener) { 873 // console.log("[bindPageControls] Removing existing clear button listener"); 874 document.removeEventListener('click', this.clearButtonListener); 875 this.clearButtonListener = null; 876 } 877 878 // Verwende Event-Delegation für robuste Clear-Button-Unterstützung 879 this.clearButtonListener = (e) => { 880 // Clear Search Button (verschiedene mögliche Selektoren) 881 if (e.target.closest('#clearSearchBtn') || 882 e.target.closest('.clear-search-btn') || 883 e.target.closest('[data-action="clear-search"]') || 884 (e.target.closest('.input-group') && e.target.closest('button') && e.target.closest('#searchInput')?.parentElement)) { 885 886 // console.log("[bindPageControls] Clear search button clicked."); 887 const searchInput = document.querySelector('#searchInput'); 888 if (searchInput) { 889 searchInput.value = ''; 890 // Wenn Suche gelöscht wird, ist keine Smart List mehr aktiv 891 localStorage.removeItem('activeSmartListId'); 892 this.activeSmartListId = null; 893 this.applyFilters(); // Wende leere Filter an, um alle Kontakte zu zeigen 894 // Speichere den aktualisierten Zustand 895 this.storeContactsState(); 896 } 897 e.preventDefault(); 898 e.stopPropagation(); 899 } 900 901 // Clear Lead Status Filter Button 902 if (e.target.closest('#clearLeadStatusBtn') || 903 e.target.closest('.clear-lead-status-btn') || 904 e.target.closest('[data-action="clear-lead-status"]')) { 905 906 // console.log("[bindPageControls] Clear lead status filter button clicked."); 907 const leadStatusFilter = document.querySelector('#leadStatusFilter'); 908 if (leadStatusFilter) leadStatusFilter.value = ''; 909 this.applyFilters(); 910 this.storeContactsState(); 911 e.preventDefault(); 912 e.stopPropagation(); 913 } 914 915 // Clear Branche Filter Button 916 if (e.target.closest('#clearBrancheBtn') || 917 e.target.closest('#clearIndustryBtn') || 918 e.target.closest('.clear-branche-btn') || 919 e.target.closest('.clear-industry-btn') || 920 e.target.closest('[data-action="clear-branche"]') || 921 e.target.closest('[data-action="clear-industry"]')) { 922 923 // console.log("[bindPageControls] Clear branche filter button clicked."); 924 const brancheFilter = document.querySelector('#brancheFilter'); 925 if (brancheFilter) brancheFilter.value = ''; 926 this.applyFilters(); 927 this.storeContactsState(); 928 e.preventDefault(); 929 e.stopPropagation(); 930 } 931 932 // Clear Task/Aufgabe Filter Button 933 if (e.target.closest('#clearTaskBtn') || 934 e.target.closest('#clearAufgabeBtn') || 935 e.target.closest('.clear-task-btn') || 936 e.target.closest('.clear-aufgabe-btn') || 937 e.target.closest('[data-action="clear-task"]') || 938 e.target.closest('[data-action="clear-aufgabe"]')) { 939 940 // console.log("[bindPageControls] Clear task filter button clicked."); 941 const taskFilter = document.querySelector('#aufgabeFilter'); 942 if (taskFilter) taskFilter.value = ''; 943 this.applyFilters(); 944 this.storeContactsState(); 945 e.preventDefault(); 946 e.stopPropagation(); 947 } 948 949 // Clear Date Filter Button (verschiedene mögliche Selektoren) 950 if (e.target.closest('#clearDateFilterBtn') || 951 e.target.closest('.clear-date-filter-btn') || 952 e.target.closest('[data-action="clear-date-filter"]')) { 953 954 // console.log("[bindPageControls] Clear date filter button clicked."); 955 const dateFromFilter = document.querySelector('#dateFromFilter'); 956 const dateToFilter = document.querySelector('#dateToFilter'); 957 if (dateFromFilter) dateFromFilter.value = ''; 958 if (dateToFilter) dateToFilter.value = ''; 959 this.applyFilters(); 960 // Speichere den aktualisierten Zustand 961 this.storeContactsState(); 962 e.preventDefault(); 963 e.stopPropagation(); 964 } 965 966 // GENERIC Clear Filter Button - fängt alle anderen Clear-Buttons ab 967 if (e.target.closest('.clear-filter') || 968 e.target.closest('.filter-clear') || 969 e.target.closest('.btn-clear-filter') || 970 (e.target.closest('button') && ( 971 e.target.closest('button').textContent.includes('Ã') || 972 e.target.closest('button').innerHTML.includes('times') || 973 e.target.closest('button').innerHTML.includes('fa-times') || 974 e.target.closest('button').innerHTML.includes('fa-close') 975 ))) { 976 977 // console.log("[bindPageControls] Generic clear filter button clicked."); 978
979 // Versuche herauszufinden, welcher Filter gelöscht werden soll 980 const button = e.target.closest('button'); 981 const filterContainer = button?.closest('.form-group') || button?.closest('.filter-group') || button?.closest('.input-group'); 982 983 if (filterContainer) { 984 const select = filterContainer.querySelector('select'); 985 const input = filterContainer.querySelector('input'); 986 987 if (select) { 988 // console.log("[bindPageControls] Clearing select element:", select.id); 989 select.value = ''; 990 } 991 if (input) { 992 // console.log("[bindPageControls] Clearing input element:", input.id); 993 input.value = ''; 994 } 995 996 this.applyFilters(); 997 this.storeContactsState(); 998 } 999 1000 e.preventDefault(); 1001 e.stopPropagation(); 1002 } 1003 1004 // Clear All Filters Button (neuer Button) 1005 if (e.target.closest('#clearAllFiltersBtn') || 1006 e.target.closest('.clear-all-filters-btn') || 1007 e.target.closest('[data-action="clear-all-filters"]')) { 1008 1009 // console.log("[bindPageControls] Clear all filters button clicked."); 1010 this.clearAllFilters(); 1011 e.preventDefault(); 1012 e.stopPropagation(); 1013 } 1014 }; 1015 1016 // Füge den Event-Listener hinzu 1017 document.addEventListener('click', this.clearButtonListener); 1018 // console.log("[bindPageControls] Clear button listener added to document"); 1019 1020 // --- Suche --- 1021 const searchInput = document.querySelector('#searchInput'); 1022 if (searchInput) { 1023 // Verwende debounceSearch direkt als Listener 1024 searchInput.addEventListener('input', (e) => this.debounceSearch(e)); 1025 } else { 1026 console.warn("[bindPageControls] Search input not found."); 1027 } 1028 1029 // --- Filter Dropdowns --- 1030 const leadStatusFilter = document.querySelector('#leadStatusFilter'); 1031 const taskFilter = document.querySelector('#aufgabeFilter'); 1032 const brancheFilter = document.querySelector('#brancheFilter'); 1033 const dateFromFilter = document.querySelector('#dateFromFilter'); 1034 const dateToFilter = document.querySelector('#dateToFilter'); 1035 1036 // Entferne vorherige Filter-Listener falls vorhanden 1037 if (this.filterChangeHandler) { 1038 [leadStatusFilter, taskFilter, brancheFilter, dateFromFilter, dateToFilter].forEach(filter => { 1039 if (filter) { 1040 filter.removeEventListener('change', this.filterChangeHandler); 1041 } 1042 }); 1043 } 1044 1045 // Einziger Filter-Handler 1046 this.filterChangeHandler = () => { 1047 // console.log("[filterChangeHandler] Filter change detected"); 1048 if (this.filterTimeout) { 1049 clearTimeout(this.filterTimeout); 1050 } 1051 this.filterTimeout = setTimeout(() => { 1052 // console.log("[filterChangeHandler] Applying filters after timeout"); 1053 this.applyFilters(); 1054 // Speichere den neuen Zustand nach Filteranwendung 1055 this.storeContactsState(); 1056 }, 300); 1057 }; 1058 1059 // Binde Filter-Listener 1060 [leadStatusFilter, taskFilter, brancheFilter, dateFromFilter, dateToFilter].forEach((filter, index) => { 1061 if (filter) { 1062 filter.addEventListener('change', this.filterChangeHandler); 1063 // console.log(`[bindPageControls] Filter listener bound to element ${index}:`, filter.id || filter.className); 1064 } else { 1065 console.warn(`[bindPageControls] Filter element ${index} not found.`); 1066 } 1067 }); 1068 1069 this.pageControlsBound = true; 1070 // console.log("[bindPageControls] All listeners bound successfully - Search, Filter, and Clear buttons ready."); 1071 } 1072 1073 // Neue Hilfsmethode zum Löschen aller Filter 1074 clearAllFilters() { 1075 // console.log("[clearAllFilters] Clearing all filters and search."); 1076 1077 // Suche leeren 1078 const searchInput = document.querySelector('#searchInput'); 1079 if (searchInput) { 1080 searchInput.value = ''; 1081 } 1082 1083 // Alle Filter-Dropdowns zurücksetzen 1084 const filters = [ 1085 '#leadStatusFilter', 1086 '#aufgabeFilter', 1087 '#brancheFilter' 1088 ]; 1089
1090 filters.forEach(selector => { 1091 const element = document.querySelector(selector); 1092 if (element) { 1093 element.value = ''; 1094 } 1095 }); 1096 1097 // Datumsfilter leeren 1098 const dateFromFilter = document.querySelector('#dateFromFilter'); 1099 const dateToFilter = document.querySelector('#dateToFilter'); 1100 if (dateFromFilter) dateFromFilter.value = ''; 1101 if (dateToFilter) dateToFilter.value = ''; 1102 1103 // Smart List deaktivieren 1104 localStorage.removeItem('activeSmartListId'); 1105 this.activeSmartListId = null; 1106 1107 // Filter anwenden (zeigt alle Kontakte) 1108 this.applyFilters(); 1109 1110 // Zustand speichern 1111 this.storeContactsState(); 1112 1113 UI.showSuccess(t('Alle Filter wurden geleert.')); 1114 } 1115 1116 // Neue Methode zum Prüfen ob SIP-Account vorhanden ist 1117 async checkSipAccount() { 1118 try { 1119 const user = auth.currentUser; 1120 if (!user) { 1121 this.hasSipAccount = false; 1122 return; 1123 } 1124 1125 // Lade User-Dokument falls noch nicht vorhanden 1126 if (!this.userDoc) { 1127 const userDocSnap = await _phase8UserSnap(user.uid); /* phase8 (was userDocRef + getDoc) */ 1128 if (userDocSnap.exists()) { 1129 this.userDoc = userDocSnap.data(); 1130 } 1131 } 1132 1133 const tenantId = this.userDoc?.tenantID || this.userDoc?.tenant_id; 1134 if (!tenantId) { 1135 this.hasSipAccount = false; 1136 return; 1137 } 1138 1139 // Lade Tenant-Dokument und prüfe auf SIP-Accounts 1140 /* phase8c */ const _me = await _c8c_userMe(); const tenantDocSnap = { exists: () => !!_me, data: () => ({ name: (_me && (_me.companyName || _me.company)) || '', company: (_me && _me.company) || '', country: (_me && _me.country) || 'DE' }) }; 1141 1142 if (tenantDocSnap.exists()) { 1143 const tenantData = tenantDocSnap.data(); 1144 // Prüfe ob telephonySettings.sipTrunks existiert und nicht leer ist 1145 const sipTrunks = tenantData?.telephonySettings?.sipTrunks; 1146 this.hasSipAccount = sipTrunks && Object.keys(sipTrunks).length > 0; 1147 console.log('[checkSipAccount] SIP-Account gefunden:', this.hasSipAccount); 1148 } else { 1149 this.hasSipAccount = false; 1150 } 1151 } catch (error) { 1152 console.error('[checkSipAccount] Fehler beim Prüfen des SIP-Accounts:', error); 1153 this.hasSipAccount = false; 1154 } 1155 } 1156 1157 // Neue Methode zum Laden der Lead-Status (Standard + Custom über app.js) 1158 async loadLeadStatuses() { 1159 try { 1160 const user = auth.currentUser; 1161 if (!user) { 1162 console.warn("Kein Benutzer angemeldet, Standard-Lead-Status werden verwendet."); 1163 // Fallback zu einem minimalen Set oder leer lassen? 1164 this.leadStatuses = DEFAULT_LEAD_STATUSES; 1165 return; 1166 } 1167 1168 // Stelle sicher, dass userDoc geladen ist (sollte durch init geschehen sein) 1169 if (!this.userDoc) { 1170 console.warn("Benutzerdokument nicht geladen, versuche es jetzt zu laden..."); 1171 const userDocSnap = await _phase8UserSnap(user.uid); /* phase8 (was userDocRef + getDoc) */ 1172 if (userDocSnap.exists()) { 1173 this.userDoc = userDocSnap.data(); 1174 } else { 1175 console.error("Benutzerdokument konnte nicht geladen werden."); 1176 this.leadStatuses = DEFAULT_LEAD_STATUSES; // Fallback 1177 return; 1178 } 1179 } 1180 1181 const tenantId = this.userDoc.tenantID || this.userDoc.tenant_id; 1182 1183 if (!tenantId) { 1184 console.error("Keine Tenant-ID im Benutzerdokument gefunden."); 1185 this.leadStatuses = DEFAULT_LEAD_STATUSES; // Fallback 1186 return; 1187 } 1188 1189 // Rufe die zentrale Funktion aus app.js auf 1190 this.leadStatuses = await loadCustomLeadStatuses(tenantId); 1191 // console.log('Geladene Lead-Status (über app.js):', this.leadStatuses); 1192 1193 } catch (error) { 1194 console.error('Fehler beim Laden der Lead-Status über loadCustomLeadStatuses:', error); 1195 // Fallback zu Standard-Status bei Fehlern 1196 this.leadStatuses = DEFAULT_LEAD_STATUSES; 1197 } 1198 } 1199 1200 initializeModal() { 1201 const modalElement = document.getElementById('addContactModal'); 1202 if (modalElement && !this.addContactModal) { 1203 // Initialize modal with Bootstrap options 1204 this.addContactModal = new bootstrap.Modal(modalElement); 1205 1206 // Add form submit handler 1207 const form = document.getElementById('addContactForm'); 1208 if (form) { 1209 form.onsubmit = async (e) => { 1210 e.preventDefault(); 1211 await this.createNewContact(); 1212 }; 1213 } 1214 1215 // Listen for modal hidden event to reset form 1216 modalElement.addEventListener('hidden.bs.modal', () => { 1217 const form = document.getElementById('addContactForm'); 1218 if (form) { 1219 form.reset(); 1220 } 1221 }); 1222 1223 // Listen for modal shown event to focus first input 1224 modalElement.addEventListener('shown.bs.modal', () => { 1225 const firstInput = modalElement.querySelector('input[name="name"]'); 1226 if (firstInput) { 1227 firstInput.focus(); 1228 } 1229 }); 1230 } 1231 } 1232 1233 // NEU: Modal für AutoCall-Regeln initialisieren 1234 initializeAutoCallRulesModal() { 1235 // HTML für das Modal (wird später dynamisch hinzugefügt oder muss in index.html sein) 1236 const modalHtml = ` 1237 <div class="modal fade" id="autoCallRulesModal" tabindex="-1" aria-labelledby="autoCallRulesModalLabel" aria-hidden="true"> 1238 <div class="modal-dialog modal-xl"> <!-- Geändert zu modal-xl für mehr Breite --> 1239 <div class="modal-content"> 1240 <div class="modal-header"> 1241 <h5 class="modal-title" id="autoCallRulesModalLabel">${t('AutoCall Regeln verwalten')}</h5> 1242 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> 1243 </div> 1244 <div class="modal-body"> 1245 <h6>${t('Bestehende Regeln')}</h6> 1246 <div class="table-responsive mb-4"> 1247 <table class="table table-sm"> 1248 <thead> 1249 <tr> 1250 <th>${t('Trigger Status')}</th> 1251 <th>${t('Vorlage')}</th> 1252 <th>${t('Aktiv')}</th> 1253 <th>${t('Aktionen')}</th> 1254 </tr> 1255 </thead> 1256 <tbody id="autoCallRulesTableBody"> 1257 1258 <tr><td colspan="4" class="text-center">${t('Lade Regeln...')}</td></tr> 1259 </tbody> 1260 </table> 1261 </div> 1262 1263 <h6>${t('Neue Regel hinzufügen')}</h6> 1264 <form id="newAutoCallRuleForm"> 1265 <div class="row g-3 align-items-end"> 1266 <div class="col-md-4"> 1267 <label for="ruleLeadStatus" class="form-label">${t('Wenn Lead-Status:')}</label> 1268 <select class="form-select" id="ruleLeadStatus" required> 1269 <option value="">${t('Wählen...')}</option> 1270 1271 </select> 1272 </div> 1273 <div class="col-md-4"> 1274 <label for="ruleTemplate" class="form-label">${t('Verwende Vorlage:')}</label> 1275 <select class="form-select" id="ruleTemplate" required> 1276 <option value="">${t('Wählen...')}</option> 1277 1278 </select> 1279 </div> 1280 <div class="col-md-2"> 1281 <div class="form-check mb-2"> 1282 <input class="form-check-input" type="checkbox" id="ruleActive" checked /> 1283 <label class="form-check-label" for="ruleActive"> 1284 ${t('Aktiv')} 1285 </label> 1286 </div> 1287 </div> 1288 <div class="col-md-2"> 1289 <button type="submit" class="btn btn-primary w-100">${t('Regel hinzufügen')}</button> 1290 </div> 1291 </div> 1292 </form> 1293 </div> 1294 <div class="modal-footer"> 1295 <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">${t('SchlieÃen')}</button> 1296 </div> 1297 </div> 1298 </div> 1299 </div> 1300 `; 1301 1302 // Füge das Modal zum Body hinzu, wenn es noch nicht existiert 1303 if (!document.getElementById('autoCallRulesModal')) { 1304 document.body.insertAdjacentHTML('beforeend', modalHtml); 1305 } 1306 1307 const modalElement = document.getElementById('autoCallRulesModal'); 1308 if (modalElement) { 1309 this.autoCallRulesModal = new bootstrap.Modal(modalElement); 1310 1311 // Event Listener für das Formular (wird nur einmal hinzugefügt) 1312 const form = document.getElementById('newAutoCallRuleForm'); 1313 if (form && !form.dataset.listenerAttached) { 1314 form.addEventListener('submit', (e) => this.handleSaveAutoCallRule(e)); 1315 form.dataset.listenerAttached = 'true'; // Markieren, dass Listener angehängt ist 1316 } 1317 // Event Listener für die Tabelle (Event Delegation) 1318 const tableBody = document.getElementById('autoCallRulesTableBody'); 1319 if (tableBody && !tableBody.dataset.listenerAttached) { 1320 tableBody.addEventListener('click', (e) => { 1321 if (e.target.classList.contains('toggle-rule-active')) { 1322 const ruleId = e.target.dataset.ruleId; 1323 const currentStatus = e.target.checked; 1324 this.toggleAutoCallRuleActive(ruleId, currentStatus); 1325 } else if (e.target.closest('.delete-rule')) { 1326 const ruleId = e.target.closest('.delete-rule').dataset.ruleId; 1327 this.deleteAutoCallRule(ruleId); 1328 } 1329 }); 1330 tableBody.dataset.listenerAttached = 'true'; // Markieren 1331 } 1332 } else { 1333 console.error("AutoCall Rules Modal Element konnte nicht gefunden werden."); 1334 } 1335 } 1336 1337 // NEU: Methode zum Aktualisieren der AutoCall-Modal-Texte nach Sprachwechsel 1338 updateAutoCallModalTexts() { 1339 const modal = document.getElementById('autoCallRulesModal'); 1340 if (!modal) return; 1341 1342 // Modal-Titel aktualisieren 1343 const modalTitle = modal.querySelector('#autoCallRulesModalLabel'); 1344 if (modalTitle) modalTitle.textContent = t('AutoCall Regeln verwalten'); 1345 1346 // Tabellen-Header aktualisieren 1347 const headers = modal.querySelectorAll('thead th'); 1348 if (headers.length >= 4) { 1349 headers[0].textContent = t('Trigger Status'); 1350 headers[1].textContent = t('Vorlage'); 1351 headers[2].textContent = t('Aktiv'); 1352 headers[3].textContent = t('Aktionen'); 1353 } 1354 1355 // Ãberschriften aktualisieren 1356 const headings = modal.querySelectorAll('h6'); 1357 if (headings.length >= 2) { 1358 headings[0].textContent = t('Bestehende Regeln'); 1359 headings[1].textContent = t('Neue Regel hinzufügen'); 1360 } 1361 1362 // Formular-Labels aktualisieren 1363 const leadStatusLabel = modal.querySelector('label[for="ruleLeadStatus"]'); 1364 if (leadStatusLabel) leadStatusLabel.textContent = t('Wenn Lead-Status:'); 1365 1366 const templateLabel = modal.querySelector('label[for="ruleTemplate"]'); 1367 if (templateLabel) templateLabel.textContent = t('Verwende Vorlage:'); 1368 1369 const activeLabel = modal.querySelector('label[for="ruleActive"]'); 1370 if (activeLabel) activeLabel.textContent = t('Aktiv'); 1371 1372 // Button-Texte aktualisieren 1373 const submitButton = modal.querySelector('button[type="submit"]'); 1374 if (submitButton) submitButton.textContent = t('Regel hinzufügen'); 1375 1376 const closeButton = modal.querySelector('.modal-footer .btn-secondary'); 1377 if (closeButton) closeButton.textContent = t('SchlieÃen'); 1378 1379 // Dropdown-Optionen aktualisieren 1380 const leadStatusSelect = modal.querySelector('#ruleLeadStatus'); 1381 if (leadStatusSelect && leadStatusSelect.children.length > 0) { 1382 leadStatusSelect.children[0].textContent = t('Wählen...'); 1383 } 1384 1385 const templateSelect = modal.querySelector('#ruleTemplate'); 1386 if (templateSelect && templateSelect.children.length > 0) { 1387 templateSelect.children[0].textContent = t('Wählen...'); 1388 } 1389 1390 // Tabelle neu rendern, falls Regeln vorhanden sind 1391 if (this.autoCallRules && this.autoCallRules.length > 0) { 1392 this.renderAutoCallRulesTable(this.autoCallRules); 1393 } 1394 } 1395 1396 // NEU: Methode zum Anzeigen des Modals und Laden der Daten 1397 async showAutoCallRulesModal() { 1398 if (!this.autoCallRulesModal) { 1399 console.error("AutoCall Rules Modal ist nicht initialisiert."); 1400 UI.showError("Fehler beim Ãffnen der Regeleinstellungen."); 1401 return; 1402 } 1403 1404 // Ladeindikator in Tabelle anzeigen 1405 const tableBody = document.getElementById('autoCallRulesTableBody'); 1406 if(tableBody) tableBody.innerHTML = `<tr><td colspan="4" class="text-center"><div class="spinner-border spinner-border-sm" role="status"><span class="visually-hidden">${t('Lade Regeln...')}</span></div></td></tr>`; 1407 1408 // Fülle Lead Status Dropdown (verwende geladene Status) 1409 const leadStatusSelect = document.getElementById('ruleLeadStatus'); 1410 if (leadStatusSelect) { 1411 leadStatusSelect.innerHTML = `<option value="">${t('Wählen...')}</option>`; 1412 // Stelle sicher, dass leadStatuses geladen ist 1413 // if (!this.leadStatuses || this.leadStatuses.length === 0) { 1414 // await this.loadLeadStatuses(); // Lade Status, falls noch nicht geschehen 1415 // } 1416 // Entferne die Schleife über alle Status
1417 // this.leadStatuses.forEach(status => { 1418 // leadStatusSelect.innerHTML += `<option value="${status}">${t(status)}</option>`; 1419 // }); 1420 1421 // Füge nur die Option "Anrufen" hinzu 1422 leadStatusSelect.innerHTML += `<option value="Anrufen">${t('Anrufen')}</option>`; 1423 1424 } else { 1425 console.error("Lead Status Select Element ('ruleLeadStatus') nicht gefunden."); 1426 } 1427 1428 // Lade Vorlagen und Regeln parallel 1429 try { 1430 await Promise.all([ 1431 this.populateTemplateDropdown(), // Lädt Vorlagen und füllt Dropdown 1432 this.loadAutoCallRules() // Lädt Regeln und rendert Tabelle 1433 ]); 1434 } catch (error) { 1435 console.error("Fehler beim Laden der AutoCall-Regeldaten:", error); 1436 UI.showError(t('Fehler beim Laden der Regeleinstellungen.')); 1437 if(tableBody) tableBody.innerHTML = `<tr><td colspan="4" class="text-center text-danger">${t('Fehler beim Laden.')}</td></tr>`; 1438 } 1439 1440 // Ãbersetze das Modal vor dem Anzeigen 1441 this.updateAutoCallModalTexts(); 1442 1443 this.autoCallRulesModal.show(); 1444 } 1445 1446 // NEU: Methode zum Laden der MultiCall-Vorlagen und Befüllen des Dropdowns 1447 async populateTemplateDropdown() { 1448 const templateSelect = document.getElementById('ruleTemplate'); 1449 if (!templateSelect) { 1450 console.error("Template Select Element ('ruleTemplate') nicht gefunden."); 1451 return; 1452 } 1453 templateSelect.innerHTML = `<option value="">${t('Lade Vorlagen...')}</option>`; 1454 templateSelect.disabled = true; 1455 1456 try { 1457 const user = auth.currentUser; 1458 if (!user) throw new Error(t('Benutzer nicht angemeldet.')); 1459 1460 // Tenant ID ermitteln (aus userDoc, wie in anderen Methoden) 1461 if (!this.userDoc) { 1462 console.warn("UserDoc nicht gefunden in populateTemplateDropdown, versuche neu zu laden."); 1463 const userDocSnap = await _phase8UserSnap(user.uid); /* phase8 (was userDocRef + getDoc) */ 1464 if (userDocSnap.exists()) this.userDoc = userDocSnap.data(); 1465 else throw new Error(t('Benutzerprofil nicht gefunden.')); 1466 } 1467 const tenantId = this.userDoc.tenantID || this.userDoc.tenant_id; 1468 if (!tenantId) throw new Error(t('Keine Tenant-ID gefunden.')); 1469 1470 // Korrekter Pfad zur 'multicall'-Sammlung auf Root-Ebene 1471/* phase8c */ this.multiCallTemplates = (await _c8c_listMulticallTemplates()).filter(d => d.id && (d.task || d.name)); 1472 // console.log("Geladene MultiCall-Vorlagen:", this.multiCallTemplates); // Debug Log 1473 1474 // Dropdown befüllen 1475 templateSelect.innerHTML = `<option value="">${t('Wählen...')}</option>`; 1476 if (this.multiCallTemplates.length > 0) {
1477 this.multiCallTemplates.forEach(template => { 1478 // Verwende 'task' oder 'name' für die Anzeige, mit Fallback 1479 const templateName = template.task || template.name || `Vorlage ${template.id.substring(0, 5)}...`; 1480 templateSelect.innerHTML += `<option value="${template.id}">${templateName}</option>`; 1481 }); 1482 templateSelect.disabled = false; 1483 } else { 1484 templateSelect.innerHTML = '<option value="">Keine Vorlagen gefunden</option>'; 1485 templateSelect.disabled = true; // Deaktivieren, wenn keine Vorlagen da sind 1486 } 1487 1488 } catch (error) { 1489 console.error("Fehler beim Laden der MultiCall-Vorlagen:", error); 1490 templateSelect.innerHTML = '<option value="">Fehler beim Laden</option>'; 1491 templateSelect.disabled = true; 1492 } 1493 } 1494 1495 // NEU: Methode zum Laden der AutoCall-Regeln 1496 async loadAutoCallRules() { 1497 const tableBody = document.getElementById('autoCallRulesTableBody'); 1498 if (!tableBody) { 1499 console.error("AutoCall Rules Table Body ('autoCallRulesTableBody') nicht gefunden."); 1500 return; // Frühzeitiger Ausstieg, wenn das Element fehlt 1501 } 1502 tableBody.innerHTML = '<tr><td colspan="4" class="text-center">Lade Regeln...</td></tr>'; // Ladezustand anzeigen 1503 1504 try { 1505 const user = auth.currentUser; 1506 if (!user) throw new Error("Benutzer nicht angemeldet."); 1507 1508 // Stelle sicher, dass userDoc geladen ist 1509 if (!this.userDoc) { 1510 console.warn("UserDoc nicht gefunden in loadAutoCallRules, versuche neu zu laden."); 1511 const userDocSnap = await _phase8UserSnap(user.uid); /* phase8 (was userDocRef + getDoc) */ 1512 if (userDocSnap.exists()) this.userDoc = userDocSnap.data(); 1513 else throw new Error("Benutzerprofil nicht gefunden."); 1514 } 1515 1516 const tenantId = this.userDoc.tenantID || this.userDoc.tenant_id; 1517 if (!tenantId) throw new Error("Keine Tenant-ID gefunden."); 1518 1519 // Pfad zu den Regeln (Tenant-Ebene) - KORREKT 1520/* phase8c */ this.autoCallRules = await AutoCallRulesApi.list(true); 1521 // console.log("Geladene AutoCall-Regeln:", this.autoCallRules); // Debug Log 1522 1523 this.renderAutoCallRulesTable(this.autoCallRules); 1524 1525 } catch (error) { 1526 console.error("Fehler beim Laden der AutoCall-Regeln:", error); 1527 if(tableBody) tableBody.innerHTML = '<tr><td colspan="4" class="text-center text-danger">Fehler beim Laden der Regeln.</td></tr>'; 1528 // Fehler weitergeben, damit showAutoCallRulesModal ihn fangen kann (optional, je nach gewünschtem Verhalten) 1529 // throw error; 1530 } 1531 } 1532 1533 // NEU: Methode zum Rendern der Regel-Tabelle 1534 renderAutoCallRulesTable(rules) { 1535 const tableBody = document.getElementById('autoCallRulesTableBody'); 1536 if (!tableBody) return; 1537 1538 if (!rules || rules.length === 0) { 1539 tableBody.innerHTML = `<tr><td colspan="4" class="text-center">${t('Keine Regeln vorhanden')}</td></tr>`; 1540 return; 1541 } 1542 1543 // Stelle sicher, dass Vorlagen geladen sind, bevor gerendert wird 1544 if (!this.multiCallTemplates || this.multiCallTemplates.length === 0) { 1545 console.warn("Vorlagen sind noch nicht geladen, Regel-Tabelle kann unvollständig sein."); 1546 // Optional: Erneut laden oder warten? Vorerst nur warnen. 1547 } 1548 1549 tableBody.innerHTML = rules.map(rule => { 1550 // Finde den Vorlagennamen (Fallback, falls Vorlage gelöscht wurde oder noch nicht geladen) 1551 const template = this.multiCallTemplates?.find(t => t.id === rule.templateId); 1552 const templateName = template 1553 ? (template.task || template.name || `${t('Vorlage')} ${template.id.substring(0,5)}...`) 1554 : (rule.templateId ? `${t('Unbekannt')} (${rule.templateId.substring(0,5)}...)` : t('Keine Vorlage')); 1555 1556 return ` 1557 <tr> 1558 <td>${rule.triggerLeadStatus || 'N/A'}</td> 1559 <td>${templateName}</td> 1560 <td> 1561 <div class="form-check form-switch"> 1562 <input class="form-check-input toggle-rule-active" type="checkbox" role="switch" 1563 id="ruleSwitch-${rule.id}" data-rule-id="${rule.id}" ${rule.active ? 'checked' : ''} /> 1564 <label class="form-check-label" for="ruleSwitch-${rule.id}">${rule.active ? t('Aktiv') : t('Inaktiv')}</label> 1565 </div> 1566 </td> 1567 <td> 1568 <button class="btn btn-outline-danger btn-sm delete-rule" data-rule-id="${rule.id}" title="Regel löschen">
1569 <i class="fas fa-trash"></i> 1570 </button> 1571 </td> 1572 </tr> 1573 `; 1574 }).join(''); 1575 } 1576 1577 // NEU: Handler zum Speichern einer neuen Regel 1578 async handleSaveAutoCallRule(e) { 1579 e.preventDefault(); 1580 const form = e.target; 1581 const submitButton = form.querySelector('button[type="submit"]'); 1582 if (!submitButton) return; // Sicherstellen, dass der Button existiert 1583 const originalButtonText = submitButton.innerHTML; 1584 submitButton.disabled = true; 1585 submitButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Speichern...'; 1586 1587 try { 1588 const user = auth.currentUser; 1589 if (!user) throw new Error("Benutzer nicht angemeldet."); 1590 1591 // Stelle sicher, dass userDoc geladen ist 1592 if (!this.userDoc) { 1593 console.warn("UserDoc nicht gefunden in handleSaveAutoCallRule, versuche neu zu laden."); 1594 const userDocSnap = await _phase8UserSnap(user.uid); /* phase8 (was userDocRef + getDoc) */ 1595 if (userDocSnap.exists()) this.userDoc = userDocSnap.data(); 1596 else throw new Error("Benutzerprofil nicht gefunden."); 1597 } 1598 1599 const tenantId = this.userDoc.tenantID || this.userDoc.tenant_id; 1600 if (!tenantId) throw new Error("Keine Tenant-ID gefunden."); 1601 1602 const leadStatusSelect = document.getElementById('ruleLeadStatus'); 1603 const templateSelect = document.getElementById('ruleTemplate'); 1604 const activeCheckbox = document.getElementById('ruleActive'); 1605 1606 // Prüfe, ob die Elemente gefunden wurden 1607 if (!leadStatusSelect || !templateSelect || !activeCheckbox) { 1608 throw new Error("Formular-Elemente für neue Regel nicht gefunden."); 1609 } 1610 1611 const leadStatus = leadStatusSelect.value; 1612 const templateId = templateSelect.value; 1613 const active = activeCheckbox.checked; 1614 1615 if (!leadStatus || !templateId) { 1616 UI.showWarning("Bitte wählen Sie einen Status und eine Vorlage aus."); // Geändert zu Warning 1617 return; // Frühzeitiger Ausstieg 1618 } 1619 1620 // Prüfen, ob Regel für diesen Status schon existiert 1621 const existingRule = this.autoCallRules?.find(r => r.triggerLeadStatus === leadStatus); 1622 if (existingRule) { 1623 // Hinweis geben, ohne zu blockieren oder zu fragen 1624 UI.showWarning(`Es existiert bereits eine Regel für den Status '${leadStatus}'. Sie wird überschrieben.`); 1625 // Lösche die alte Regel, bevor die neue hinzugefügt wird 1626 /* phase8c: backend overwrites by status */ 1627 } 1628 1629/* phase8c */ await AutoCallRulesApi.create({ triggerLeadStatus: leadStatus, templateId: templateId, active: active }); 1630 1631 UI.showSuccess("AutoCall Regel erfolgreich gespeichert."); 1632 form.reset(); // Formular zurücksetzen 1633 activeCheckbox.checked = true; // Checkbox standardmäÃig aktivieren 1634 1635 // Regeln neu laden und Tabelle aktualisieren 1636 await this.loadAutoCallRules(); // Dies rendert die Tabelle neu 1637 1638 } catch (error) { 1639 console.error("Fehler beim Speichern der AutoCall Regel:", error); 1640 UI.showError("Fehler beim Speichern der Regel: " + error.message); 1641 } finally { 1642 submitButton.disabled = false; 1643 submitButton.innerHTML = originalButtonText; 1644 } 1645 } 1646 1647 // NEU: Handler zum Aktivieren/Deaktivieren einer Regel 1648 async toggleAutoCallRuleActive(ruleId, newActiveState) { 1649 // console.log(`Toggle rule ${ruleId} to active: ${newActiveState}`); 1650 const label = document.querySelector(`label[for="ruleSwitch-${ruleId}"]`); 1651 const checkbox = document.getElementById(`ruleSwitch-${ruleId}`); 1652 if (label) label.textContent = newActiveState ? 'Aktiviere...' : 'Deaktiviere...'; 1653 if (checkbox) checkbox.disabled = true; // Deaktivieren während der Aktualisierung 1654 1655 try { 1656 const user = auth.currentUser; 1657 if (!user) throw new Error("Benutzer nicht angemeldet."); 1658 1659 // Stelle sicher, dass userDoc geladen ist 1660 if (!this.userDoc) { 1661 console.warn("UserDoc nicht gefunden in toggleAutoCallRuleActive, versuche neu zu laden."); 1662 const userDocSnap = await _phase8UserSnap(user.uid); /* phase8 (was userDocRef + getDoc) */ 1663 if (userDocSnap.exists()) this.userDoc = userDocSnap.data(); 1664 else throw new Error("Benutzerprofil nicht gefunden."); 1665 } 1666 1667 const tenantId = this.userDoc.tenantID || this.userDoc.tenant_id; 1668 if (!tenantId) throw new Error("Keine Tenant-ID gefunden."); 1669 1670 /* phase8c */ await AutoCallRulesApi.update(ruleId, { active: newActiveState }); 1671 1672 UI.showSuccess(`Regel ${newActiveState ? 'aktiviert' : 'deaktiviert'}.`); 1673 1674 // Update lokalen Cache und UI direkt, anstatt alles neu zu laden 1675 const ruleIndex = this.autoCallRules.findIndex(rule => rule.id === ruleId); 1676 if (ruleIndex > -1) { 1677 this.autoCallRules[ruleIndex].active = newActiveState; 1678 } 1679 if (label) label.textContent = newActiveState ? 'Aktiv' : 'Inaktiv'; 1680 1681 1682 // Alternativ: Neu laden und rendern (wie vorher) 1683 // await this.loadAutoCallRules(); 1684 1685 } catch (error) { 1686 console.error("Fehler beim Ãndern des Regelstatus:", error); 1687 UI.showError("Fehler beim Ãndern des Status."); 1688 // Status im UI zurücksetzen bei Fehler 1689 if (checkbox) checkbox.checked = !newActiveState; // Alten Zustand wiederherstellen 1690 if(label) label.textContent = !newActiveState ? 'Aktiv' : 'Inaktiv'; // Alten Text wiederherstellen 1691 } finally { 1692 if (checkbox) checkbox.disabled = false; // Checkbox wieder aktivieren 1693 } 1694 } 1695 1696 // NEU: Handler zum Löschen einer Regel 1697 async deleteAutoCallRule(ruleId) { 1698 // Verwende ein benutzerdefiniertes Bestätigungsmodal oder die Standard-Confirm-Box 1699 // Beispiel mit standard confirm: 1700 if (!confirm(t('Möchten Sie diese AutoCall Regel wirklich löschen?'))) { 1701 return; 1702 } 1703 1704 // Optional: Zeige einen Ladezustand oder deaktiviere den Button 1705 const deleteButton = document.querySelector(`.delete-rule[data-rule-id="${ruleId}"]`); 1706 if (deleteButton) deleteButton.disabled = true; 1707 1708 try { 1709 const user = auth.currentUser; 1710 if (!user) throw new Error("Benutzer nicht angemeldet."); 1711 1712 // Stelle sicher, dass userDoc geladen ist 1713 if (!this.userDoc) { 1714 console.warn("UserDoc nicht gefunden in deleteAutoCallRule, versuche neu zu laden."); 1715 const userDocSnap = await _phase8UserSnap(user.uid); /* phase8 (was userDocRef + getDoc) */ 1716 if (userDocSnap.exists()) this.userDoc = userDocSnap.data(); 1717 else throw new Error("Benutzerprofil nicht gefunden."); 1718 } 1719 1720 const tenantId = this.userDoc.tenantID || this.userDoc.tenant_id; 1721 if (!tenantId) throw new Error("Keine Tenant-ID gefunden."); 1722 1723/* phase8c */ await AutoCallRulesApi.delete(ruleId); 1724 1725 UI.showSuccess("AutoCall Regel gelöscht."); 1726 1727 // Update lokalen Cache und UI direkt 1728 this.autoCallRules = this.autoCallRules.filter(rule => rule.id !== ruleId); 1729 this.renderAutoCallRulesTable(this.autoCallRules); // Tabelle neu rendern 1730 1731 // Alternativ: Neu laden und rendern (wie vorher) 1732 // await this.loadAutoCallRules(); 1733 1734 } catch (error) { 1735 console.error("Fehler beim Löschen der AutoCall Regel:", error); 1736 UI.showError("Fehler beim Löschen der Regel."); 1737 // Optional: Button wieder aktivieren bei Fehler 1738 if (deleteButton) deleteButton.disabled = false;
1739 } 1740 } 1741 1742 async initializeSmartLists() { 1743 // Smart List Modal initialisieren 1744 const modalElement = document.getElementById('smartListModal'); 1745 if (modalElement && !this.smartListModal) { 1746 this.smartListModal = new bootstrap.Modal(modalElement); 1747 1748 if (!this.smartListEventsBound) { 1749 // Event Listener für das Formular 1750 const form = document.getElementById('smartListForm'); 1751 if (form) { 1752 // Stelle sicher, dass die Ort-Option im Filter-Feld vorhanden ist 1753 const filterFieldSelects = form.querySelectorAll('.filter-field'); 1754 filterFieldSelects.forEach(select => { 1755 if (!select.querySelector('option[value="city"]')) { 1756 const cityOption = document.createElement('option'); 1757 cityOption.value = 'city'; 1758 cityOption.textContent = 'Ort'; 1759 select.appendChild(cityOption); 1760 } 1761 }); 1762 1763 // Entferne vorherige Event-Listener 1764 const newForm = form.cloneNode(true); 1765 form.parentNode.replaceChild(newForm, form); 1766 newForm.addEventListener('submit', (e) => this.handleSmartListSubmit(e)); 1767 1768 // Initialisiere die erste Filter-Zeile 1769 const firstRow = newForm.querySelector('.filter-row'); 1770 if (firstRow) { 1771 this.initializeFilterRow(firstRow); 1772 } 1773 } 1774 1775 // Event Listener für Filter hinzufügen/entfernen 1776 const addFilterBtn = document.getElementById('addFilterBtn'); 1777 if (addFilterBtn) { 1778 // Entferne vorherige Event-Listener 1779 const newBtn = addFilterBtn.cloneNode(true); 1780 addFilterBtn.parentNode.replaceChild(newBtn, addFilterBtn); 1781 newBtn.addEventListener('click', () => this.addFilterRow()); 1782 } 1783 1784 const filterContainer = document.getElementById('filterContainer'); 1785 if (filterContainer) { 1786 // Entferne vorherige Event-Listener 1787 const newContainer = filterContainer.cloneNode(true); 1788 filterContainer.parentNode.replaceChild(newContainer, filterContainer); 1789 newContainer.addEventListener('click', (e) => { 1790 if (e.target.closest('.remove-filter')) { 1791 e.target.closest('.filter-row').remove(); 1792 } 1793 }); 1794 } 1795 1796 // Event Listener für Modal-Events 1797 modalElement.addEventListener('hidden.bs.modal', () => { 1798 // Entferne die Backdrop manuell 1799 const backdrop = document.querySelector('.modal-backdrop'); 1800 if (backdrop) { 1801 backdrop.remove(); 1802 } 1803 // Entferne die Modal-Klassen vom Body 1804 document.body.classList.remove('modal-open'); 1805 document.body.style.removeProperty('padding-right'); 1806 }); 1807 1808 this.smartListEventsBound = true; 1809 } 1810 } 1811 1812 // Lade gespeicherte Smart Lists 1813 await this.loadSmartLists(false); // Hier UI-Update unterdrücken 1814 } 1815 1816 async loadSmartLists(updateUI = true) { // Parameter hinzugefügt 1817 try { 1818 const user = auth.currentUser; 1819 if (!user) return; 1820 1821/* phase8c */ this.smartLists = await SmartListsApi.list(true); 1822 // console.log("[loadSmartLists] Smart lists loaded:", this.smartLists); // DEBUG LOG 1823 1824 if (updateUI) { // UI nur aktualisieren, wenn explizit gewünscht 1825 this.updateSmartListsUI(); 1826 } 1827 } catch (error) { 1828 console.error('Error loading smart lists:', error); 1829 UI.showError(t('Fehler beim Laden der Smart-Listen')); 1830 } 1831 } 1832 1833 updateSmartListsUI() {
1834 // console.log("[updateSmartListsUI] Updating UI for smart lists."); // DEBUG LOG 1835 const container = document.getElementById('smart-lists-placeholder'); // Ziel ist jetzt der Platzhalter 1836 if (!container) { 1837 console.error("[updateSmartListsUI] Placeholder container not found!"); // DEBUG LOG 1838 return; 1839 } 1840 1841 const smartListsDropdown = document.createElement('div'); 1842 smartListsDropdown.className = 'smart-lists-dropdown dropdown mt-3'; // Füge Bootstrap Dropdown Klassen hinzu 1843 1844 // Dropdown-Button 1845 const dropdownButton = document.createElement('button'); 1846 dropdownButton.className = 'btn btn-outline-primary btn-sm dropdown-toggle'; 1847 dropdownButton.type = 'button'; 1848 dropdownButton.id = 'smartListsDropdownMenuButton'; 1849 dropdownButton.setAttribute('data-bs-toggle', 'dropdown'); 1850 dropdownButton.setAttribute('aria-expanded', 'false'); 1851 dropdownButton.textContent = t('Smart-Listen') + ' '; 1852 1853 // Dropdown-Menü 1854 const dropdownMenu = document.createElement('ul'); 1855 dropdownMenu.className = 'dropdown-menu'; 1856 dropdownMenu.setAttribute('aria-labelledby', 'smartListsDropdownMenuButton'); 1857 1858 // Erstelle Dropdown-Items für jede Liste 1859 if (this.smartLists.length > 0) { 1860 this.smartLists.forEach(list => { 1861 const listItem = document.createElement('li'); 1862 listItem.className = 'd-flex justify-content-between align-items-center dropdown-item-container'; // Eigene Klasse für Styling 1863 1864 const listLink = document.createElement('a'); 1865 listLink.className = 'dropdown-item smart-list-link'; 1866 listLink.href = '#'; 1867 listLink.dataset.listId = list.id; 1868 listLink.innerHTML = ` 1869 ${list.name} 1870 `; 1871 1872 const deleteButton = document.createElement('button'); 1873 deleteButton.className = 'btn btn-outline-danger btn-sm delete-smart-list ms-2'; // Kleinere GröÃe und Abstand 1874 deleteButton.dataset.listId = list.id; 1875 deleteButton.innerHTML = '<i class="fas fa-trash"></i>'; 1876 deleteButton.style.padding = '0.1rem 0.3rem'; // Padding anpassen für kleines Icon 1877 deleteButton.style.lineHeight = '1'; // Zeilenhöhe anpassen 1878 1879 listItem.appendChild(listLink); 1880 listItem.appendChild(deleteButton); 1881 dropdownMenu.appendChild(listItem); 1882 }); 1883 } else { 1884 dropdownMenu.innerHTML = `<li><span class="dropdown-item disabled">${t('Keine Smart-Listen vorhanden')}</span></li>`; 1885 } 1886 1887 smartListsDropdown.appendChild(dropdownButton); 1888 smartListsDropdown.appendChild(dropdownMenu); 1889 1890 // Ersetze existierende Smart-Listen oder füge neue hinzu 1891 const existingSmartLists = container.querySelector('.smart-lists-dropdown, .smart-lists'); // Auch alte Klasse suchen 1892 if (existingSmartLists) { 1893 existingSmartLists.replaceWith(smartListsDropdown); 1894 } else { 1895 container.appendChild(smartListsDropdown); 1896 } 1897 1898 // === NEU: Event Delegation über document.body === 1899 // console.log("[updateSmartListsUI] Attaching delegated listener to document.body for smart lists."); 1900 1901 // Entferne alten Listener, falls vorhanden 1902 if (this.smartListBodyClickListener) { 1903 // console.log("[updateSmartListsUI] Removing existing body listener."); 1904 document.body.removeEventListener('click', this.smartListBodyClickListener); 1905 this.smartListBodyClickListener = null; 1906 } 1907 1908 // Definiere die Listener-Funktion und speichere sie 1909 this.smartListBodyClickListener = (e) => { 1910 const placeholder = document.getElementById('smart-lists-placeholder'); 1911 1912 // Nur fortfahren, wenn der Klick im Placeholder stattfindet 1913 if (!placeholder || !placeholder.contains(e.target)) { 1914 return; 1915 } 1916 1917 // console.log("[updateSmartListsUI] Delegated BODY click detected inside placeholder."); 1918 1919 const link = e.target.closest('.smart-list-link'); 1920 const deleteBtn = e.target.closest('.delete-smart-list'); 1921 1922 if (link) { 1923 e.preventDefault(); 1924 const listId = link.dataset.listId; 1925 // console.log(`[updateSmartListsUI] Delegated BODY click matched smart list link: ${listId}`); 1926 // Rufe applySmartList erst nach kurzer Verzögerung auf, um sicherzustellen, dass andere UI-Updates abgeschlossen sind 1927 setTimeout(() => this.applySmartList(listId), 0); 1928 } else if (deleteBtn) { 1929 e.stopPropagation(); // Verhindert SchlieÃen des Dropdowns 1930 const listId = deleteBtn.dataset.listId; 1931 // console.log(`[updateSmartListsUI] Delegated BODY click matched delete button: ${listId}`); 1932 // Rufe deleteSmartList erst nach kurzer Verzögerung auf 1933 setTimeout(() => this.deleteSmartList(e, listId), 0); 1934 } else { 1935 // console.log("[updateSmartListsUI] Delegated BODY click did not match link or delete button within placeholder."); 1936 } 1937 }; 1938 1939 // Hänge den neuen Listener an body an 1940 document.body.addEventListener('click', this.smartListBodyClickListener); 1941 // console.log("[updateSmartListsUI] Attached new body listener."); 1942 // === Event Delegation ENDE === 1943 1944 /* === ALT: Listener am Dropdown-Container (auskommentiert) === 1945 // const newDropdown = smartListsDropdown.cloneNode(true); 1946 // smartListsDropdown.parentNode.replaceChild(newDropdown, smartListsDropdown); 1947 // console.log("[updateSmartListsUI] Adding delegated click listener to dropdown container."); 1948 // newDropdown.addEventListener('click', (e) => { ... }); 1949 */ 1950 1951 /* === ALT: Individuelle Listener (auskommentiert/entfernt) === ... */ 1952 1953 // Style für den Container der Dropdown-Items 1954 const style = document.createElement('style'); 1955 style.textContent = ` 1956 /* Stelle sicher, dass geöffnete Dropdown-Menüs einen hohen z-index haben */ 1957 .dropdown-menu.show { 1958 z-index: 1060 !important; /* Erzwinge hohen z-index */ 1959 } 1960 .dropdown-item-container { 1961 padding: 0.25rem 1rem; /* Standard padding für Dropdown-Items */ 1962 } 1963 .dropdown-item-container .smart-list-link { 1964 flex-grow: 1; /* Link nimmt verfügbaren Platz ein */ 1965 white-space: normal; /* Erlaube Umbruch für längere Namen */ 1966 padding: 0; /* Kein extra Padding für den Link selbst */ 1967 background-color: transparent !important; /* Kein Hover-Effekt vom Dropdown-Item */ 1968 color: inherit !important; /* Standardtextfarbe erben */ 1969 } 1970 .dropdown-item-container .smart-list-link:hover, 1971 .dropdown-item-container .smart-list-link:focus { 1972 text-decoration: underline; /* Unterstreichen bei Hover/Fokus */ 1973 } 1974 1975 .dropdown-item-container .delete-smart-list { 1976 flex-shrink: 0; /* Button schrumpft nicht */ 1977 } 1978 `; 1979 // Append style only once to the head 1980 if (!document.getElementById('smartlist-dropdown-style')) { 1981 style.id = 'smartlist-dropdown-style'; 1982 document.head.appendChild(style); 1983 } 1984 } 1985 1986 async deleteSmartList(e, listId) { 1987 e.stopPropagation(); // Verhindert das Triggern des Filter-Events 1988 1989 try { 1990 const user = auth.currentUser; 1991 if (!user) return; 1992 1993 // Bestätigungsdialog 1994 if (!confirm(t('Möchten Sie diese Smart-Liste wirklich löschen?'))) { 1995 return; 1996 } 1997 1998 // Lösche die Smart-Liste aus Firestore 1999/* phase8c */ await SmartListsApi.delete(listId); 2000 2001 // Aktualisiere die UI 2002 await this.loadSmartLists(); 2003 2004 UI.showSuccess('Smart-Liste wurde gelöscht'); 2005 } catch (error) { 2006 console.error('Error deleting smart list:', error); 2007 UI.showError('Fehler beim Löschen der Smart-Liste'); 2008 } 2009 } 2010
2011 async loadAllContactsInBackground() { 2012 // Lade alle restlichen Kontakte im Hintergrund für vollständige Suche 2013 if (this.backgroundLoadingComplete || !this.lastVisible) { 2014 return; 2015 } 2016 2017 // console.log('Starte Hintergrund-Laden aller Kontakte...'); 2018 2019 try { 2020 const user = auth.currentUser; 2021 if (!user) return; 2022 2023 const userDoc = await _phase8UserSnap(user.uid); /* phase8 */ 2024 const tenantId = userDoc.data()?.tenantID || userDoc.data()?.tenant_id; 2025 2026 let currentLastVisible = this.lastVisible; 2027 let hasMoreToLoad = true; 2028 let totalLoaded = this.contacts.length; 2029 2030 while (hasMoreToLoad) { 2031 /* phase8c bg-pagination noop */ const snapshot = { empty: true, forEach: () => {}, docs: [], size: 0 }; 2032 2033 if (snapshot.empty) { 2034 hasMoreToLoad = false; 2035 this.backgroundLoadingComplete = true; 2036 this.allContactsLoaded = true; 2037 // console.log(`Hintergrund-Laden abgeschlossen. Total: ${this.contacts.length} Kontakte`); 2038 break; 2039 } 2040 2041 const newContacts = []; 2042 snapshot.forEach(doc => { 2043 const contact = { id: doc.id, ...doc.data() }; 2044 if (!this.contacts.find(c =>
2044 c.id === contact.id)) { 2045 newContacts.push(contact); 2046 } 2047 }); 2048 2049 // Füge neue Kontakte zur Liste hinzu (ohne zu rendern) 2050 this.contacts = [...this.contacts, ...newContacts]; 2051 totalLoaded += newContacts.length; 2052 2053 currentLastVisible = snapshot.docs[snapshot.docs.length - 1]; 2054 hasMoreToLoad = snapshot.docs.length === 500; 2055 2056 // Cache aktualisieren 2057 const cacheKey = `contacts_${user.uid}_${tenantId}`; 2058 try { 2059 localStorage.setItem(cacheKey, JSON.stringify({ 2060 timestamp: Date.now(), 2061 data: this.contacts 2062 })); 2063 } catch (e) { 2064 console.warn('Cache-Speichern fehlgeschlagen:', e); 2065 } 2066 2067 // console.log(`Hintergrund: ${totalLoaded} Kontakte geladen...`); 2068 2069 // Kleine Pause zwischen Batches 2070 await new Promise(resolve => setTimeout(resolve, 100)); 2071 } 2072 2073 } catch (error) { 2074 console.error('Fehler beim Hintergrund-Laden:', error); 2075 } 2076 } 2077 2078 async loadMoreContacts() { 2079 if (this.isLoadingMore || !this.hasMore || !this.lastVisible) { 2080 // console.log('LoadMore abgebrochen: isLoading=', this.isLoadingMore, 'hasMore=', this.hasMore); 2081 return; 2082 } 2083 2084 this.isLoadingMore = true; 2085 // console.log('Lade weitere Kontakte...'); 2086 2087 // Zeige Loading Indicator 2088 const loadIndicator = document.getElementById('loadMoreIndicator'); 2089 if (loadIndicator) { 2090 loadIndicator.style.display = 'block'; 2091 } 2092 2093 // Für Mobile: Zeige auch einen Hinweis in der Pagination 2094 const isMobile = window.innerWidth <= 768; 2095 if (isMobile) { 2096 const paginationInfo = document.querySelector('.mobile-pagination-info'); 2097 if (paginationInfo) { 2098 paginationInfo.textContent = t('Lädt...'); 2099 } 2100 } 2101 2102 try { 2103 const user = auth.currentUser; 2104 if (!user) return; 2105 2106 const userDoc = await _phase8UserSnap(user.uid); /* phase8 */ 2107 const tenantId = userDoc.data()?.tenantID || userDoc.data()?.tenant_id; 2108 2109 /* phase8c loadMore noop */ const snapshot = { empty: true, forEach: () => {}, docs: [], size: 0 }; 2110 2111 if (snapshot.empty) { 2112 this.hasMore = false; 2113 // console.log('Keine weiteren Kontakte verfügbar'); 2114 this.showEndOfListIndicator(); 2115 return; 2116 } 2117 2118 const newContacts = []; 2119 snapshot.forEach(doc => { 2120 newContacts.push({ id: doc.id, ...doc.data() }); 2121 }); 2122 2123 // Füge neue Kontakte zur Liste hinzu 2124 this.contacts = [...this.contacts, ...newContacts]; 2125 2126 // Update last visible 2127 this.lastVisible = snapshot.docs[snapshot.docs.length - 1]; 2128 this.hasMore = snapshot.docs.length === 50; // Prüfe ob es noch mehr gibt 2129 2130 // Rendere nur die neuen Kontakte (append) 2131 this.appendContactsToTable(newContacts); 2132 2133 // console.log(`${newContacts.length} weitere Kontakte geladen. Total: ${this.contacts.length}`); 2134 2135 } catch (error) { 2136 console.error('Fehler beim Laden weiterer Kontakte:', error); 2137 } finally { 2138 this.isLoadingMore = false; 2139 // Verstecke Loading Indicator 2140 const loadIndicator = document.getElementById('loadMoreIndicator'); 2141 if (loadIndicator) { 2142 loadIndicator.style.display = 'none'; 2143 } 2144 } 2145 } 2146 2147 appendContactsToTable(newContacts) { 2148 const tbody = document.getElementById('contactsTableBody'); 2149 if (!tbody) return; 2150 2151 // Verwende DocumentFragment für bessere Performance 2152 const fragment = document.createDocumentFragment(); 2153 2154 /* contacts-list-v1: EINE Quelle fuer die Zeilen-Darstellung. Vorher baute dieser 2155 Pfad eigenes HTML mit nur 6 von 9 Zellen - Nachladen erzeugte kaputte Zeilen. */ 2156 const wrap = document.createElement('tbody'); 2157 wrap.innerHTML = newContacts.map(contact => this.renderContactRow(contact)).join(''); 2158 while (wrap.firstElementChild) fragment.appendChild(wrap.firstElementChild); 2159 2160 tbody.appendChild(fragment); 2161 2162 // Re-bind Event-Listener für neue Zeilen 2163 this.bindNewRowEventListeners(); 2164 } 2165 2166 setupInfiniteScroll() { 2167 // Cleanup previous observer 2168 if (this.scrollObserver) { 2169 this.scrollObserver.disconnect(); 2170 } 2171 2172 const isMobile = window.innerWidth <= 768; 2173 2174 // Für Mobile nutze einen einfacheren Scroll-Ansatz 2175 if (isMobile) { 2176 // Entferne vorherige Scroll-Listener 2177 if (this.scrollHandler) { 2178 window.removeEventListener('scroll', this.scrollHandler); 2179 } 2180 2181 let isScrolling = false;
2182 this.scrollHandler = () => { 2183 if (isScrolling) return; 2184 2185 // Prüfe ob wir am Ende der Seite sind 2186 const scrollPosition = window.innerHeight + window.scrollY; 2187 const threshold = document.body.offsetHeight - 300; // 300px vor dem Ende 2188 2189 if (scrollPosition >= threshold && this.hasMore && !this.isLoadingMore) { 2190 isScrolling = true; 2191 // console.log('Mobile: Triggering load more contacts'); 2192 this.loadMoreContacts().then(() => { 2193 isScrolling = false; 2194 }); 2195 } 2196 }; 2197 2198 // Throttle scroll events für bessere Performance 2199 let scrollTimeout; 2200 const throttledScrollHandler = () => { 2201 if (!scrollTimeout) { 2202 scrollTimeout = setTimeout(() => { 2203 scrollTimeout = null; 2204 this.scrollHandler(); 2205 }, 150); // 150ms throttle 2206 } 2207 }; 2208 2209 window.addEventListener('scroll', throttledScrollHandler, { passive: true }); 2210 2211 } else { 2212 // Desktop: Verwende Intersection Observer 2213 const sentinel = document.createElement('div'); 2214 sentinel.id = 'scrollSentinel'; 2215 sentinel.style.height = '1px'; 2216 2217 const container = document.getElementById('contactsContainer'); 2218 if (container) { 2219 container.appendChild(sentinel); 2220 } 2221 2222 // Setup Intersection Observer 2223 const options = { 2224 root: null, 2225 rootMargin: '200px', 2226 threshold: 0.1 2227 }; 2228 2229 this.scrollObserver = new IntersectionObserver((entries) => { 2230 entries.forEach(entry => { 2231 if (entry.isIntersecting && this.hasMore && !this.isLoadingMore) { 2232 this.loadMoreContacts(); 2233 } 2234 }); 2235 }, options); 2236 2237 if (sentinel) { 2238 this.scrollObserver.observe(sentinel); 2239 } 2240 } 2241 } 2242 2243 showEndOfListIndicator() { 2244 const container = document.getElementById('contactsContainer'); 2245 if (container) { 2246 const indicator = document.createElement('div'); 2247 indicator.className = 'text-center py-3 text-muted'; 2248 indicator.innerHTML = t('Alle Kontakte geladen'); 2249 container.appendChild(indicator); 2250 } 2251 } 2252 2253 getLeadStatusColor(status) { 2254 const colors = { 2255 'Neu': 'primary', 2256 'Kontaktiert': 'info', 2257 'Qualifiziert': 'warning', 2258 'Angebot': 'secondary', 2259 'Gewonnen': 'success', 2260 'Verloren': 'danger' 2261 }; 2262 return colors[status] || 'secondary'; 2263 } 2264 2265 bindNewRowEventListeners() { 2266 // Binde Event-Listener nur für neue Zeilen 2267 const newRows = document.querySelectorAll('.contact-row:not([data-events-bound])'); 2268 newRows.forEach(row => { 2269 row.addEventListener('click', async (e) => { 2270 if (e.target.closest('.checkbox-column')) return; 2271 2272 e.preventDefault(); 2273 const contactId = row.dataset.id; 2274 if (contactId) { 2275 this.storeContactsState(); 2276 window.history.pushState({}, '', `?page=contact-detail&id=${contactId}`); 2277 await Pages.loadPage('contact-detail'); 2278 } 2279 }); 2280 row.setAttribute('data-events-bound', 'true'); 2281 }); 2282 } 2283 2284 async loadContacts(forceRefresh = false) { 2285 // DEBOUNCE FIX: Wenn bereits ein Request läuft, gib das existierende Promise zurück 2286 if (this.isLoadingContacts && this.pendingLoadPromise && !forceRefresh) { 2287 console.log('âï¸ loadContacts bereits in Arbeit, warte auf existierenden Request...'); 2288 return this.pendingLoadPromise; 2289 } 2290
2291 // Setze Loading-Flag 2292 this.isLoadingContacts = true; 2293 2294 // Erstelle ein Promise für diesen Request 2295 this.pendingLoadPromise = this._loadContactsInternal(forceRefresh); 2296 2297 try { 2298 const result = await this.pendingLoadPromise; 2299 return result; 2300 } finally { 2301 // Cleanup nach Abschluss 2302 this.isLoadingContacts = false; 2303 this.pendingLoadPromise = null; 2304 } 2305 } 2306 2307 async _loadContactsInternal(forceRefresh = false) { 2308 // showContactsLoadingIndicator(); // Ladeindikator anzeigen <-- Entfernt 2309 // console.log("Lade Kontakte..."); 2310 // this.showLoadingIndicator('contacts'); // <-- Entfernt 2311 2312 try { 2313 const user = auth.currentUser; 2314 if (!user) { 2315 console.error('No user logged in'); 2316 return; 2317 } 2318 2319 // console.log('Loading contacts for user:', user.uid); 2320 2321 // Get user's tenant ID from their profile 2322 const userDoc = await _phase8UserSnap(user.uid); /* phase8 */ 2323 if (!userDoc.exists()) { 2324 console.error('User profile not found'); 2325 return; 2326 } 2327 2328 const userData = userDoc.data(); 2329 const tenantId = userData.tenantID || userData.tenant_id; 2330 if (!tenantId) { 2331 UI.showError('No tenant ID found for user'); 2332 console.error('No tenant ID found for user'); 2333 return; 2334 } 2335 2336 // Prüfe Kontakt-Berechtigungen für Mitarbeiter 2337 const userRole = userData.role || 'user'; 2338 const contactPermissions = userData.contact_permissions || {}; 2339 const visibilityMode = contactPermissions.visibility_mode || 'all'; 2340 const onlyOwnContacts = (userRole === 'employee' && visibilityMode === 'own'); 2341 2342 if (onlyOwnContacts) { 2343 console.log('[Contacts] Mitarbeiter mit nur eigene Kontakte - filtere nach uid:', user.uid); 2344 } 2345 2346 // console.log('Using tenant ID:', tenantId); 2347 2348 // Define fetch function for caching - Optimiert für minimale Daten 2349 const fetchContacts = async () => { 2350 const contactsRef = null; /* phase8c stub for dead code below */ 2351 /* phase8c initial-load via API */ const _allC = await ContactsApi.list({ limit: 5000 }).catch(()=>[]); const snapshot = { forEach: (cb) => _allC.forEach(d => cb({ id: d.id, data: () => d })), docs: _allC.map(d => ({ id: d.id, data: () => d })), size: _allC.length, empty: _allC.length === 0 }; 2352 const contacts = []; 2353 2354 // Speichere nur minimale Daten für die Liste 2355 snapshot.forEach(doc => { 2356 const fullData = doc.data(); 2357 2358 // Speichere vollständige Daten in separatem Cache für späteren Zugriff 2359 this.fullContactsCache.set(doc.id, { id: doc.id, ...fullData }); 2360 2361 // Für die Liste nur benötigte Felder 2362 contacts.push({ 2363 id: doc.id, 2364 name: fullData.name || '', 2365 email: fullData.email || '', 2366 phone_number: fullData.phone_number || '', 2367 company: fullData.company || '', 2368 updated_at: fullData.updated_at, 2369 lead_status: fullData.lead_status || '', 2370 task: fullData.task || '', 2371 tenantID: fullData.tenantID || '', 2372 // Für Filter benötigt 2373 industry: fullData.industry || '', 2374 team: fullData.team || [], team_uids: fullData.team_uids || [], liste: fullData.liste || '', // kontakt-team-v1 2375 created_at: fullData.created_at 2376 }); 2377 }); 2378 2379 // Speichere last visible document für späteres Laden 2380 if (snapshot.docs.length > 0) { 2381 this.lastVisible = snapshot.docs[snapshot.docs.length - 1]; 2382 this.hasMore = snapshot.docs.length === 100; 2383 } else { 2384 this.hasMore = false;
2385 } 2386 2387 this.allContactsLoaded = !this.hasMore; 2388 this.backgroundLoadingComplete = !this.hasMore; 2389 2390 // console.log(`Mobile: ${contacts.length} Kontakte initial geladen`); 2391 return contacts; 2392 2393 if (false) { 2394 // DESKTOP: dead code (phase8c â ContactsApi.list now returns full set) 2395 let q; 2396 if (onlyOwnContacts) { 2397 // Mitarbeiter sieht nur eigene Kontakte 2398 q = query(contactsRef, 2399 where('tenantID', '==', tenantId.toString()), 2400 where('uid', '==', user.uid), 2401 orderBy('updated_at', 'desc') 2402 ); 2403 } else { 2404 q = query(contactsRef, 2405 where('tenantID', '==', tenantId.toString()), 2406 orderBy('updated_at', 'desc') 2407 ); 2408 } 2409 const snapshot = await getDocs(q); 2410 const contacts = []; 2411 2412 // Speichere nur minimale Daten für die Liste 2413 snapshot.forEach(doc => { 2414 const fullData = doc.data(); 2415 2416 // Speichere vollständige Daten in separatem Cache für späteren Zugriff 2417 this.fullContactsCache.set(doc.id, { id: doc.id, ...fullData }); 2418 2419 // Für die Liste nur benötigte Felder 2420 contacts.push({ 2421 id: doc.id, 2422 name: fullData.name || '', 2423 email: fullData.email || '', 2424 phone_number: fullData.phone_number || '', 2425 company: fullData.company || '', 2426 updated_at: fullData.updated_at, 2427 lead_status: fullData.lead_status || '', 2428 task: fullData.task || '', 2429 tenantID: fullData.tenantID || '', 2430 // Für Filter benötigt 2431 industry: fullData.industry || '', 2432 team: fullData.team || [], team_uids: fullData.team_uids || [], liste: fullData.liste || '', // kontakt-team-v1 2433 created_at: fullData.created_at 2434 }); 2435 }); 2436 2437 this.allContactsLoaded = true; 2438 this.backgroundLoadingComplete = true; 2439 this.hasMore = false; 2440 2441 // console.log(`Desktop: ${contacts.length} Kontakte vollständig geladen`); 2442 return contacts; 2443 } 2444 }; 2445 2446 // Get contacts with caching - Unterschiedlicher Cache für Mobile/Desktop 2447 const isMobile = window.innerWidth <= 768; 2448 const cacheKey = `contacts_${user.uid}_${tenantId}_${isMobile ? 'mobile' : 'desktop'}`; 2449 2450 // Bei forceRefresh Cache komplett umgehen 2451 if (forceRefresh) { 2452 console.log('[loadContacts] forceRefresh aktiviert - lösche alle Caches'); 2453 // Lösche alle Cache-Keys 2454 localStorage.removeItem(cacheKey); 2455 localStorage.removeItem(`contacts_${user.uid}_${tenantId}_desktop`); 2456 localStorage.removeItem(`contacts_${user.uid}_${tenantId}_mobile`); 2457 localStorage.removeItem(`contacts_${user.uid}_${tenantId}`); 2458 // Lösche auch den Memory-Cache 2459 if (this.fullContactsCache) { 2460 this.fullContactsCache.clear(); 2461 } 2462 console.log('[loadContacts] Lade Daten direkt von Firebase...'); 2463 // Lade Daten direkt ohne Cache 2464 this.contacts = await fetchContacts(); 2465 console.log(`[loadContacts] ${this.contacts.length} Kontakte von Firebase geladen`); 2466 2467 // Debug: Zeige Lead-Status der ersten 5 Kontakte 2468 if (this.contacts.length > 0) { 2469 console.log('[loadContacts] Lead-Status Beispiele:');
2470 this.contacts.slice(0, 5).forEach(contact => { 2471 console.log(` - ${contact.name || contact.email}: ${contact.lead_status || 'KEIN STATUS'}`); 2472 }); 2473 } 2474 2475 // Speichere die neuen Daten im Cache 2476 try { 2477 localStorage.setItem(cacheKey, JSON.stringify({ 2478 timestamp: Date.now(), 2479 data: this.contacts 2480 })); 2481 } catch (e) { 2482 console.warn('Fehler beim Speichern im Cache:', e); 2483 } 2484 } else { 2485 // Cache für eine Minute invalidieren um sicherzustellen dass alle Kontakte neu geladen werden 2486 const cached = localStorage.getItem(cacheKey); 2487 if (cached) { 2488 try { 2489 const parsed = JSON.parse(cached); 2490 if (parsed.data && parsed.data.length < 50) { 2491 // console.log('Alter Cache mit nur', parsed.data.length, 'Kontakten gefunden - lösche Cache...'); 2492 localStorage.removeItem(cacheKey); 2493 } 2494 } catch (e) { 2495 console.error('Fehler beim Prüfen des Cache:', e); 2496 } 2497 } 2498 2499 this.contacts = await getCachedData(cacheKey, fetchContacts, 15 * 60 * 1000); // 15 Minuten Cache 2500 } 2501 2502 // console.log('Loaded contacts count:', this.contacts.length); 2503 if (this.contacts.length > 0) { 2504 // console.log('Sample contact fields:', Object.keys(this.contacts[0])); 2505 // console.log('Sample contact lead status:', this.contacts[0].lead_status); 2506 } 2507 2508 // Reset event binding state 2509 this.eventsBound = false; 2510 await this.renderContacts(this.contacts); 2511 // await this.bindEvents(); // Entfernt 2512 // this.setupContactEventListeners(); // Entfernt von hier 2513 // this.updateSmartListsUI(); // Wird jetzt in renderContactsImmediate aufgerufen 2514 2515 // Aktiviere Infinite Scrolling nach dem ersten Render 2516 // setTimeout(() => { 2517 // this.setupInfiniteScroll(); 2518 // }, 100); 2519 // Infinite Scrolling deaktiviert - alle Kontakte sind bereits geladen 2520 2521 return this.contacts; 2522 } catch (error) { 2523 console.error('Error loading contacts:', error); 2524 UI.showError('Error loading contacts. Please try again.'); 2525 // hideContactsLoadingIndicator(); // Ladeindikator deaktiviert 2526 } finally { 2527 // hideContactsLoadingIndicator(); // <-- Entfernt 2528 // this.hideLoadingIndicator('contacts'); // <-- Entfernt 2529 // console.log("Kontakte Ladevorgang abgeschlossen."); 2530 } 2531 } 2532 2533 /** 2534 * Lädt vollständige Kontaktdaten bei Bedarf (z.B. für Detailansicht) 2535 * Nutzt Cache falls vorhanden, sonst lädt von Firestore 2536 */ 2537 async getFullContactData(contactId) { 2538 try { 2539 // Prüfe zuerst den Cache 2540 if (this.fullContactsCache.has(contactId)) { 2541 // console.log(`Vollständige Daten für Kontakt ${contactId} aus Cache geladen`); 2542 return this.fullContactsCache.get(contactId); 2543 } 2544 2545 // Falls nicht im Cache, lade von Firestore 2546 // console.log(`Lade vollständige Daten für Kontakt ${contactId} von Firestore`); 2547 /* phase8: contacts/${contactId} via API */ 2548 const __ctRes = await fetch(`/api/contacts/${contactId}`, { headers: { 'Authorization': 'Bearer ' + (await Auth.getIdToken()) } }); 2549 const __ctData = __ctRes.ok ? await __ctRes.json() : null; 2550 const contactDoc = { exists: () => !!__ctData, data: () => __ctData, id: __ctData && __ctData.id }; 2551 2552 if (contactDoc.exists()) { 2553 const fullData = { id: contactDoc.id, ...contactDoc.data() }; 2554 // Speichere im Cache für zukünftige Verwendung 2555 this.fullContactsCache.set(contactId, fullData); 2556 return fullData; 2557 } else { 2558 console.error(`Kontakt ${contactId} nicht gefunden`); 2559 return null; 2560 } 2561 } catch (error) { 2562 console.error(`Fehler beim Laden vollständiger Daten für Kontakt ${contactId}:`, error); 2563 return null; 2564 } 2565 } 2566 2567 setupContactEventListeners() { 2568 // Remove any existing event listeners first 2569 const buttons = [ 2570 'newContactBtn', 2571 'importContactsBtn', 2572 'createSmartListBtn', 2573 'sendGroupEmailBtn', 2574 'newMultiCallBtn', 2575 'sofortAnrufBtn', 2576 'autoCallBtn' 2577 ]; 2578 2579 // Mobile Load More Button 2580 const mobileLoadMoreBtn = document.getElementById('mobileLoadMoreBtn'); 2581 if (mobileLoadMoreBtn) { 2582 mobileLoadMoreBtn.addEventListener('click', async () => { 2583 mobileLoadMoreBtn.disabled = true; 2584 mobileLoadMoreBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>' + t('Lädt...'); 2585 2586 await this.loadMoreContacts(); 2587 2588 // Neu rendern nach dem Laden 2589 await this.renderContacts(this.contacts); 2590 this.setupContactEventListeners(); // Event-Listener neu binden 2591 }); 2592 } 2593 2594 // Setup Infinite Scroll für Mobile 2595 const isMobile = window.innerWidth <= 768; 2596 if (isMobile && this.hasMore) { 2597 this.setupInfiniteScroll(); 2598 } 2599 2600 buttons.forEach(btnId => { 2601 const btn = document.getElementById(btnId); 2602 if (btn) { 2603 // Clone and replace the button to remove old event listeners 2604 const newBtn = btn.cloneNode(true); 2605 btn.parentNode.replaceChild(newBtn, btn); 2606 } 2607 }); 2608 2609 // Now add the new event listeners 2610 const newContactBtn = document.getElementById('newContactBtn'); 2611 if (newContactBtn) { 2612 newContactBtn.addEventListener('click', () => this.showNewContactModal()); 2613 } 2614 2615 const importBtn = document.getElementById('importContactsBtn'); 2616 if (importBtn) { 2617 importBtn.addEventListener('click', (e) => { 2618 e.stopPropagation(); 2619 this.contactImportUI.show(); 2620 }); 2621 } 2622 2623 const createSmartListBtn = document.getElementById('createSmartListBtn'); 2624 if (createSmartListBtn) { 2625 createSmartListBtn.addEventListener('click', () => { 2626 // Ãbersetze das Modal vor dem Anzeigen 2627 this.translateSmartListModal(); 2628 if (this.smartListModal) { 2629 this.smartListModal.show(); 2630 } 2631 }); 2632 } 2633 2634 const sendGroupEmailBtn = document.getElementById('sendGroupEmailBtn'); 2635 if (sendGroupEmailBtn) { 2636 sendGroupEmailBtn.addEventListener('click', () => this.handleSendGroupEmail()); 2637 } 2638 2639 const newMultiCallBtn = document.getElementById('newMultiCallBtn'); 2640 if (newMultiCallBtn) { 2641 // Clone button to remove old event listeners 2642 const newBtn = newMultiCallBtn.cloneNode(true); 2643 newMultiCallBtn.parentNode.replaceChild(newBtn, newMultiCallBtn); 2644 newBtn.addEventListener('click', () => { 2645 // console.log('MultiCall Button clicked!'); // Debug log 2646 this.handleSipOutboundCall(); 2647 }); 2648 } 2649 2650 const sofortAnrufBtn = document.getElementById('sofortAnrufBtn'); 2651 if (sofortAnrufBtn) { 2652 sofortAnrufBtn.addEventListener('click', () => this.handleSofortAnruf()); 2653 } 2654 2655 const autoCallBtn = document.getElementById('autoCallBtn'); 2656 if (autoCallBtn) { 2657 autoCallBtn.addEventListener('click', () => this.handleAutoCall()); 2658 } 2659 2660 // === HINZUGEFÃGT: Listener für Bulk Action Buttons === 2661 const bulkUpdateBtn = document.getElementById('bulkUpdateBtn'); 2662 if (bulkUpdateBtn) { 2663 // console.log("[setupContactEventListeners] Adding listener to #bulkUpdateBtn."); 2664 const newBulkUpdateBtn = bulkUpdateBtn.cloneNode(true); 2665 bulkUpdateBtn.parentNode.replaceChild(newBulkUpdateBtn, bulkUpdateBtn); 2666 newBulkUpdateBtn.addEventListener('click', () => this.handleBulkUpdate()); 2667 } else { 2668 // Dieser Log sollte jetzt nicht mehr erscheinen, da der Button im gerenderten HTML ist 2669 console.warn("[setupContactEventListeners] #bulkUpdateBtn not found during setup."); 2670 } 2671 2672 const bulkDeleteBtn = document.getElementById('bulkDeleteBtn'); 2673 if (bulkDeleteBtn) { 2674 // console.log("[setupContactEventListeners] Adding listener to #bulkDeleteBtn."); 2675 const newBulkDeleteBtn = bulkDeleteBtn.cloneNode(true); 2676 bulkDeleteBtn.parentNode.replaceChild(newBulkDeleteBtn, bulkDeleteBtn); 2677 newBulkDeleteBtn.addEventListener('click', () => this.handleBulkDelete()); 2678 } else { 2679 console.warn("[setupContactEventListeners] #bulkDeleteBtn not found during setup."); 2680 } 2681 // === HINZUGEFÃGT ENDE === 2682 2683 // Event-Listener für Zeilen-Klicks (ZUERST klonen, damit Delete-Buttons danach Handler bekommen) 2684 document.querySelectorAll('.contact-link').forEach(link => { 2685 const newLink = link.cloneNode(true); 2686 link.parentNode.replaceChild(newLink, link); 2687 2688 // Füge den Event-Listener zum neuen Element hinzu 2689 newLink.addEventListener('click', async (e) => { 2690 // Wenn auf die Checkbox oder Delete-Button geklickt wurde, nicht zur Detail-Ansicht navigieren 2691 if (e.target.closest('.checkbox-column') || e.target.closest('.delete-contact') || (e.target.closest('.contact-noclick') && !e.target.closest('.contact-open-item'))) { 2692 return; 2693 } 2694 2695 e.preventDefault(); 2696 const contactId = e.currentTarget.dataset.id; 2697 if (contactId) { 2698 // Speichere den aktuellen Zustand vor der Navigation 2699 // console.log('[setupContactEventListeners] Speichere State vor Navigation zu Kontakt:', contactId); 2700 this.storeContactsState(); 2701 2702 // Update URL without page reload 2703 window.history.pushState({}, '', `?page=contact-detail&id=${contactId}`); 2704 // Load the contact detail page 2705 await Pages.loadPage('contact-detail'); 2706 } 2707 }); 2708 }); 2709 2710 // === Delete-Buttons in jeder Zeile (NACH dem Zeilen-Klonen, damit Handler erhalten bleiben) === 2711 document.querySelectorAll('.delete-contact').forEach(button => { 2712 button.addEventListener('click', async (e) => { 2713 e.stopPropagation(); 2714 e.preventDefault(); 2715 const contactId = button.dataset.id; 2716 if (confirm(t('Sind Sie sicher, dass Sie diesen Kontakt löschen möchten?'))) { 2717 await this.deleteContact(contactId); 2718 } 2719 }); 2720 }); 2721 2722 // Event-Listener für Checkboxen 2723 document.querySelectorAll('.contact-checkbox').forEach(checkbox => { 2724 const newCheckbox = checkbox.cloneNode(true); 2725 checkbox.parentNode.replaceChild(newCheckbox, checkbox); 2726 2727 // Checkbox-Event separat behandeln 2728 newCheckbox.addEventListener('change', (e) => { 2729 e.stopPropagation(); // Verhindert Bubble-up zum Row-Click 2730 this.updateBulkActionsVisibility(); 2731 }); 2732 }); 2733 2734 // Event-Listener für "Alle auswählen" Dropdown 2735 const selectAll = document.getElementById('selectAll'); 2736 if (selectAll) { 2737 // Verhindere das Standard-Checkbox-Verhalten beim Klick 2738 selectAll.addEventListener('click', (e) => { 2739 e.preventDefault(); 2740 }); 2741 } 2742 2743 // Event-Listener für "Alle auf dieser Seite" 2744 const selectAllPage = document.getElementById('selectAllPage'); 2745 if (selectAllPage) { 2746 selectAllPage.addEventListener('click', (e) => { 2747 e.preventDefault(); 2748 // Nur sichtbare Checkboxen auf der aktuellen Seite auswählen 2749 document.querySelectorAll('.contact-row .contact-checkbox').forEach(checkbox => { 2750 checkbox.checked = true; 2751 }); 2752 // SchlieÃe Dropdown 2753 const dropdown = bootstrap.Dropdown.getInstance(selectAll); 2754 if (dropdown) dropdown.hide(); 2755 this.updateBulkActionsVisibility(); 2756 }); 2757 } 2758 2759 // Event-Listener für "Alle gefilterten Kontakte" 2760 const selectAllFiltered = document.getElementById('selectAllFiltered'); 2761 if (selectAllFiltered) { 2762 selectAllFiltered.addEventListener('click', async (e) => { 2763 e.preventDefault(); 2764 2765 // Check if currentFilteredContacts exists and has contacts 2766 if (!this.currentFilteredContacts || this.currentFilteredContacts.length === 0) { 2767 UI.showInfo('Keine gefilterten Kontakte vorhanden'); 2768 return; 2769 } 2770 2771 // Bestätige bei groÃen Mengen 2772 const filteredCount = this.currentFilteredContacts.length; 2773 if (filteredCount > 100) { 2774 if (!confirm(`${t('Möchten Sie wirklich alle')} ${filteredCount}
2774 ${t('gefilterten Kontakte auswählen?')}`)) { 2775 return; 2776 } 2777 } 2778 2779 // Markiere alle gefilterten Kontakte als ausgewählt 2780 this.allFilteredContactsSelected = true; 2781 this.selectedFilteredContactIds = this.currentFilteredContacts.map(c => c.id); 2782 2783 // Wähle alle sichtbaren Checkboxen aus 2784 document.querySelectorAll('.contact-row .contact-checkbox').forEach(checkbox => { 2785 checkbox.checked = true; 2786 }); 2787 2788 // Zeige Hinweis 2789 UI.showInfo(`${filteredCount} gefilterte Kontakte ausgewählt`); 2790 2791 // SchlieÃe Dropdown 2792 const dropdown = bootstrap.Dropdown.getInstance(selectAll); 2793 if (dropdown) dropdown.hide(); 2794 2795 this.updateBulkActionsVisibility(); 2796 }); 2797 } 2798 2799 // Event-Listener für mobilen "Alle auswählen"-Button 2800 const selectAllMobileBtn = document.getElementById('selectAllMobile'); 2801 if (selectAllMobileBtn) { 2802 // Entferne alte Listener 2803 const newSelectAllMobileBtn = selectAllMobileBtn.cloneNode(true); 2804 selectAllMobileBtn.parentNode.replaceChild(newSelectAllMobileBtn, selectAllMobileBtn); 2805 2806 newSelectAllMobileBtn.addEventListener('click', (e) => { 2807 // Bestimme, ob alle bereits ausgewählt sind 2808 const allCheckboxes = document.querySelectorAll('.contact-checkbox'); 2809 const checkedCheckboxes = document.querySelectorAll('.contact-checkbox:checked'); 2810 const shouldSelectAll = checkedCheckboxes.length < allCheckboxes.length; 2811 2812 allCheckboxes.forEach(checkbox => { 2813 checkbox.checked = shouldSelectAll; 2814 }); 2815 2816 // Ãndere den Button-Text entsprechend 2817 if (shouldSelectAll) { 2818 newSelectAllMobileBtn.innerHTML = `<i class="fas fa-minus-square me-1"></i>${t('Alle abwählen')}`; 2819 } else { 2820 newSelectAllMobileBtn.innerHTML = `<i class="fas fa-check-square me-1"></i>${t('Alle auswählen')}`; 2821 } 2822 2823 this.updateBulkActionsVisibility(); 2824 }); 2825 } 2826 } 2827 2828 debounceSearch(event) { 2829 // Clear the previous timeout 2830 if (this.searchTimeout) { 2831 clearTimeout(this.searchTimeout); 2832 } 2833 2834 // Set a new timeout 2835 this.searchTimeout = setTimeout(() => { 2836 this.applyFilters(); 2837 }, 300); // Wait 300ms after the user stops typing 2838 } 2839 2840 async renderContacts(filteredContacts) { 2841 // Debounce rendering für bessere Performance 2842 if (this.renderDebounceTimer) { 2843 clearTimeout(this.renderDebounceTimer); 2844 } 2845 2846 this.renderDebounceTimer = setTimeout(async () => { 2847 await this.renderContactsImmediate(filteredContacts); 2848 }, 50); 2849 } 2850 2851 async renderContactsImmediate(filteredContacts) { 2852 // kontakt-team-v1: Filter/Knopf nachruesten; beim ersten Laden der 2853 // Teamdaten einmal neu zeichnen, damit die Zuweisungen in den Zeilen stehen. 2854 setTimeout(async () => { 2855 const vorher = !!this._kteamInfo; 2856 await this._kteamFilterEinbauen(); 2857 if (!vorher && this._kteamInfo && (filteredContacts || []).some(c => Array.isArray(c.team) && c.team.length)) { 2858 this.renderContacts(filteredContacts); 2859 } 2860 }, 0); 2861 console.log('[renderContactsImmediate] Start'); 2862 console.log('[renderContactsImmediate] Anzahl Kontakte zu rendern:', filteredContacts ? filteredContacts.length : 'keine'); 2863 2864 // Debug: Zeige Lead-Status der ersten 5 Kontakte 2865 if (filteredContacts && filteredContacts.length > 0) { 2866 console.log('[renderContactsImmediate] Lead-Status der ersten 5:');
2867 filteredContacts.slice(0, 5).forEach(contact => { 2868 console.log(` - ${contact.name || contact.email}: ${contact.lead_status || 'KEIN STATUS'}`); 2869 }); 2870 } 2871 2872 // Prüfe, ob der Container existiert 2873 const contactsContainer = document.getElementById('contactsContainer'); 2874 if (!contactsContainer) { 2875 console.error('[renderContactsImmediate] Container nicht gefunden!'); 2876 return; 2877 } 2878 2879 console.log('[renderContactsImmediate] Container gefunden'); 2880 2881 // Stelle sicher, dass Lead-Status geladen sind 2882 await this.getLeadStatuses(); 2883 2884 // Sonst normale Rendering-Logik... 2885 // ... vorhandener Code ... 2886 2887 // Verwende ein leeres Array wenn keine Kontakte vorhanden sind, 2888 // damit die Tabelle trotzdem gerendert wird 2889 const contactsToRender = this.currentFilteredContacts || filteredContacts || []; 2890 2891 // Calculate pagination 2892 const totalPages = Math.ceil(contactsToRender.length / this.itemsPerPage); 2893 const startIndex = (this.currentPage - 1) * this.itemsPerPage; 2894 const endIndex = startIndex + this.itemsPerPage; 2895 const contactsToShow = contactsToRender.slice(startIndex, endIndex); 2896 2897 let html = ` 2898 <div class="container-fluid"> 2899 <style id="contacts-list-v1-style"> 2900 .contact-avatar{display:inline-flex;align-items:center;justify-content:center; 2901 width:34px;height:34px;min-width:34px;border-radius:50%;color:#fff; 2902 font-size:.78rem;font-weight:600;letter-spacing:.02em;} 2903 .contact-identity{display:flex;align-items:center;gap:.6rem;min-width:0;} 2904 .contact-identity .contact-identity-text{min-width:0;} 2905 .contact-name-text,.contact-subline{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;} 2906 .contact-subline{font-size:.8rem;} 2907 .contact-task-chip{font-size:.7rem;font-weight:500;} 2908 /* contacts-menu-fix-v1: das Menue wird per JS auf position:fixed gesetzt; z-index muss ueber der Tabelle liegen. */ 2909 .contact-quick-actions .dropdown-menu{z-index:1080;} 2910 /* contacts-menu-fix-v1b: styles.css:3841 legt auf JEDES tbody tr eine 2911 Einblendung mit fill-mode "both" - dadurch bleibt dauerhaft ein 2912 transform (Einheitsmatrix) an der Zeile stehen. Ein transformierter 2913 Vorfahr wird zum Bezugsrahmen fuer position:fixed: das Menue war 2914 dadurch WIEDER an die Zeile gebunden und wurde erneut abgeschnitten. 2915 Nur fuer die Kontakttabelle abgeschaltet - nimmt nebenbei das 2916 Flackern bei jedem Neuaufbau der Liste (Suche/Blaettern) weg. */ 2917 .contacts-table-wrap tbody tr{animation:none !important;transform:none !important;} 2918 .contact-quick-actions{display:inline-flex;gap:.25rem;align-items:center;} 2919 .contact-quick-actions .btn{--bs-btn-padding-y:.15rem;--bs-btn-padding-x:.45rem;} 2920 </style> 2921 <!-- Platzhalter für Smart-Listen Dropdown --> 2922 <div id="smart-lists-placeholder" class="mb-3"></div> 2923 2924 <div class="d-flex justify-content-between align-items-center mb-4"> 2925 <div class="d-flex flex-column flex-md-row gap-2 contact-actions-container"> 2926 <button type="button" class="btn btn-primary" id="newContactBtn"> 2927 <i class="fas fa-plus me-1"></i>${t('Neu')} 2928 </button> 2929 <button type="button" class="btn btn-outline-secondary d-md-none" id="selectAllMobile"> 2930 <i class="fas fa-check-square me-1"></i>${t('Alle auswählen')} 2931 </button> 2932 <button type="button" class="btn btn-outline-secondary" id="importContactsBtn"> 2933 <i class="fas fa-file-import me-1"></i>${t('Kontakte importieren')} 2934 </button> 2935 <button type="button" class="btn btn-outline-secondary" id="createSmartListBtn"> 2936 <i class="fas fa-list me-1"></i>${t('Smart-Liste erstellen')} 2937 </button> 2938 <button type="button" class="btn btn-outline-secondary" id="sendGroupEmailBtn"> 2939 <i class="fas fa-envelope me-1"></i>${t('Gruppen-E-Mail')} 2940 </button> 2941 ${this.hasSipAccount ? ` 2942 <button type="button" 2943 class="btn btn-outline-secondary ${!this.hasMultiCallPermission() ? 'disabled opacity-50' : ''}" 2944 id="newMultiCallBtn" 2945 data-bs-toggle="tooltip" 2946 data-bs-placement="top" 2947 title="${!this.hasMultiCallPermission() ? t('MultiCall Abo erforderlich') : ''}"> 2948 <i class="fas fa-phone me-1"></i>${t('Sofort Anruf')} 2949 </button> 2950 ` : ` 2951 <button type="button" 2952 class="btn btn-outline-secondary ${!this.hasMultiCallPermission() ? 'disabled opacity-50' : ''}" 2953 id="sofortAnrufBtn" 2954 data-bs-toggle="tooltip" 2955 data-bs-placement="top" 2956 title="${!this.hasMultiCallPermission() ? t('MultiCall Abo erforderlich') : ''}"> 2957 <i class="fas fa-phone me-1"></i>${t('Sofort Anruf')} 2958 </button> 2959 `} 2960 <button type="button" 2961 class="btn btn-outline-secondary ${!this.hasPersonalAssistantPermission() ? 'disabled opacity-50' : ''}" 2962 id="autoCallBtn" 2963 data-bs-toggle="tooltip" 2964 data-bs-placement="top" 2965 title="${!this.hasPersonalAssistantPermission() ? t('Personal Assistant Abo erforderlich') : ''}"> 2966 <i class="fas fa-phone-volume me-1"></i>${t('AutoCall')} 2967 </button> 2968 </div> 2969 </div> 2970 2971 2972 <!-- MultiCall Template Modal --> 2973 <div class="modal fade" id="multiCallTemplateModal" tabindex="-1" aria-labelledby="multiCallTemplateModalLabel" aria-hidden="true"> 2974 <div class="modal-dialog"> 2975 <div class="modal-content"> 2976 <div class="modal-header"> 2977 <h5 class="modal-title" id="multiCallTemplateModalLabel">${t('MultiCall Vorlage auswählen')}</h5> 2978 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> 2979 </div> 2980 <div class="modal-body"> 2981 <div class="mb-3"> 2982 <label for="multiCallTemplateSelect" class="form-label">${t('Vorlage')}</label> 2983 <select class="form-select" id="multiCallTemplateSelect" required> 2984 </select> 2985 </div> 2986 <div class="mb-3"> 2987 <label for="multiCallCalendarSelect" class="form-label">${t('Kalender')}</label> 2988 <select class="form-select" id="multiCallCalendarSelect"> 2989 <option value="">${t('Kein Kalender')}</option> 2990 ${await this.getCalendarOptionsHtml()} 2991 </select> 2992 </div> 2993 <div id="selectedContactsCountMultiCall" class="text-muted small"></div> 2994 </div> 2995 <div class="modal-footer"> 2996 <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-i18n="Abbrechen">${t('Abbrechen')}</button> 2997 <button type="button" class="btn btn-primary" id="createMultiCallTasksBtn" data-i18n="MultiCall starten">${t('MultiCall starten')}</button> 2998 </div> 2999 </div> 3000 </div> 3001 </div> 3002 3003 <!-- Sofort Anruf Template Modal --> 3004 <div class="modal fade" id="sofortAnrufTemplateModal" tabindex="-1" aria-labelledby="sofortAnrufTemplateModalLabel" aria-hidden="true"> 3005 <div class="modal-dialog"> 3006 <div class="modal-content"> 3007 <div class="modal-header"> 3008 <h5 class="modal-title" id="sofortAnrufTemplateModalLabel">${t('Sofort Anruf - Vorlage auswählen')}</h5> 3009 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> 3010 </div> 3011 <div class="modal-body"> 3012 <div class="mb-3"> 3013 <label for="sofortAnrufTemplateSelect" class="form-label">${t('Vorlage')}</label> 3014 <select class="form-select" id="sofortAnrufTemplateSelect" required> 3015 </select> 3016 </div> 3017 <div class="mb-3"> 3018 <label for="sofortAnrufAgentSelect" class="form-label">${t('Agent')}</label> 3019 <select class="form-select" id="sofortAnrufAgentSelect"> 3020 <option value="">${t('Wird geladen...')}</option> 3021 </select> 3022 </div> 3023 <div class="mb-3"> 3024 <label for="sofortAnrufCalendarSelect" class="form-label">${t('Kalender')}</label> 3025 <select class="form-select" id="sofortAnrufCalendarSelect"> 3026 <option value="">${t('Kein Kalender')}</option> 3027 ${await this.getCalendarOptionsHtml()} 3028 </select> 3029 </div> 3030 <div id="selectedContactsCountSofortAnruf" class="text-muted small"></div> 3031 </div> 3032 <div class="modal-footer"> 3033 <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-i18n="Abbrechen">${t('Abbrechen')}</button> 3034 <button type="button" class="btn btn-warning" id="startSofortAnrufBtn" data-i18n="Sofort anrufen"><i class="fas fa-phone me-1"></i>${t('Sofort anrufen')}</button> 3035 </div> 3036 </div> 3037 </div> 3038 </div> 3039 3040 <!-- Bulk Actions --> 3041 <div class="bulk-actions-container mb-3" style="display: none;"> 3042 <div class="card"> 3043 <div class="card-body"> 3044 <div class="row g-3 align-items-end"> 3045 <div class="col-12 col-md-3"> 3046 <label class="form-label" for="bulkLeadStatus">${t('Lead-Status')}</label> 3047 <select class="form-select" id="bulkLeadStatus" name="bulkLeadStatus"> 3048 <option value="">${t('Status wählen')}</option> 3049 ${(this.leadStatuses || DEFAULT_LEAD_STATUSES).map(status => 3050 `<option value="${status}">${t(status)}</option>` 3051 ).join('')} 3052 </select> 3053 </div> 3054 <div class="col-12 col-md-3"> 3055 <label class="form-label" for="bulkAufgabe">${t('Aufgabe')}</label> 3056 <select class="form-select" id="bulkAufgabe" name="bulkAufgabe"> 3057 <option value="">${t('Aufgabe wählen')}</option> 3058 ${AUFGABE_OPTIONS.map(option => 3059 `<option value="${option}">${t(option)}</option>` 3060 ).join('')} 3061 </select> 3062 </div> 3063 <div class="col-12 col-md-3"> 3064 <label class="form-label" for="bulkIndustry">${t('Branche')}</label> 3065 <select class="form-select" id="bulkIndustry" name="bulkIndustry"> 3066 <option value="">${t('Branche wählen')}</option> 3067 ${DEFAULT_INDUSTRIES.map(industry => 3068 `<option value="${industry}">${t(industry)}</option>` 3069 ).join('')} 3070 </select> 3071 </div> 3072 <div class="col-12 col-md-2"> 3073 <label class="form-label" style="visibility: hidden;">${t('Aktion')}</label> 3074 <button type="button" class="btn btn-primary w-100" id="bulkUpdateBtn">${t('Ãndern')}</button> 3075 </div> 3076 <div class="col-12 col-md-auto kteam-bulk-col ${this._kteamInfo?.sicht?.darf_zuweisen ? '' : 'd-none'}"> 3077 <label class="form-label" style="visibility: hidden;">${t('Zuweisen')}</label> 3078 <button type="button" class="btn btn-outline-primary w-100" id="bulkAssignBtn" title="${t('Ausgewählte Kontakte einer Person zuweisen')}"><i class="fas fa-user-tag me-1"></i>${t('Zuweisen')}</button> 3079 </div> 3080 <div class="col-12 col-md-1"> 3081 <label class="form-label" style="visibility: hidden;">${t('Löschen')}</label> 3082 <button type="button" class="btn btn-danger w-100" id="bulkDeleteBtn">
3083 <i class="fas fa-trash"></i> 3084 </button> 3085 </div> 3086 </div> 3087 </div> 3088 </div> 3089 </div> 3090 <div class="table-responsive contacts-table-wrap"> 3091 <table class="table email-table mb-0"> 3092 <thead class="table-light"> 3093 <tr> 3094 <th class="checkbox-column text-center" style="width: 40px;"> 3095 <div class="dropdown"> 3096 <input type="checkbox" class="form-check-input dropdown-toggle" id="selectAll" 3097 data-bs-toggle="dropdown" aria-expanded="false" 3098 style="transform: scale(1.2); cursor: pointer;" /> 3099 <ul class="dropdown-menu" aria-labelledby="selectAll"> 3100 <li><a class="dropdown-item" href="#" id="selectAllPage"> 3101 <i class="fas fa-check-square me-2"></i>${t('Alle auf dieser Seite')} 3102 </a></li> 3103 <li><a class="dropdown-item" href="#" id="selectAllFiltered"> 3104 <i class="fas fa-filter me-2"></i>${t('Alle gefilterten Kontakte')} 3105 </a></li> 3106 </ul> 3107 </div> 3108 </th> 3109 <th class="name-column">${t('Kontakt')}</th> 3110 <th class="phone-column d-none d-md-table-cell" style="width: 190px;">${t('Telefonnummer')}</th> 3111 <th class="date-column d-none d-lg-table-cell" style="width: 150px;">${t('Aktualisiert')}</th> 3112 <th class="lead-status-column" style="width: 130px;">${t('Lead-Status')}</th> 3113 <th class="action-column text-end" style="width: 110px;">${t('Aktionen')}</th> 3114 </tr> 3115 </thead> 3116 <tbody id="contactsTableBody"> 3117 `; 3118 3119 // OPTIMIERUNG: Synchrones Rendering ohne await 3120 let rowsHtml = ''; 3121 if (contactsToShow.length === 0) { 3122 // Zeige eine Nachricht wenn keine Kontakte vorhanden sind 3123 rowsHtml = ` 3124 <tr> 3125 <td colspan="6" class="text-center py-4"> 3126 <div class="text-muted"> 3127 <i class="fas fa-users fa-3x mb-3"></i> 3128 <p>${contactsToRender.length === 0 ? t('Keine Kontakte vorhanden') : t('Keine Kontakte auf dieser Seite')}</p> 3129 </div> 3130 </td> 3131 </tr> 3132 `; 3133 } else { 3134 for (const contact of contactsToShow) { 3135 rowsHtml += this.renderContactRow(contact); 3136 } 3137 } 3138 html += rowsHtml; 3139 3140 html += ` 3141 </tbody> 3142 </table> 3143 </div> 3144 3145 <!-- Mobile Contacts Container --> 3146 <div class="mobile-contacts-container"> 3147 ${this.renderMobileContacts(contactsToShow, contactsToRender)} 3148 </div> 3149 3150 <!-- Mobile Floating Action Button --> 3151 <button type="button" class="mobile-fab" id="mobileNewContactBtn"> 3152 <i class="fas fa-plus"></i> 3153 </button> 3154 `; 3155 3156 // Add pagination controls if needed 3157 if (totalPages > 1) { 3158 html += ` 3159 <style> 3160 .pagination .page-link { 3161 color: #6c757d; 3162 background-color: #fff; 3163 border-color: #dee2e6; 3164 } 3165 .pagination .page-link:hover { 3166 color: #495057; 3167 background-color: #e9ecef; 3168 border-color: #dee2e6; 3169 } 3170 .pagination .page-item.active .page-link { 3171 color: #fff; 3172 background-color: #6c757d; 3173 border-color: #6c757d; 3174 } 3175 .pagination .page-item.disabled .page-link { 3176 color: #adb5bd; 3177 background-color: #fff; 3178 border-color: #dee2e6; 3179 } 3180 </style> 3181 <div class="d-flex flex-column align-items-start flex-md-row justify-content-md-between align-items-md-center mt-3"> 3182 <div class="text-muted mb-2 mb-md-0"> 3183 ${t('Zeige')} ${startIndex + 1} ${t('bis')} ${Math.min(endIndex, contactsToRender.length)} ${t('von')} ${contactsToRender.length} 3184 </div> 3185 <nav aria-label="${t('Kontakte Navigation')}" class="contacts-pagination"> 3186 <ul class="pagination mb-0"> 3187 <li class="page-item ${this.currentPage === 1 ? 'disabled' : ''}"> 3188 <a class="page-link" href="#" data-page="${this.currentPage - 1}"> 3189 <i class="fas fa-chevron-left"></i> 3190 </a> 3191 </li> 3192 `; 3193 3194 // Calculate which pages to show 3195 const maxVisiblePages = window.innerWidth >= 768 ? 20 : 5; // Desktop: 20, Mobile: 5 3196 let startPage = Math.max(1, this.currentPage - Math.floor(maxVisiblePages / 2)); 3197 let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1); 3198 3199 // Adjust startPage if we're near the end to keep maxVisiblePages 3200 if (endPage - startPage + 1 < maxVisiblePages) { 3201 startPage = Math.max(1, endPage - maxVisiblePages + 1); 3202 } 3203
3204 // Add first page if we're not starting from 1 3205 if (startPage > 1) { 3206 html += ` 3207 <li class="page-item"> 3208 <a class="page-link" href="#" data-page="1">1</a> 3209 </li> 3210 `; 3211 if (startPage > 2) { 3212 html += ` 3213 <li class="page-item disabled"> 3214 <span class="page-link">...</span> 3215 </li> 3216 `; 3217 } 3218 } 3219 3220 // Add page numbers 3221 for (let i = startPage; i <= endPage; i++) { 3222 html += ` 3223 <li class="page-item ${i === this.currentPage ? 'active' : ''}"> 3224 <a class="page-link" href="#" data-page="${i}">${i}</a> 3225 </li> 3226 `; 3227 } 3228 3229 // Add last page if we're not ending at totalPages 3230 if (endPage < totalPages) { 3231 if (endPage < totalPages - 1) { 3232 html += ` 3233 <li class="page-item disabled"> 3234 <span class="page-link">...</span> 3235 </li> 3236 `; 3237 } 3238 html += ` 3239 <li class="page-item"> 3240 <a class="page-link" href="#" data-page="${totalPages}">${totalPages}</a> 3241 </li> 3242 `; 3243 } 3244 3245 html += ` 3246 <li class="page-item ${this.currentPage === totalPages ? 'disabled' : ''}"> 3247 <a class="page-link" href="#" data-page="${this.currentPage + 1}"> 3248 <i class="fas fa-chevron-right"></i> 3249 </a> 3250 </li> 3251 </ul> 3252 </nav> 3253 </div> 3254 `; 3255 } 3256 3257 // Mobile Pagination und Load More Button 3258 const isMobile = window.innerWidth <= 768; 3259 if (isMobile) { 3260 // Zeige "Mehr laden" Button statt Pagination auf Mobile 3261 if (this.hasMore && !this.allContactsLoaded) { 3262 html += ` 3263 <div class="mobile-load-more-container text-center py-3"> 3264 <button class="btn btn-primary" id="mobileLoadMoreBtn"> 3265 <i class="fas fa-plus-circle me-2"></i>${t('Mehr Kontakte laden')} 3266 </button> 3267 <div class="text-muted small mt-2"> 3268 ${contactsToRender.length} ${t('von')} ${this.hasMore ? t('vielen') : contactsToRender.length} ${t('Kontakten geladen')} 3269 </div> 3270 </div> 3271 `; 3272 } else if (!this.hasMore) { 3273 html += ` 3274 <div class="text-center py-3 text-muted"> 3275 <i class="fas fa-check-circle me-2"></i>${t('Alle')} ${contactsToRender.length} ${t('Kontakte geladen')} 3276 </div> 3277 `; 3278 } 3279 } else if (totalPages > 1) { 3280 // Desktop Pagination wie gehabt 3281 html += ` 3282 <div class="mobile-pagination"> 3283 <button class="mobile-pagination-btn" ${this.currentPage === 1 ? 'disabled' : ''} data-page="${this.currentPage - 1}"> 3284 <i class="fas fa-chevron-left"></i> 3285 </button> 3286 <span class="mobile-pagination-info"> 3287 ${this.currentPage} / ${totalPages} 3288 </span> 3289 <button class="mobile-pagination-btn" ${this.currentPage === totalPages ? 'disabled' : ''} data-page="${this.currentPage + 1}"> 3290 <i class="fas fa-chevron-right"></i> 3291 </button> 3292 </div> 3293 `; 3294 } 3295 3296 // Mobile Bulk Actions Bar 3297 html += ` 3298 <div class="mobile-bulk-actions" id="mobileBulkActions"> 3299 <div class="mobile-bulk-actions-content">
3300 <span class="mobile-bulk-count"> 3301 <span id="mobileSelectedCount">0</span> ${t('ausgewählt')} 3302 </span> 3303 <button class="btn btn-sm btn-primary" id="mobileBulkEdit"> 3304 <i class="fas fa-edit"></i> 3305 </button> 3306 <button class="btn btn-sm btn-danger" id="mobileBulkDelete"> 3307 <i class="fas fa-trash"></i> 3308 </button> 3309 <button class="btn btn-sm btn-secondary" id="mobileCancelSelection"> 3310 ${t('Abbrechen')} 3311 </button> 3312 </div> 3313 </div> 3314 `; 3315 3316 const container = document.getElementById('contactsContainer'); 3317 if (!container) { 3318 console.error('Contacts container not found'); 3319 return; 3320 } 3321 3322 // Debug-Ausgabe für iPhone 3323 console.log('[renderContacts] Container gefunden, füge HTML ein'); 3324 console.log('[renderContacts] HTML-Länge:', html.length); 3325 3326 // Verwende requestAnimationFrame für bessere Performance 3327 requestAnimationFrame(() => { 3328 try { 3329 container.innerHTML = html; 3330 console.log('[renderContacts] HTML erfolgreich eingefügt'); 3331 } catch (e) { 3332 console.error('[renderContacts] Fehler beim Einfügen des HTML:', e); 3333 // Fallback: Einfache Nachricht anzeigen 3334 container.innerHTML = '<div class="alert alert-warning m-3">Fehler beim Laden der Kontakte. Bitte Seite neu laden.</div>'; 3335 } 3336 3337 // Binde Paginierungs-Event-Handler neu 3338 this.bindPaginationEvents(); 3339 3340 // Binde Event-Listener für Kontakt-Zeilen neu 3341 this.setupContactEventListeners(); 3342 3343 // Stelle Smart-Listen-UI wieder her 3344 this.updateSmartListsUI(); 3345 }); 3346 } 3347 3348 // Neue Methode für Paginierungs-Event-Handler 3349 bindPaginationEvents() { 3350 const paginationContainer = document.querySelector('.contacts-pagination'); 3351 if (paginationContainer) { 3352 // Entferne alte Event-Listener durch Klonen 3353 const newPaginationContainer = paginationContainer.cloneNode(true); 3354 paginationContainer.parentNode.replaceChild(newPaginationContainer, paginationContainer); 3355 3356 // Füge neue Event-Listener hinzu 3357 newPaginationContainer.addEventListener('click', async (e) => { 3358 e.preventDefault(); 3359 const pageLink = e.target.closest('.page-link'); 3360 if (pageLink) { 3361 const newPage = parseInt(pageLink.dataset.page); 3362 if (!isNaN(newPage) && newPage !== this.currentPage) { 3363 this.currentPage = newPage; 3364 // Wenn eine Smart List aktiv ist, rendere die gefilterten Kontakte 3365 if (this.currentFilteredContacts) { 3366 await this.renderContacts(this.currentFilteredContacts); 3367 } else { 3368 await this.renderContacts(this.contacts); 3369 } 3370 // Binde Event-Handler neu 3371 this.setupContactEventListeners(); 3372 // Stelle Smart-Listen-UI wieder her 3373 this.updateSmartListsUI(); 3374 // Speichere den aktualisierten Zustand 3375 this.storeContactsState(); 3376 // Scrolle nach oben 3377 window.scrollTo(0, 0); 3378 } 3379 } 3380 }); 3381 } 3382 } 3383 3384 /* contacts-list-v1: HTML-Escaping fuer alle Nutzerwerte (CSV-Import + KI-Erfassung am Telefon). */ 3385 escC(v) { 3386 if (v === null || v === undefined) return ''; 3387 return String(v) 3388 .replace(/&/g, '&') 3389 .replace(/</g, '<') 3390 .replace(/>/g, '>') 3391 .replace(/"/g, '"') 3392 .replace(/'/g, '''); 3393 } 3394 3395 /* contacts-list-v1: Anzeige-Identitaet eines Kontakts. Ein Viertel der Kontakte
3396 heisst "Unbekannt" - dort ist die Rufnummer die eigentliche Identitaet. */ 3397 contactIdentity(contact) { 3398 const rawName = String(contact.name || '').trim(); 3399 const phone = String(contact.phone_number || '').trim(); 3400 const company = String(contact.company || '').trim(); 3401 const email = String(contact.email || '').trim(); 3402 const unknown = !rawName || rawName === '-' || /^unbekannt/i.test(rawName); 3403 3404 const primary = unknown ? (phone || t('Unbekannt')) : rawName; 3405 const sub = []; 3406 if (unknown && phone) sub.push(t('Unbekannter Anrufer')); 3407 if (company) sub.push(company); 3408 if (email) sub.push(email); 3409 3410 let initials = ''; 3411 if (!unknown) { 3412 const parts = rawName.split(/\s+/).filter(Boolean); 3413 initials = parts.slice(0, 2).map(p => p.charAt(0)).join('').toUpperCase(); 3414 } 3415 // Deterministische Farbe, damit derselbe Kontakt immer gleich aussieht 3416 const key = (rawName || phone || String(contact.id || '')); 3417 let h = 0; 3418 for (let i = 0; i < key.length; i++) h = (h * 31 + key.charCodeAt(i)) >>> 0; 3419 const palette = ['#4C6EF5', '#12B886', '#F76707', '#7048E8', '#0CA678', '#E8590C', '#1C7ED6', '#AE3EC9']; 3420 3421 return { 3422 unknown, phone, company, email, 3423 primary, 3424 sub: sub.join(' \u00b7 '), 3425 initials, 3426 color: palette[h % palette.length] 3427 }; 3428 } 3429 3430 /* contacts-menu-fix-v1: Das Aktionsmenue lag in einem td mit overflow:hidden und wurde 3431 dadurch abgeschnitten - lesbar war nur der obere Rand von "Oeffnen". DREI Vorfahren 3432 klippen: .table td {overflow:hidden}, .v2-layout .table {overflow:hidden !important} 3433 und #mainContent .table-responsive {overflow-x:auto !important} aus erp.css. Genau 3434 daran ist meine fruehere Regel .contacts-table-wrap{overflow:visible} gescheitert 3435 (id-Selektor + !important schlagen sie). Diese Regeln zu ueberschreiben traefe JEDE 3436 Tabelle der App (Textkuerzung, runde Ecken, Querscrollen). Stattdessen holt 3437 position:fixed das Menue aus dem Klipp-Bereich - es bleibt dabei an seinem Platz im 3438 DOM, damit alle bestehenden Klick-Bindungen (Oeffnen/Loeschen) unveraendert laufen. 3439 data-bs-display="static" am Knopf schaltet Popper ab, das sonst dagegen positioniert. */ 3440 ensureMenuPositioning() { 3441 if (window.__clMenuFixBound) return; 3442 window.__clMenuFixBound = true; 3443 3444 const SEL = '.contact-quick-actions [data-bs-toggle="dropdown"]'; 3445 3446 const place = (toggle) => { 3447 const menu = toggle.parentElement && toggle.parentElement.querySelector('.dropdown-menu'); 3448 if (!menu) return; 3449 menu.style.position = 'fixed'; 3450 menu.style.transform = 'none'; 3451 menu.style.margin = '0'; 3452 menu.style.inset = 'auto'; 3453 const r = toggle.getBoundingClientRect(); 3454 const w = menu.offsetWidth || 180; 3455 const h = menu.offsetHeight || 0; 3456 const gap = 4; 3457 let top = r.bottom + gap; 3458 if (h && top + h > window.innerHeight - 8) { 3459 const above = r.top - gap - h; 3460 top = above > 8 ? above : Math.max(8, window.innerHeight - h - 8); 3461 } 3462 let left = r.right - w; 3463 left = Math.max(8, Math.min(left, window.innerWidth - w - 8)); 3464 menu.style.top = top + 'px'; 3465 menu.style.left = left + 'px'; 3466 }; 3467 3468 const closeAll = () => { 3469 document.querySelectorAll('.contact-quick-actions .dropdown-menu.show').forEach((menu) => { 3470 const toggle = menu.parentElement && menu.parentElement.querySelector('[data-bs-toggle="dropdown"]'); 3471 const inst = (toggle && window.bootstrap && window.bootstrap.Dropdown) 3472 ? window.bootstrap.Dropdown.getInstance(toggle) : null; 3473 if (inst) { inst.hide(); return; } 3474 menu.classList.remove('show'); 3475 if (toggle) toggle.setAttribute('aria-expanded', 'false'); 3476 }); 3477 }; 3478 3479 document.addEventListener('shown.bs.dropdown', (e) => { 3480 const toggle = (e.target && e.target.closest) ? e.target.closest(SEL) : null; 3481 if (toggle) place(toggle); 3482 }); 3483 /* Beim Scrollen wandert der Knopf, ein festes Menue nicht -> schliessen statt verrutschen. */ 3484 window.addEventListener('scroll', closeAll, true); 3485 window.addEventListener('resize', closeAll); 3486 } 3487 3488 renderContactRow(contact) { 3489 this.ensureMenuPositioning(); 3490 /* contacts-list-v1: Firma/Aufgabe wandern in die Unterzeile (waren zu 84%/92% leer), 3491 die tote Status-Spalte ist entfallen, alle Nutzerwerte sind escaped. */ 3492 const id = this.escC(contact.id); 3493 const idt = this.contactIdentity(contact); 3494 const task = String(contact.task || '').trim(); 3495 3496 const avatar = idt.initials 3497 ? `<span class="contact-avatar" style="background:${idt.color}">${this.escC(idt.initials)}</span>` 3498 : `<span class="contact-avatar" style="background:#adb5bd"><i class="fas fa-user"></i></span>`;
3499 3500 const callBtn = idt.phone 3501 ? `<a href="tel:${this.escC(idt.phone)}" class="btn btn-sm btn-outline-secondary contact-call-btn" title="${t('Anrufen')}"><i class="fas fa-phone"></i></a>` 3502 : ''; 3503 3504 return ` 3505 <tr data-contact-id="${id}" class="contact-row contact-link" style="cursor: pointer;" data-id="${id}" data-contact-name="${this.escC(contact.name || '')}" data-contact-email="${this.escC(contact.email || '')}" data-contact-phone="${this.escC(contact.phone_number || '')}"> 3506 <td class="checkbox-column text-center" style="width: 40px;"> 3507 <input type="checkbox" name="contactSelect" class="form-check-input contact-checkbox" value="${id}" id="contact-${id}" style="transform: scale(1.2);" /> 3508 </td> 3509 <td class="name-column"> 3510 <div class="contact-identity"> 3511 ${avatar} 3512 <div class="contact-identity-text"> 3513 <div class="fw-semibold contact-name-text" title="${this.escC(idt.primary)}">${this.escC(idt.primary)}</div> 3514 ${idt.sub ? `<div class="text-muted contact-subline" title="${this.escC(idt.sub)}">${this.escC(idt.sub)}</div>` : ''} 3515 <div class="small text-muted d-block d-md-none"> 3516 ${idt.phone && !idt.unknown ? `<i class="fas fa-phone me-1"></i>${this.escC(idt.phone)}` : ''} 3517 </div> 3518 ${task ? `<span class="badge bg-light text-dark border contact-task-chip mt-1"><i class="fas fa-clipboard-check me-1"></i>${this.escC(task)}</span>` : ''} 3519 ${KTeam.teamChips(contact, this._kteamInfo) /* kontakt-team-v1 */} 3520 </div> 3521 </div> 3522 </td> 3523 <td class="phone-column d-none d-md-table-cell">${idt.phone ? this.escC(idt.phone) : '<span class="text-muted">-</span>'}</td> 3524 <td class="date-column d-none d-lg-table-cell">${/* contacts-list-v1b: formatDate liefert bewusst HTML (Datum + Uhrzeit in 2 Zeilen) - hier NICHT escapen */ this.formatDate(contact.updated_at) || '-'}</td> 3525 <td class="lead-status-column"> 3526 <span class="badge ${this.getStatusClass(contact.lead_status)}">${this.escC(t(contact.lead_status || 'Neu'))}</span> 3527 </td> 3528 <td class="text-end action-column contact-noclick"> 3529 <div class="contact-quick-actions"> 3530 ${callBtn} 3531 <div class="dropdown"> 3532 <button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="dropdown" data-bs-display="static" aria-expanded="false" title="${t('Weitere Aktionen')}"> 3533 <i class="fas fa-ellipsis-h"></i> 3534 </button> 3535 <ul class="dropdown-menu dropdown-menu-end"> 3536 <li><a class="dropdown-item contact-open-item" href="#" data-id="${id}"><i class="fas fa-up-right-from-square fa-fw me-2"></i>${t('Ãffnen')}</a></li> 3537 ${idt.email ? `<li><a class="dropdown-item" href="mailto:${this.escC(idt.email)}"><i class="fas fa-envelope fa-fw me-2"></i>${t('E-Mail schreiben')}</a></li>` : ''} 3538 <li><hr class="dropdown-divider"></li> 3539 <li><a class="dropdown-item text-danger delete-contact" href="#" data-id="${id}"><i class="fas fa-trash-alt fa-fw me-2"></i>${t('Löschen')}</a></li> 3540 </ul> 3541 </div> 3542 </div> 3543 </td> 3544 </tr> 3545 `; 3546 } 3547 3548 renderMobileContacts(contactsToShow, allContacts) { 3549 if (!contactsToShow || contactsToShow.length === 0) { 3550 return ` 3551 <div class="mobile-no-contacts"> 3552 <i class="fas fa-users"></i> 3553 <p>
3553${allContacts && allContacts.length === 0 ? t('Keine Kontakte vorhanden') : t('Keine Kontakte auf dieser Seite')}</p> 3554 </div> 3555 `; 3556 } 3557 3558 let html = ''; 3559 3560 for (const contact of contactsToShow) { 3561 const latestCallSummary = null; // Performance-Optimierung 3562 3563 html += ` 3564 <div class="mobile-contact-card" data-contact-id="${this.escC(contact.id)}" data-contact-name="${this.escC(contact.name || '')}" data-contact-email="${this.escC(contact.email || '')}" data-contact-phone="${this.escC(contact.phone_number || '')}"> 3565 <div class="mobile-contact-header" data-id="${contact.id}"> 3566 <div class="mobile-contact-checkbox"> 3567 <input type="checkbox" 3568 class="form-check-input contact-checkbox" 3569 value="${contact.id}" 3570 id="mobile-contact-${contact.id}"> 3571 </div> 3572 3573 <div class="mobile-contact-info"> 3574 <div class="mobile-contact-name" data-field="name">${this.escC(this.contactIdentity(contact).primary)}</div> 3575 ${contact.company ? `<div class="mobile-contact-company" data-field="company">${this.escC(contact.company)}</div>` : ''} 3576 3577 <div class="mobile-contact-details"> 3578 ${contact.email ? ` 3579 <div class="mobile-contact-detail-item"> 3580 <i class="fas fa-envelope"></i> 3581 <span class="mobile-contact-email" data-field="email">${this.escC(contact.email)}</span> 3582 </div> 3583 ` : ''} 3584 ${contact.phone_number ? ` 3585 <div class="mobile-contact-detail-item"> 3586 <i class="fas fa-phone"></i> 3587 <span class="mobile-contact-phone" data-field="phone">${this.escC(contact.phone_number)}</span> 3588 </div> 3589 ` : ''} 3590 </div> 3591 </div> 3592 3593 <div class="mobile-contact-badges"> 3594 <span class="mobile-lead-badge ${this.getStatusClass(contact.lead_status)}"> 3595 ${this.escC(t(contact.lead_status || 'Neu'))} 3596 </span> 3597 </div> 3598 3599 <div class="mobile-contact-actions"> 3600 <button class="mobile-delete-btn delete-contact" data-id="${contact.id}"> 3601 <i class="fas fa-trash"></i> 3602 </button> 3603 </div> 3604 </div> 3605 </div> 3606 `; 3607 } 3608 3609 return html; 3610 } 3611 3612 getStatusClass(status) { 3613 const statusClasses = { 3614 'Neu': 'bg-info', 3615 'Anrufen': 'bg-warning', 3616 'Angerufen': 'bg-primary', 3617 'In Bearbeitung': 'bg-warning', 3618 'Qualifiziert': 'bg-success', 3619 'Nicht Qualifiziert': 'bg-danger', 3620 'Kein Bedarf': 'bg-secondary', 3621 'Kunde': 'bg-success', 3622 'Wiedervorlage': 'bg-info' 3623 }; 3624 return statusClasses[status] || 'bg-secondary'; 3625 } 3626 3627 parseContactDate(timestamp) { 3628 // Hilfsmethode zum Parsen von Kontakt-Datumsfeldern für Filter 3629 if (!timestamp) return null; 3630 3631 try { 3632 let date; 3633 if (timestamp?.toDate) { 3634 // Firebase Timestamp 3635 date = timestamp.toDate(); 3636 } else if (typeof timestamp === 'object' && timestamp.seconds) { 3637 // Firebase Timestamp-ähnliches Objekt 3638 date = new Date(timestamp.seconds * 1000); 3639 } else if (typeof timestamp === 'number') { 3640 // Unix Timestamp in Millisekunden 3641 date = new Date(timestamp); 3642 } else if (typeof timestamp === 'string') { 3643 // ISO String oder anderes Datumsformat 3644 if (timestamp.includes('T')) { 3645 // ISO String 3646 date = new Date(timestamp); 3647 } else { 3648 // Versuche verschiedene Datumsformate
3649 const parts = timestamp.split(/[./-]/); 3650 if (parts.length === 3) { 3651 // Annahme: DD.MM.YYYY oder YYYY-MM-DD 3652 if (parts[0].length === 4) { 3653 // YYYY-MM-DD 3654 date = new Date(parts[0], parts[1] - 1, parts[2]); 3655 } else { 3656 // DD.MM.YYYY 3657 date = new Date(parts[2], parts[1] - 1, parts[0]); 3658 } 3659 } else { 3660 date = new Date(timestamp); 3661 } 3662 } 3663 } else { 3664 return null; 3665 } 3666 3667 // Ãberprüfe ob das Datum gültig ist 3668 if (isNaN(date.getTime())) { 3669 return null; 3670 } 3671 3672 return date; 3673 } catch (error) { 3674 console.error('Error parsing contact date:', error, 'Timestamp:', timestamp); 3675 return null; 3676 } 3677 } 3678 3679 formatDate(timestamp) { 3680 // Wenn kein Timestamp vorhanden ist, return '-' 3681 if (!timestamp) return '-'; 3682 3683 try { 3684 // Debug-Ausgabe 3685 // console.log('Original timestamp:', timestamp); 3686 // console.log('Timestamp type:', typeof timestamp); 3687 if (timestamp?.toDate) { 3688 // console.log('Is Firestore timestamp'); 3689 } 3690 3691 // Konvertiere Firebase Timestamp oder String zu Date Object 3692 let date; 3693 if (timestamp?.toDate) { 3694 // Firebase Timestamp 3695 date = timestamp.toDate(); 3696 } else if (typeof timestamp === 'object' && timestamp.seconds) { 3697 // Firebase Timestamp-ähnliches Objekt 3698 date = new Date(timestamp.seconds * 1000); 3699 } else if (typeof timestamp === 'number') { 3700 // Unix Timestamp in Millisekunden 3701 date = new Date(timestamp); 3702 } else if (typeof timestamp === 'string') { 3703 // ISO String oder anderes Datumsformat 3704 if (timestamp.includes('T')) { 3705 // ISO String 3706 date = new Date(timestamp); 3707 } else { 3708 // Versuche verschiedene Datumsformate 3709 const parts = timestamp.split(/[./-]/); 3710 if (parts.length === 3) { 3711 // Annahme: DD.MM.YYYY oder YYYY-MM-DD 3712 if (parts[0].length === 4) { 3713 // YYYY-MM-DD 3714 date = new Date(parts[0], parts[1] - 1, parts[2]); 3715 } else { 3716 // DD.MM.YYYY 3717 date = new Date(parts[2], parts[1] - 1, parts[0]); 3718 } 3719 } else { 3720 date = new Date(timestamp); 3721 } 3722 } 3723 } else { 3724 console.error('Unbekanntes Datumsformat:', timestamp); 3725 return '-'; 3726 } 3727 3728 // console.log('Parsed date:', date); 3729 3730 // Ãberprüfe ob das Datum gültig ist 3731 if (isNaN(date.getTime())) { 3732 console.error('Ungültiges Datum:', timestamp); 3733 return '-'; 3734 } 3735 3736 // console.log('Parsed date:', date); 3737 3738 // Formatiere das Datum in 2 Zeilen: Datum + Uhrzeit 3739 const datePart = date.toLocaleDateString(getLocale(), { 3740 year: 'numeric', 3741 month: '2-digit', 3742 day: '2-digit' 3743 }); 3744 const timePart = date.toLocaleTimeString(getLocale(), { 3745 hour: '2-digit', 3746 minute: '2-digit' 3747 }); 3748 3749 // console.log('Formatted date:', datePart, timePart); 3750 return `${datePart}<br><span class="text-muted small">${timePart}</span>`; 3751 3752 } catch (error) { 3753 console.error('Error formatting date:', error, 'Timestamp:', timestamp); 3754 return '-'; 3755 } 3756 } 3757 3758 // kontakt-team-v1: Filter "Zuweisung"/"Liste" und Knopf "Zuweisen". 3759 // Die Filterleiste selbst entsteht in pages.js; die beiden Auswahlfelder 3760 // werden hier nachgeruestet, damit die Aenderung auf diese Datei begrenzt bleibt. 3761 async _kteamFilterEinbauen() { 3762 try { 3763 if (!this._kteamInfo) { 3764 this._kteamInfo = await KTeam.ladeInfo(); 3765 } 3766 } catch (e) { 3767 console.warn('[kontakt-team] Info nicht ladbar:', e); 3768 return; 3769 } 3770 const info = this._kteamInfo; 3771 const anker = document.querySelector('#aufgabeFilter'); 3772 if (anker && !document.querySelector('#zuweisungFilter')) { 3773 const sel = document.createElement('select'); 3774 sel.className = 'form-select'; 3775 sel.id = 'zuweisungFilter'; 3776 sel.title = t('Zuweisung'); 3777 sel.innerHTML = `<option value="">${t('Zuweisung: Alle')}</option> 3778 <option value="__mir">${t('Mir zugewiesen')}</option> 3779 <option value="__keine">${t('Nicht zugewiesen')}</option> 3780 ${(info.mitglieder || []).map(m =>
3780 `<option value="${this.escC(m.uid)}">${this.escC(m.name)}</option>`).join('')}`; 3781 anker.insertAdjacentElement('afterend', sel); 3782 sel.addEventListener('change', () => this.applyFilters()); 3783 } 3784 const listen = [...new Set((this.contacts || []).map(c => c.liste).filter(Boolean))].sort(); 3785 let lsel = document.querySelector('#listeFilter'); 3786 if (listen.length && anker && !lsel) { 3787 lsel = document.createElement('select'); 3788 lsel.className = 'form-select'; 3789 lsel.id = 'listeFilter'; 3790 lsel.title = t('Liste'); 3791 (document.querySelector('#zuweisungFilter') || anker).insertAdjacentElement('afterend', lsel); 3792 lsel.addEventListener('change', () => this.applyFilters()); 3793 } 3794 if (lsel) { 3795 const aktuell = lsel.value; 3796 lsel.innerHTML = `<option value="">${t('Liste: Alle')}</option>` 3797 + listen.map(l => `<option value="${this.escC(l)}">${this.escC(l)}</option>`).join(''); 3798 if (listen.includes(aktuell)) lsel.value = aktuell; 3799 } 3800 const btn = document.getElementById('bulkAssignBtn'); 3801 if (btn) btn.closest('.kteam-bulk-col')?.classList.toggle('d-none', !info.sicht?.darf_zuweisen); 3802 if (!window.__kteamBulkListener) { 3803 window.__kteamBulkListener = true; 3804 document.addEventListener('click', (e) => { 3805 if (!e.target.closest('#bulkAssignBtn')) return; 3806 e.preventDefault(); 3807 const inst = window.__kteamContacts; 3808 if (!inst) return; 3809 const ids = inst.getSelectedContactIds(); 3810 if (!ids.length) { UI.showError(t('Keine Kontakte ausgewählt')); return; } 3811 KTeam.oeffneZuweisenDialog(ids, async (r) => { 3812 if (r?.meldung) UI.showSuccess(r.meldung); 3813 await inst.clearContactsCache(); 3814 await inst.loadContacts(true);
3815 await inst.applyFilters(); 3816 }); 3817 }); 3818 } 3819 window.__kteamContacts = this; 3820 } 3821 3822 async applyFilters() { 3823 // showContactsLoadingIndicator(); // Ladeindikator deaktiviert für bessere Performance 3824 const filterButton = document.getElementById('apply-filters-btn'); 3825 let originalButtonText = ''; // Standardwert definieren 3826 3827 // Wenn Filter manuell angewendet werden, ist keine Smart List mehr aktiv 3828 localStorage.removeItem('activeSmartListId'); 3829 this.activeSmartListId = null; 3830 // console.log("[applyFilters] Removed activeSmartListId from localStorage due to manual filter change."); 3831 3832 // Prüfe, ob der Button existiert, bevor darauf zugegriffen wird 3833 if (filterButton) { 3834 originalButtonText = filterButton.innerHTML; 3835 filterButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Laden...'; 3836 filterButton.disabled = true; 3837 } 3838 3839 try { 3840 const searchTerm = document.querySelector('#searchInput')?.value?.toLowerCase() || ''; 3841 const leadStatus = document.querySelector('#leadStatusFilter')?.value || ''; 3842 const task = document.querySelector('#aufgabeFilter')?.value || ''; 3843 const branche = document.querySelector('#brancheFilter')?.value || ''; 3844 const dateFrom = document.querySelector('#dateFromFilter')?.value || ''; 3845 const dateTo = document.querySelector('#dateToFilter')?.value || ''; 3846 // kontakt-team-v1: Zuweisung und Importliste 3847 await this._kteamFilterEinbauen(); 3848 const zuweisung = document.querySelector('#zuweisungFilter')?.value || ''; 3849 const liste = document.querySelector('#listeFilter')?.value || ''; 3850 const eigeneUid = auth.currentUser?.uid || ''; 3851 3852 // Filter the existing contacts instead of loading them again 3853 let filteredContacts = [...this.contacts]; 3854 3855 // Wenn nicht alle Kontakte geladen sind und es eine Suche gibt, zeige Hinweis 3856 if (searchTerm && !this.backgroundLoadingComplete) { 3857 // console.log(`Suche in ${this.contacts.length} geladenen Kontakten. Hintergrund-Laden läuft noch...`); 3858 // Optional: Zeige einen Hinweis an den Benutzer 3859 const searchInfo = document.getElementById('searchInfo'); 3860 if (searchInfo) { 3861 searchInfo.innerHTML = `<small class="text-muted">Durchsuche ${this.contacts.length} Kontakte... (weitere werden geladen)</small>`; 3862 searchInfo.style.display = 'block'; 3863 } 3864 } 3865 3866 // Apply filters 3867 filteredContacts = filteredContacts.filter(contact => { 3868 const searchFields = [ 3869 contact.name, 3870 contact.email, 3871 contact.company, 3872 contact.phone_number, 3873 contact.industry 3874 ].map(suchText).filter(Boolean); // kontakte-filter-v1 3875 3876 const matchesSearch = searchTerm === '' || searchFields.some(field => field.includes(searchTerm)); 3877 const matchesStatus = !leadStatus || contact.lead_status === leadStatus; 3878 const matchesTask = !task || contact.task === task; 3879 const matchesBranche = !branche || contact.industry === branche; 3880 3881 // Datumsbereich-Filter für updated_at 3882 let matchesDateRange = true; 3883 if (dateFrom || dateTo) { 3884 const contactDate = this.parseContactDate(contact.updated_at); 3885 3886 if (contactDate) { 3887 if (dateFrom) { 3888 const fromDate = new Date(dateFrom); 3889 fromDate.setHours(0, 0, 0, 0); // Start des Tages 3890 matchesDateRange = matchesDateRange && contactDate >= fromDate; 3891 } 3892 3893 if (dateTo) { 3894 const toDate = new Date(dateTo); 3895 toDate.setHours(23, 59, 59, 999); // Ende des Tages 3896 matchesDateRange = matchesDateRange && contactDate <= toDate; 3897 } 3898 } else { 3899 // Wenn kein Datum vorhanden ist, Filter nicht erfüllt 3900 matchesDateRange = false;
3901 } 3902 } 3903 3904 const matchesTeam = KTeam.passtZuweisung(contact, zuweisung, eigeneUid); // kontakt-team-v1 3905 const matchesListe = !liste || (contact.liste || '') === liste; 3906 return matchesSearch && matchesStatus && matchesTask && matchesBranche && matchesDateRange && matchesTeam && matchesListe; 3907 }); 3908 3909 // Store the filtered contacts 3910 this.currentFilteredContacts = filteredContacts; 3911 3912 // Render the filtered contacts - this removes old DOM listeners implicitly 3913 await this.renderContacts(filteredContacts); 3914 3915 // Set up event listeners for the new DOM elements 3916 this.setupContactEventListeners(); // Zurück geändert 3917 3918 // Restore the Smart Lists UI as renderContacts replaced the placeholder 3919 this.updateSmartListsUI(); 3920 3921 } catch (error) { 3922 console.error('Error applying filters:', error); 3923 UI.showError('Error filtering contacts'); 3924 } finally { 3925 // Stelle den Button-Text nur wieder her, wenn der Button existiert 3926 if (filterButton) { 3927 filterButton.innerHTML = originalButtonText; 3928 filterButton.disabled = false; 3929 } 3930 // hideContactsLoadingIndicator(); // Ladeindikator deaktiviert 3931 } 3932 3933 // Nach dem Filtern die gefilterten Kontakte im Cache speichern 3934 // kontakte-cache-v1: Der Schluessel enthielt Date.now(), also legte JEDER 3935 // Filtervorgang einen NEUEN localStorage-Eintrag mit der kompletten Liste an 3936 // (bei einem mittleren Mandanten ~230 KB pro Eintrag). Nach wenigen Suchen 3937 // war das 5-MB-Kontingent erschoepft, der QuotaExceeded-Fehler wurde still 3938 // verschluckt und danach funktionierte gar kein Cache mehr. Fe
3938ster 3939 // Schluessel: der letzte Stand ueberschreibt den vorigen. 3940 const cacheKey = `filtered_contacts_${this.currentUserUID}`; 3941 setCachedFilteredContacts(cacheKey, this.currentFilteredContacts); 3942 } 3943 3944 async bindEvents() { 3945 // console.log("[bindEvents] Function called."); // NEUER LOG AM ANFANG 3946 // Wait for DOM to be ready 3947 if (!document.getElementById('contactsContainer')) { 3948 console.warn('Contacts container not found, skipping event binding'); 3949 return; 3950 } 3951 3952 // Initialize modal if not already done 3953 this.initializeModal(); 3954 3955 // New contact button handler 3956 const newContactBtn = document.getElementById('newContactBtn'); 3957 if (newContactBtn) { 3958 newContactBtn.addEventListener('click', () => this.showNewContactModal()); 3959 } 3960 3961 // Mobile New contact floating button handler 3962 const mobileNewContactBtn = document.getElementById('mobileNewContactBtn'); 3963 if (mobileNewContactBtn) { 3964 mobileNewContactBtn.addEventListener('click', () => this.showNewContactModal()); 3965 } 3966 3967 // Mobile contact card click handlers mit Event-Delegation 3968 const mobileContactsContainer = document.querySelector('.mobile-contacts-container'); 3969 console.log('[Mobile Click Setup] Container gefunden:', !!mobileContactsContainer); 3970 3971 if (mobileContactsContainer) { 3972 // Zähle mobile Kontakt-Karten 3973 const mobileCards = mobileContactsContainer.querySelectorAll('.mobile-contact-card'); 3974 console.log('[Mobile Click Setup] Anzahl mobile Karten:', mobileCards.length); 3975 3976 // Entferne alte Listener falls vorhanden 3977 if (this.mobileContactClickHandler) { 3978 mobileContactsContainer.removeEventListener('click', this.mobileContactClickHandler); 3979 } 3980 3981 // Definiere neuen Handler 3982 this.mobileContactClickHandler = async (e) => { 3983 console.log('[Mobile Click] Click-Event ausgelöst auf:', e.target); 3984 3985 // Finde den geklickten Header oder die Karte selbst 3986 const card = e.target.closest('.mobile-contact-card'); 3987 const header = e.target.closest('.mobile-contact-header'); 3988 3989 console.log('[Mobile Click] Card gefunden:', !!card, 'Header gefunden:', !!header); 3990 3991 if (!card && !header) return; 3992 3993 // Verhindere Click wenn Checkbox oder Delete-Button geklickt wurde 3994 if (e.target.closest('.mobile-contact-checkbox') || 3995 e.target.closest('.mobile-delete-btn') || 3996 e.target.closest('input[type="checkbox"]') || 3997 e.target.closest('button.delete-contact')) { 3998 console.log('[Mobile Click] Click auf Checkbox oder Delete-Button - ignoriert'); 3999 return; 4000 } 4001 4002 // Hole contactId von Header oder Card 4003 const contactId = (header && header.dataset.id) || (card && card.dataset.contactId); 4004 console.log('[Mobile Click] Contact ID:', contactId); 4005 4006 if (contactId) {
4007 console.log('[Mobile Click] Ãffne Kontakt-Details für ID:', contactId); 4008 4009 // Prüfe ob ContactDetail verfügbar ist 4010 if (typeof ContactDetail !== 'undefined' && ContactDetail.show) { 4011 console.log('[Mobile Click] ContactDetail.show() wird aufgerufen'); 4012 ContactDetail.show(contactId); 4013 } else { 4014 console.log('[Mobile Click] ContactDetail nicht gefunden, lade dynamisch...'); 4015 // Fallback: Lade ContactDetail dynamisch 4016 try { 4017 const module = await import('./contactDetail.js?v=20260807-noshow2-soap1-p5f1-rrev1-bill1-intake1-alarm1-tokfix1-dat1-mod1b-nam1-merk1--kal1-kont1--kd1-prax1-nsl1-nsl2-impauth1-tdup2-kf1-kf3-kteam1-kt3'); 4018 const ContactDetailModule = module.default || module.ContactDetail; 4019 if (ContactDetailModule && ContactDetailModule.show) { 4020 console.log('[Mobile Click] ContactDetail dynamisch geladen und aufgerufen'); 4021 ContactDetailModule.show(contactId); 4022 } else { 4023 console.error('[Mobile Click] ContactDetail.show nicht gefunden im Modul'); 4024 } 4025 } catch (error) { 4026 console.error('[Mobile Click] Fehler beim Laden von ContactDetail:', error); 4027 } 4028 } 4029 } else { 4030 console.warn('[Mobile Click] Keine Contact ID gefunden'); 4031 } 4032 }; 4033 4034 // Füge Event-Listener hinzu 4035 mobileContactsContainer.addEventListener('click', this.mobileContactClickHandler); 4036 console.log('[Mobile Click Setup] Event-Listener hinzugefügt'); 4037 } else { 4038 console.warn('[Mobile Click Setup] Mobile Container nicht gefunden - vermutlich Desktop-Ansicht'); 4039 } 4040 4041 // Mobile pagination handlers 4042 document.querySelectorAll('.mobile-pagination-btn').forEach(btn => { 4043 btn.addEventListener('click', (e) => { 4044 e.preventDefault(); 4045 const page = parseInt(btn.dataset.page); 4046 if (!btn.disabled && page) { 4047 this.currentPage = page; 4048 this.renderContacts(this.currentFilteredContacts || this.contacts); 4049 } 4050 }); 4051 }); 4052 4053 // Mobile bulk actions handlers 4054 const mobileBulkEdit = document.getElementById('mobileBulkEdit'); 4055 if (mobileBulkEdit) { 4056 mobileBulkEdit.addEventListener('click', () => this.handleBulkUpdate()); 4057 } 4058 4059 const mobileBulkDelete = document.getElementById('mobileBulkDelete'); 4060 if (mobileBulkDelete) { 4061 mobileBulkDelete.addEventListener('click', () => this.handleBulkDelete()); 4062 } 4063 4064 const mobileCancelSelection = document.getElementById('mobileCancelSelection'); 4065 if (mobileCancelSelection) { 4066 mobileCancelSelection.addEventListener('click', () => { 4067 this.deselectAllContacts(); 4068 this.updateBulkActionsVisibility(); 4069 }); 4070 } 4071 4072 // Import button handler 4073 const importBtn = document.getElementById('importContactsBtn'); 4074 if (importBtn) { 4075 importBtn.addEventListener('click', (e) => { 4076 e.stopPropagation(); 4077 this.contactImportUI.show(); 4078 }); 4079 } 4080 4081 // Smart List button handler 4082 const createSmartListBtn = document.getElementById('createSmartListBtn'); 4083 if (createSmartListBtn) { 4084 createSmartListBtn.addEventListener('click', () => { 4085 // Ãbersetze das Modal vor dem Anzeigen 4086 this.translateSmartListModal(); 4087 if (this.smartListModal) { 4088 this.smartListModal.show(); 4089 } 4090 }); 4091 } 4092 4093 // Add event listener for group email sending 4094 const sendGroupEmailBtn = document.getElementById('sendGroupEmailBtn'); 4095 if (sendGroupEmailBtn) { 4096 sendGroupEmailBtn.addEventListener('click', () => this.handleSendGroupEmail()); 4097 } 4098 4099 // Add event listener for Sofort Anruf button 4100 const sofortAnrufBtn = document.getElementById('sofortAnrufBtn'); 4101 if (sofortAnrufBtn) { 4102 sofortAnrufBtn.addEventListener('click', () => this.handleSofortAnruf()); 4103 } 4104 4105 // Add event listener for AutoCall button 4106 const autoCallBtn = document.getElementById('autoCallBtn'); 4107 if (autoCallBtn) { 4108 autoCallBtn.addEventListener('click', () => this.handleAutoCall()); 4109 } 4110 4111 // Clear any existing handlers 4112 if (this.checkboxHandler) { 4113 const oldCheckboxes = document.querySelectorAll('.contact-checkbox');
4114 oldCheckboxes.forEach(checkbox => { 4115 checkbox.removeEventListener('change', this.checkboxHandler); 4116 }); 4117 } 4118 4119 // Store event handler references 4120 this.bulkUpdateHandler = () => this.handleBulkUpdate(); 4121 this.bulkDeleteHandler = () => this.handleBulkDelete(); 4122 this.checkboxHandler = () => this.updateBulkActionsVisibility(); 4123 4124 // Bind checkboxes 4125 const checkboxes = document.querySelectorAll('.contact-checkbox'); 4126 checkboxes.forEach(checkbox => { 4127 checkbox.addEventListener('change', this.checkboxHandler); 4128 }); 4129 4130 // Bind select all checkbox 4131 const selectAllCheckbox = document.getElementById('selectAll'); 4132 if (selectAllCheckbox) { 4133 this.selectAllHandler = (e) => { 4134 const isChecked = e.target.checked; 4135 const visibleCheckboxes = document.querySelectorAll('.contact-row:not([style*="display: none"]) .contact-checkbox'); 4136 visibleCheckboxes.forEach(checkbox => { 4137 checkbox.checked = isChecked; 4138 }); 4139 this.updateBulkActionsVisibility(); 4140 }; 4141 selectAllCheckbox.removeEventListener('change', this.selectAllHandler); 4142 selectAllCheckbox.addEventListener('change', this.selectAllHandler); 4143 } 4144 4145 // Search 4146 const searchInput = document.querySelector('#searchInput'); 4147 if (searchInput) { 4148 searchInput.removeEventListener('input', e => this.debounceSearch(e)); 4149 searchInput.addEventListener('input', e => this.debounceSearch(e)); 4150 } 4151 4152 // Clear search button 4153 const clearSearchBtn = document.querySelector('#clearSearchBtn'); 4154 if (clearSearchBtn) { 4155 this.clearSearchHandler = async () => { 4156 const searchInput = document.querySelector('#searchInput'); 4157 if (searchInput) { 4158 searchInput.value = ''; 4159 // Wenn Suche gelöscht wird, ist keine Smart List mehr aktiv 4160 localStorage.removeItem('activeSmartListId'); 4161 this.activeSmartListId = null; 4162 this.applyFilters(); // Wende leere Filter an, um alle Kontakte zu zeigen 4163 } 4164 }; 4165 clearSearchBtn.removeEventListener('click', this.clearSearchHandler); 4166 clearSearchBtn.addEventListener('click', this.clearSearchHandler); 4167 } 4168 4169 // Bind all filter dropdowns 4170 const leadStatusFilter = document.querySelector('#leadStatusFilter'); 4171 const taskFilter = document.querySelector('#aufgabeFilter'); 4172 const brancheFilter = document.querySelector('#brancheFilter'); 4173 const dateFromFilter = document.querySelector('#dateFromFilter'); 4174 const dateToFilter = document.querySelector('#dateToFilter'); 4175 const clearDateFilterBtn = document.querySelector('#clearDateFilterBtn'); 4176 4177 // Erstelle einen einzigen Filter-Handler für alle Filteränderungen 4178 this.filterChangeHandler = () => { 4179 // Füge eine kleine Verzögerung hinzu, falls mehrere Filter schnell geändert werden 4180 if (this.filterTimeout) { 4181 clearTimeout(this.filterTimeout); 4182 } 4183 this.filterTimeout = setTimeout(() => { 4184 this.applyFilters(); 4185 }, 300); // 300ms Verzögerung 4186 }; 4187 4188 [leadStatusFilter, taskFilter, brancheFilter, dateFromFilter, dateToFilter].forEach(filter => { 4189 if (filter) { 4190 // Entferne zuerst alle vorherigen Listener für diesen Handler 4191 filter.removeEventListener('change', this.filterChangeHandler); 4192 // Füge den neuen Handler hinzu 4193 filter.addEventListener('change', this.filterChangeHandler); 4194 } 4195 }); 4196 4197 // Clear Date Filter Button 4198 if (clearDateFilterBtn) { 4199 this.clearDateFilterHandler = () => { 4200 const dateFromFilter = document.querySelector('#dateFromFilter'); 4201 const dateToFilter = document.querySelector('#dateToFilter'); 4202 if (dateFromFilter) dateFromFilter.value = ''; 4203 if (dateToFilter) dateToFilter.value = ''; 4204 this.applyFilters(); 4205 }; 4206 clearDateFilterBtn.removeEventListener('click', this.clearDateFilterHandler); 4207 clearDateFilterBtn.addEventListener('click', this.clearDateFilterHandler); 4208 } 4209 4210 // --- ÃNDERUNG: Zurück zur Logik aus contacts.js2 --- 4211 // Die Handler werden *nicht* hier definiert, sondern sind die Funktionen selbst. 4212 // Entferne die vorherigen Definitionen von this.bulkUpdateHandler und this.bulkDeleteHandler 4213 4214 // Bind bulk action buttons (ruft direkt die implementierenden Funktionen auf) 4215 const bulkUpdateBtn = document.getElementById('bulkUpdateBtn'); 4216 const bulkDeleteBtn = document.getElementById('bulkDeleteBtn'); 4217 4218 if (bulkUpdateBtn) { 4219 // console.log("[bindEvents] Trying to attach listener to #bulkUpdateBtn. Element found:", bulkUpdateBtn); 4220 // console.log("[bindEvents] #bulkUpdateBtn disabled status:", bulkUpdateBtn.disabled); 4221 // Stelle sicher, dass alte Listener entfernt werden (cloneNode ist eine robuste Methode) 4222 const newBulkUpdateBtn = bulkUpdateBtn.cloneNode(true); 4223 bulkUpdateBtn.parentNode.replaceChild(newBulkUpdateBtn, bulkUpdateBtn); 4224 // Füge den Listener hinzu, der direkt die Funktion aufruft 4225 newBulkUpdateBtn.addEventListener('click', () => { 4226 // console.log("[bindEvents] #bulkUpdateBtn clicked! Calling handleBulkUpdate..."); // Log wird jetzt in handleBulkUpdate gemacht 4227 this.handleBulkUpdate(); 4228 });
4229 // console.log("[bindEvents] Listener ATTACHED to #bulkUpdateBtn."); 4230 } else { 4231 console.error("[bindEvents] #bulkUpdateBtn element NOT FOUND!"); 4232 } 4233 4234 if (bulkDeleteBtn) { 4235 // console.log("[bindEvents] Trying to attach listener to #bulkDeleteBtn. Element found:", bulkDeleteBtn); 4236 // Stelle sicher, dass alte Listener entfernt werden 4237 const newBulkDeleteBtn = bulkDeleteBtn.cloneNode(true); 4238 bulkDeleteBtn.parentNode.replaceChild(newBulkDeleteBtn, bulkDeleteBtn); 4239 // Füge den Listener hinzu, der direkt die Funktion aufruft 4240 newBulkDeleteBtn.addEventListener('click', () => { 4241 // console.log("[bindEvents] #bulkDeleteBtn clicked! Calling handleBulkDelete..."); // Log wird jetzt in handleBulkDelete gemacht 4242 this.handleBulkDelete(); 4243 }); 4244 // console.log("[bindEvents] Listener ATTACHED to #bulkDeleteBtn."); 4245 } else { 4246 console.error("[bindEvents] #bulkDeleteBtn element NOT FOUND!"); 4247 } 4248 // --- ÃNDERUNG ENDE --- 4249 4250 // Bind row click handlers 4251 const contactRows = document.querySelectorAll('.contact-row'); 4252 contactRows.forEach(row => { 4253 row.addEventListener('click', async (e) => { 4254 // Don't navigate if clicking checkbox or delete button 4255 if (e.target.closest('.contact-checkbox') || 4256 (e.target.closest('.contact-noclick') && !e.target.closest('.contact-open-item')) || 4257 e.target.closest('.delete-contact')) { 4258 return; 4259 } 4260 4261 e.preventDefault(); 4262 const contactId = row.dataset.contactId; 4263 4264 // NEUE FUNKTION: Speichere den aktuellen Zustand vor der Navigation 4265 this.storeContactsState(); 4266 4267 const url = new URL(window.location.href); 4268 url.searchParams.set('page', 'contact-detail'); 4269 url.searchParams.set('id', contactId); 4270 window.history.pushState({}, '', url); 4271 await Pages.loadPage('contact-detail'); 4272 }); 4273 }); 4274 4275 // Bind delete buttons 4276 const deleteButtons = document.querySelectorAll('.delete-contact'); 4277 deleteButtons.forEach(button => { 4278 button.addEventListener('click', async (e) => { 4279 e.stopPropagation(); // Prevent row click 4280 const contactId = button.dataset.id; 4281 if (confirm(t('Sind Sie sicher, dass Sie diesen Kontakt löschen möchten?'))) { 4282 await this.deleteContact(contactId); 4283 } 4284 }); 4285 }); 4286 4287 // Add pagination event listeners 4288 document.addEventListener('click', (e) => { 4289 if (e.target.closest('.pagination')) { 4290 e.preventDefault(); 4291 const pageLink = e.target.closest('.page-link'); 4292 if (pageLink) { 4293 const newPage = parseInt(pageLink.dataset.page); 4294 if (!isNaN(newPage) && newPage !== this.currentPage) { 4295 this.currentPage = newPage; 4296 this.applyFilters(); // This will trigger a re-render with the new page 4297 } 4298 } 4299 } 4300 }); 4301 4302 // Mark events as bound 4303 this.eventsBound = true; 4304 4305 // â FEHLENDER EVENT-LISTENER FÃR EDIT-CONTACT-MODAL HINZUFÃGEN
4306 const editContactForm = document.getElementById('editContactForm'); 4307 if (editContactForm) { 4308 editContactForm.addEventListener('submit', async (e) => { 4309 e.preventDefault(); 4310 // console.log('Edit contact form submitted'); 4311 4312 try { 4313 const contactId = document.getElementById('editContactId').value; 4314 if (!contactId) { 4315 throw new Error('Keine Kontakt-ID gefunden'); 4316 } 4317 4318 // Sammle alle Formulardaten 4319 const formData = new FormData(editContactForm); 4320 const contactData = {}; 4321 4322 // Konvertiere FormData zu Objekt 4323 formData.forEach((value, key) => { 4324 contactData[key] = value; 4325 }); 4326 4327 // console.log('Updating contact with data:', contactData); 4328 4329 // Rufe die Update-Funktion auf 4330 await this.updateContact(contactId, contactData); 4331 4332 // SchlieÃe das Modal 4333 const modal = bootstrap.Modal.getInstance(document.getElementById('editContactModal')); 4334 if (modal) { 4335 modal.hide(); 4336 } 4337 4338 // Aktualisiere die Kontaktliste 4339 await this.loadContacts(); 4340 4341 UI.showSuccess('Kontakt erfolgreich aktualisiert'); 4342 4343 } catch (error) { 4344 console.error('Error updating contact:', error); 4345 UI.showError('Fehler beim Aktualisieren des Kontakts: ' + error.message); 4346 } 4347 }); 4348 } 4349 } 4350 4351 updateBulkActionsVisibility() { 4352 const checkedBoxes = document.querySelectorAll('.contact-checkbox:checked'); 4353 const bulkActionsSection = document.querySelector('.bulk-actions-container'); // Klasse verwenden statt ID 4354 4355 if (bulkActionsSection) { 4356 if (checkedBoxes.length > 0) { 4357 bulkActionsSection.style.display = 'block'; 4358 } else { 4359 bulkActionsSection.style.display = 'none'; 4360 } 4361 } 4362 4363 // Mobile Bulk Actions Bar 4364 const mobileBulkActions = document.getElementById('mobileBulkActions'); 4365 if (mobileBulkActions) { 4366 if (checkedBoxes.length > 0) { 4367 mobileBulkActions.classList.add('show'); 4368 const mobileSelectedCount = document.getElementById('mobileSelectedCount'); 4369 if (mobileSelectedCount) { 4370 mobileSelectedCount.textContent = checkedBoxes.length; 4371 } 4372 } else { 4373 mobileBulkActions.classList.remove('show'); 4374 } 4375 } 4376 4377 // Mobile contact cards selection state 4378 document.querySelectorAll('.mobile-contact-card').forEach(card => { 4379 const checkbox = card.querySelector('.contact-checkbox'); 4380 if (checkbox && checkbox.checked) { 4381 card.classList.add('selected'); 4382 } else { 4383 card.classList.remove('selected'); 4384 } 4385 }); 4386 4387 // Aktualisiere den Zustand des mobilen "Alle auswählen"-Buttons 4388 const selectAllMobileBtn = document.getElementById('selectAllMobile'); 4389 if (selectAllMobileBtn) { 4390 const allCheckboxes = document.querySelectorAll('.contact-checkbox'); 4391 const allSelected = checkedBoxes.length === allCheckboxes.length && allCheckboxes.length > 0; 4392 if (allSelected) { 4393 selectAllMobileBtn.innerHTML = `<i class="fas fa-minus-square me-1"></i>${t('Alle abwählen')}`; 4394 } else { 4395 selectAllMobileBtn.innerHTML = `<i class="fas fa-check-square me-1"></i>${t('Alle auswählen')}`; 4396 } 4397 } 4398 } 4399 4400 removeEventListeners() { 4401 // Remove new contact button handler 4402 const newContactBtn = document.querySelector('#newContactBtn'); 4403 if (newContactBtn && this.newContactHandler) { 4404 newContactBtn.removeEventListener('click', this.newContactHandler); 4405 } 4406 4407 // Remove checkbox listeners 4408 const checkboxes = document.querySelectorAll('.contact-checkbox'); 4409 if (this.checkboxHandler) {
4410 checkboxes.forEach(checkbox => { 4411 checkbox.removeEventListener('change', this.checkboxHandler); 4412 }); 4413 } 4414 4415 // Remove select all listener 4416 const selectAll = document.getElementById('selectAll'); 4417 if (selectAll && this.selectAllHandler) { 4418 selectAll.removeEventListener('change', this.selectAllHandler); 4419 } 4420 4421 // Remove bulk action listeners 4422 const bulkUpdateBtn = document.querySelector('#bulkUpdateBtn'); 4423 if (bulkUpdateBtn && this.bulkUpdateHandler) { 4424 bulkUpdateBtn.removeEventListener('click', this.bulkUpdateHandler); 4425 } 4426 4427 const bulkDeleteBtn = document.querySelector('#bulkDeleteBtn'); 4428 if (bulkDeleteBtn && this.bulkDeleteHandler) { 4429 bulkDeleteBtn.removeEventListener('click', this.bulkDeleteHandler); 4430 } 4431 4432 // Remove clear search handler 4433 const clearSearchBtn = document.querySelector('#clearSearchBtn'); 4434 if (clearSearchBtn && this.clearSearchHandler) { 4435 clearSearchBtn.removeEventListener('click', this.clearSearchHandler); 4436 } 4437 4438 // Remove row click handlers 4439 const contactRows = document.querySelectorAll('.contact-row'); 4440 contactRows.forEach(row => { 4441 row.removeEventListener('click', this.rowClickHandler); 4442 }); 4443 4444 // Remove delete button handlers 4445 const deleteButtons = document.querySelectorAll('.delete-contact'); 4446 deleteButtons.forEach(button => { 4447 button.removeEventListener('click', this.deleteHandler); 4448 }); 4449 4450 // Remove group email button handler 4451 const sendGroupEmailBtn = document.getElementById('sendGroupEmailBtn'); 4452 if (sendGroupEmailBtn && this.sendGroupEmailHandler) { 4453 sendGroupEmailBtn.removeEventListener('click', this.sendGroupEmailHandler); 4454 } 4455 } 4456 4457 async createNewContact() { 4458 const form = document.getElementById('addContactForm'); 4459 if (!form) return; 4460 4461 if (!form.checkValidity()) { 4462 form.classList.add('was-validated'); 4463 return; 4464 } 4465 4466 try { 4467 // Get form data using form elements 4468 const formData = new FormData(form); 4469 const contactData = { 4470 name: formData.get('name'), 4471 email: formData.get('email'), 4472 company: formData.get('company'), 4473 phone_number: formData.get('phone'), 4474 industry: formData.get('industry'), 4475 lead_status: formData.get('lead_status'), 4476 task: formData.get('aufgabe'), 4477 street: formData.get('street'), 4478 zip: formData.get('zip'), 4479 city: formData.get('city'), 4480 website: formData.get('website'), 4481 created_at: serverTimestamp(), 4482 updated_at: serverTimestamp(), 4483 customer_id: Math.floor(Math.random() * 10000000000).toString(), // Generate customer_id as string 4484 }; 4485 4486 // kundenfelder-v1: eigene Felder aus dem Anlage-Modal uebernehmen 4487 try { 4488 const cfDefs = this._cfDefs || await ladeKundenfelder(); 4489 const cfWerte = sammleKundenfelder(form, cfDefs); 4490 if (Object.keys(cfWerte).length) contactData.custom = cfWerte; 4491 } catch (e) { console.warn('Kundenfelder nicht uebernommen:', e); } 4492 4493 const user = auth.currentUser; 4494 if (!user) throw new Error('No user logged in'); 4495 4496 // Add user ID to contact data 4497 contactData.uid = user.uid; 4498 4499 const userDoc = await _phase8UserSnap(user.uid); /* phase8 */ 4500 if (!userDoc.exists()) throw new Error('User profile not found'); 4501 4502 const userData = userDoc.data(); 4503 const tenantId = userData.tenantID || userData.tenant_id; 4504 if (!tenantId) throw new Error('No tenant ID found for user'); 4505 4506 contactData.tenantID = tenantId.toString(); 4507 4508 await this.createContact(contactData); 4509 4510 // Hide modal first 4511 if (this.addContactModal) { 4512 this.addContactModal.hide(); 4513 } 4514 4515 // Wait for modal to finish hiding 4516 await new Promise(resolve => setTimeout(resolve, 150)); 4517 4518 // Reload contacts and show success message 4519 await this.loadContacts(); 4520 UI.showSuccess('Kontakt erfolgreich erstellt'); 4521 } catch (error) { 4522 console.error('Error creating contact:', error); 4523 UI.showError('Fehler beim Erstellen des Kontakts'); 4524 } 4525 } 4526 4527 async restoreContactOrder(originalOrder) { 4528 // Stelle die ursprüngliche Reihenfolge der Kontakte wieder her 4529 try { 4530 console.log('[restoreContactOrder] Stelle Reihenfolge wieder her für', originalOrder.length, 'Kontakte'); 4531 4532 // Erstelle eine Map für schnelles Lookup 4533 const orderMap = new Map(); 4534 originalOrder.forEach((id, index) => { 4535 orderMap.set(id, index); 4536 }); 4537 4538 // Sortiere die aktuell gefilterten Kontakte oder alle Kontakte 4539 const contactsToSort = this.currentFilteredContacts || this.contacts; 4540 4541 // Sortiere basierend auf der ursprünglichen Reihenfolge 4542 const sortedContacts = [...contactsToSort].sort((a, b) => { 4543 const aIndex = orderMap.get(a.id); 4544 const bIndex = orderMap.get(b.id); 4545 4546 // Wenn beide in der ursprünglichen Reihenfolge waren 4547 if (aIndex !== undefined && bIndex !== undefined) { 4548 return aIndex - bIndex; 4549 } 4550 4551 // Neue Kontakte (nicht in der ursprünglichen Reihenfolge) ans Ende 4552 if (aIndex === undefined && bIndex !== undefined) return 1; 4553 if (aIndex !== undefined && bIndex === undefined) return -1; 4554 4555 // Beide neu - behalte ihre aktuelle Reihenfolge 4556 return 0; 4557 }); 4558 4559 // Aktualisiere die entsprechende Liste 4560 if (this.currentFilteredContacts) { 4561 this.currentFilteredContacts = sortedContacts; 4562 } else { 4563 this.contacts = sortedContacts; 4564 } 4565 4566 // Rendere die Kontakte neu mit der wiederhergestellten Sortierung 4567 await this.renderContacts(this.currentFilteredContacts || this.contacts); 4568 console.log('[restoreContactOrder] Sortierung wiederhergestellt und gerendert'); 4569 4570 } catch (error) { 4571 console.error('[restoreContactOrder] Fehler beim Wiederherstellen der Sortierung:', error); 4572 } 4573 } 4574 4575 async handleBulkUpdate() { // Erwartet keine Argumente mehr 4576 // console.log('[handleBulkUpdate] Click detected and function called.'); // NEUER LOG GANZ AM ANFANG 4577 // console.log('[handleBulkUpdate] Funktion aufgerufen.'); // LOGGING 4578 try {
4579 const selectedContactIds = this.getSelectedContactIds(); // Holt IDs innerhalb der Funktion 4580 // console.log('[handleBulkUpdate] Ausgewählte IDs:', selectedContactIds); // LOGGING 4581 if (!selectedContactIds.length) { 4582 UI.showError('Keine Kontakte ausgewählt'); 4583 return; 4584 } 4585 4586 // Holt Werte aus dem DOM innerhalb der Funktion 4587 const leadStatusElement = document.getElementById('bulkLeadStatus'); 4588 const taskElement = document.getElementById('bulkAufgabe'); // Korrigierte ID 4589 const industryElement = document.getElementById('bulkIndustry'); 4590 4591 const leadStatus = leadStatusElement ? leadStatusElement.value : null; 4592 const task = taskElement ? taskElement.value : null; 4593 const industry = industryElement ? industryElement.value : null; 4594 // console.log('[handleBulkUpdate] Gesammelte Werte:', { leadStatus, task, industry }); // LOGGING 4595 4596 const updateData = {}; 4597 if (leadStatus) updateData.lead_status = leadStatus; 4598 if (task) updateData.task = task; 4599 if (industry) updateData.industry = industry; 4600 // console.log('[handleBulkUpdate] Update Data Objekt:', updateData); // LOGGING 4601 4602 if (Object.keys(updateData).length === 0) { 4603 // console.log('[handleBulkUpdate] Keine Ãnderungen ausgewählt.'); // LOGGING 4604 UI.showError('Bitte wählen Sie mindestens eine Ãnderung aus (Lead-Status, Aufgabe oder Branche).'); 4605 return; 4606 } 4607 4608 // Bestätigungsdialog 4609 const changes = Object.entries(updateData).map(([key, value]) => { 4610 const fieldName = key.replace('_', ' '); 4611 // Ãbersetze den Wert, falls es ein Lead-Status ist 4612 const translatedValue = key === 'lead_status' ? t(value) : value; 4613 return `${fieldName} ${t('zu')} '${translatedValue}'`; 4614 }).join(', '); 4615 // console.log('[handleBulkUpdate] Zeige Bestätigungsdialog...'); // LOGGING 4616 if (!confirm(`${t('Möchten Sie für')} ${selectedContactIds.length} ${t('Kontakte wirklich die folgenden Ãnderungen vornehmen')}: ${changes}?`)) { 4617 // console.log('[handleBulkUpdate] Bestätigungsdialog abgelehnt.'); // LOGGING 4618 return; // Abbruch, wenn nicht bestätigt 4619 } 4620 // console.log('[handleBulkUpdate] Bestätigt. Starte Batch-Verarbeitung...'); // LOGGING 4621 4622 // --- Zeige prominenten Ladeindikator --- 4623 const updateLoadingOverlay = document.createElement('div'); 4624 updateLoadingOverlay.id = 'bulkUpdateLoadingOverlay'; 4625 updateLoadingOverlay.style.cssText = ` 4626 position: fixed; 4627 top: 0; 4628 left: 0; 4629 width: 100%; 4630 height: 100%; 4631 background: rgba(0, 0, 0, 0.7); 4632 display: flex; 4633 justify-content: center; 4634 align-items: center; 4635 z-index: 99999; 4636 `; 4637 updateLoadingOverlay.innerHTML = ` 4638 <div style="background: white; padding: 30px; border-radius: 10px; text-align: center; box-shadow: 0 4px 20px rgba(0,0,0,0.3);"> 4639 <div class="spinner-border text-primary mb-3" style="width: 3rem; height: 3rem;" role="status"> 4640 <span class="visually-hidden">Wird geladen...</span> 4641 </div> 4642 <h5 id="bulkUpdateStatus">
4642Aktualisiere ${selectedContactIds.length} Kontakte...</h5> 4643 <div id="bulkUpdateProgress" style="margin-top: 10px; color: #666;"></div> 4644 </div> 4645 `; 4646 document.body.appendChild(updateLoadingOverlay); 4647 4648 // --- API-basierte Verarbeitung statt Firestore-Batch --- 4649 4650 const user = auth.currentUser; // From firebase-init.js 4651 if (!user) { 4652 throw new Error("User not authenticated for bulk update."); 4653 } 4654 const idToken = await user.getIdToken(); 4655 4656 let updatedCount = 0; 4657 let errorCount = 0; 4658 const errors = []; 4659 4660 // console.log('[handleBulkUpdate] Starte API-Aufrufe für alle Kontakte...'); 4661 4662 // Funktion zum Verarbeiten von Kontakten in Chunks 4663 const processInChunks = async (ids, chunkSize = 50) => { 4664 const results = []; 4665 for (let i = 0; i < ids.length; i += chunkSize) { 4666 const chunk = ids.slice(i, i + chunkSize); 4667 // console.log(`[handleBulkUpdate] Verarbeite Chunk ${Math.floor(i/chunkSize) + 1}/${Math.ceil(ids.length/chunkSize)} (${chunk.length} Kontakte)`); 4668 4669 // Update UI progress 4670 const progressElement = document.getElementById('bulkUpdateProgress'); 4671 if (progressElement) { 4672 progressElement.textContent = `Verarbeite... ${i + chunk.length} von ${ids.length}`; 4673 } 4674 4675 // Verarbeite diesen Chunk parallel 4676 const chunkPromises = chunk.map(async (contactId) => { 4677 if (!contactId) { 4678 console.warn('[handleBulkUpdate] Ungültige Kontakt-ID übersprungen:', contactId); 4679 return { status: 'rejected', reason: 'Invalid contact ID' }; 4680 } 4681 try { 4682 const response = await fetch(`/api/contacts/${contactId}`, { 4683 method: 'PUT', 4684 headers: { 4685 'Content-Type': 'application/json', 4686 'Authorization': `Bearer ${idToken}` 4687 }, 4688 body: JSON.stringify(updateData) 4689 }); 4690 if (!response.ok) { 4691 const errorData = await response.json().catch(() => ({ message: response.statusText })); 4692 throw new Error(`API Error ${response.status}: ${errorData.message || response.statusText}`); 4693 } 4694 return { status: 'fulfilled', contactId }; 4695 } catch (error) { 4696 console.error(`[handleBulkUpdate] Fehler beim API-Aufruf für Kontakt ${contactId}:`, error); 4697 return { status: 'rejected', contactId, reason: error.message }; 4698 } 4699 }); 4700 4701 // Warte auf diesen Chunk 4702 const chunkResults = await Promise.allSettled(chunkPromises); 4703 results.push(...chunkResults); 4704 4705 // Kleine Pause zwischen Chunks, um Server nicht zu überlasten 4706 if (i + chunkSize < ids.length) { 4707 await new Promise(resolve => setTimeout(resolve, 100)); 4708 } 4709 } 4710 return results; 4711 }; 4712 4713 // Verarbeite alle Kontakte in Chunks 4714 const results = await processInChunks(selectedContactIds); 4715 4716 // Verarbeite die Ergebnisse 4717 results.forEach((promiseResult, index) => { 4718 if (promiseResult.status === 'fulfilled') { 4719 const result = promiseResult.value; 4720 if (result.status === 'fulfilled') { 4721 updatedCount++; 4722 } else if (result.status === 'rejected') { 4723 errorCount++; 4724 errors.push(`${result.contactId}: ${result.reason}`); 4725 } 4726 } else { 4727 errorCount++; 4728 errors.push(`Kontakt ${selectedContactIds[index]}: ${promiseResult.reason}`); 4729 } 4730 }); 4731 4732 // Zeige Ergebnisse an (Ladeindikator bleibt während Refresh aktiv) 4733 if (updatedCount > 0) { 4734 UI.showSuccess(`${updatedCount} von ${selectedContactIds.length} Kontakten erfolgreich aktualisiert.`); 4735 // console.log(`[handleBulkUpdate] Erfolgreich aktualisiert: ${updatedCount}/${selectedContactIds.length}`); 4736 } 4737 if (errorCount > 0) { 4738 console.error('[handleBulkUpdate] Fehler bei folgenden Kontakten:', errors); 4739 UI.showError(`${errorCount} Kontakte konnten nicht aktualisiert werden. Siehe Konsole für Details.`); 4740 } 4741 4742 // Update Ladeindikator-Text für Refresh 4743 const statusElement = document.getElementById('bulkUpdateStatus'); 4744 if (statusElement) { 4745 statusElement.textContent = 'Aktualisiere Ansicht...'; 4746 } 4747 const progressElement = document.getElementById('bulkUpdateProgress'); 4748 if (progressElement) { 4749 progressElement.textContent = 'Lade aktualisierte Kontakte...'; 4750 } 4751 4752 // Speichere die aktuelle Sortierung vor dem Neuladen 4753 const currentContactOrder = this.currentFilteredContacts 4754 ? this.currentFilteredContacts.map(c => c.id) 4755 : this.contacts.map(c => c.id); 4756 console.log('[handleBulkUpdate] Aktuelle Sortierung gespeichert:', currentContactOrder.length, 'Kontakte'); 4757 4758 console.log('[handleBulkUpdate] Cache leeren und Kontakte neu laden...'); 4759 console.log('[handleBulkUpdate] Update Data war:', updateData); 4760 4761 // Kleine Verzögerung, damit Firebase die Ãnderungen propagieren kann 4762 console.log('[handleBulkUpdate] Warte 500ms für Firebase-Propagierung...'); 4763 await new Promise(resolve => setTimeout(resolve, 500)); 4764 4765 await this.clearContactsCache(); 4766 console.log('[handleBulkUpdate] Starte loadContacts mit forceRefresh=true'); 4767 await this.loadContacts(true); // Lädt alle Kontakte neu mit forceRefresh 4768 console.log('[handleBulkUpdate] Kontakte wurden neu geladen'); 4769 4770 // Wende die aktive Smart List erneut an, falls eine aktiv war 4771 if (this.activeSmartListId) { 4772 // console.log(`[handleBulkUpdate] Wende aktive Smart List (${this.activeSmartListId}) erneut an...`); 4773 await this.applySmartList(this.activeSmartListId); 4774 } else { 4775 console.log('[handleBulkUpdate] Keine aktive Smart List, zeige alle Kontakte.'); 4776 // Filter anwenden falls vorhanden 4777 await this.applyFilters(); 4778 console.log('[handleBulkUpdate] Filter angewendet'); 4779 } 4780 4781 // Stelle die ursprüngliche Sortierung wieder her 4782 if (currentContactOrder.length > 0) { 4783 console.log('[handleBulkUpdate] Stelle ursprüngliche Sortierung wieder her...'); 4784 await this.restoreContactOrder(currentContactOrder); 4785 } 4786 4787 this.deselectAllContacts(); 4788 this.updateBulkActionsVisibility(); 4789 this.updateSmartListsUI(); // Hinzugefügt: Smart-Listen UI wiederherstellen 4790 4791 // Entferne Ladeindikator erst jetzt, nachdem alles fertig ist 4792 const updateOverlay = document.getElementById('bulkUpdateLoadingOverlay'); 4793 if (updateOverlay) { 4794 updateOverlay.remove(); 4795 } 4796 4797 console.log('[handleBulkUpdate] Abgeschlossen. Rendering sollte aktualisiert sein.'); 4798 4799 } catch (error) { // Fängt Fehler beim Holen der IDs oder Werte ab 4800 console.error('[handleBulkUpdate] Unerwarteter Fehler in der Funktion:', error); 4801 UI.showError('Ein unerwarteter Fehler ist bei der MassenÃ
4801¤nderung aufgetreten.'); 4802 // Entferne Ladeindikator im Fehlerfall 4803 const errorOverlay = document.getElementById('bulkUpdateLoadingOverlay'); 4804 if (errorOverlay) { 4805 errorOverlay.remove(); 4806 } 4807 } 4808 } 4809 4810 async handleBulkDelete() { // Erwartet keine Argumente mehr 4811 // console.log('[handleBulkDelete] Funktion aufgerufen.'); // LOGGING 4812 try { // Füge try-catch hinzu 4813 const selectedContactIds = this.getSelectedContactIds(); // Holt IDs innerhalb der Funktion 4814 // console.log('[handleBulkDelete] Ausgewählte IDs:', selectedContactIds); // LOGGING 4815 if (!selectedContactIds.length) { 4816 UI.showError('Keine Kontakte zum Löschen ausgewählt'); 4817 return; 4818 } 4819 4820 // Bestätigungsdialog 4821 // console.log('[handleBulkDelete] Zeige Bestätigungsdialog...'); // LOGGING 4822 if (!confirm(`${t('Sind Sie absolut sicher, dass Sie')} ${selectedContactIds.length} ${t('Kontakte unwiderruflich löschen möchten?')}`)) { 4823 // console.log('[handleBulkDelete] Löschen abgebrochen.'); // LOGGING 4824 return; 4825 } 4826 // console.log('[handleBulkDelete] Bestätigt. Starte Batch-Verarbeitung...'); // LOGGING 4827 4828 // --- Zeige prominenten Ladeindikator für Delete --- 4829 const deleteLoadingOverlay = document.createElement('div'); 4830 deleteLoadingOverlay.id = 'bulkDeleteLoadingOverlay'; 4831 deleteLoadingOverlay.style.cssText = ` 4832 position: fixed; 4833 top: 0; 4834 left: 0; 4835 width: 100%; 4836 height: 100%; 4837 background: rgba(0, 0, 0, 0.7); 4838 display: flex; 4839 justify-content: center; 4840 align-items: center; 4841 z-index: 99999; 4842 `; 4843 deleteLoadingOverlay.innerHTML = ` 4844 <div style="background: white; padding: 30px; border-radius: 10px; text-align: center; box-shadow: 0 4px 20px rgba(0,0,0,0.3);"> 4845 <div class="spinner-border text-danger mb-3" style="width: 3rem; height: 3rem;" role="status"> 4846 <span class="visually-hidden">Wird geladen...</span> 4847 </div> 4848 <h5 id="bulkDeleteStatus">Lösche ${selectedContactIds.length} Kontakte...</h5> 4849 <div id="bulkDeleteProgress" style="margin-top: 10px; color: #666;"></div> 4850 </div> 4851 `; 4852 document.body.appendChild(deleteLoadingOverlay); 4853 4854 // Welle 4: Backend-API statt direkter Firestore-Batch (siehe routes/contacts.py:bulk_delete_contacts) 4855 let deletedCount = 0; 4856 let errorCount = 0; 4857 const validIds = selectedContactIds.filter(id => !!id); 4858 errorCount = selectedContactIds.length - validIds.length; 4859 try { 4860 const idToken = await auth.currentUser.getIdToken(); 4861 const CHUNK_SIZE = 500; 4862 for (let i = 0; i < validIds.length; i += CHUNK_SIZE) { 4863 const chunk = validIds.slice(i, i + CHUNK_SIZE); 4864 const resp = await fetch('/api/contacts/bulk-delete', { 4865 method: 'POST', 4866 headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${idToken}` }, 4867 body: JSON.stringify({ contact_ids: chunk }), 4868 }); 4869 if (!resp.ok) { 4870 const text = await resp.text().catch(() => `HTTP ${resp.status}`); 4871 throw new Error(text || `HTTP ${resp.status}`); 4872 } 4873 const data = await resp.json(); 4874 deletedCount += data.deleted || 0; 4875 errorCount += (data.errors || []).length + (data.forbidden || 0); 4876 } 4877 } catch (error) { 4878 console.error('[handleBulkDelete] API error:', error); 4879 UI.showError(`Fehler beim Löschen der Kontakte: ${error.message}`); 4880 errorCount += (validIds.length - deletedCount); 4881 UI.hideLoading(); 4882 await this.loadContacts(); 4883 this.deselectAllContacts(); 4884 this.updateBulkActionsVisibility(); 4885 this.updateSmartListsUI(); 4886 return; 4887 } 4888 4889 // Zeige Ergebnisse an (Ladeindikator bleibt während Refresh aktiv) 4890 if (deletedCount > 0) { 4891 UI.showSuccess(`${deletedCount} von ${selectedContactIds.length} Kontakten erfolgreich gelöscht.`); 4892 } 4893 if (errorCount > 0) { 4894 const invalidIdErrors = selectedContactIds.filter(id => !id).length; 4895 const commitErrors = errorCount - invalidIdErrors; 4896 let errorMsg = ''; 4897 if (invalidIdErrors > 0) errorMsg += `${invalidIdErrors} ungültige Kontakt-IDs übersprungen. `; 4898 if (commitErrors > 0) errorMsg += `${commitErrors} Kontakte konnten aufgrund eines Fehlers nicht gelöscht werden.`; 4899 UI.showError(errorMsg.trim()); 4900 } 4901 4902 // Update Ladeindikator-Text für Refresh 4903 const deleteStatusElement = document.getElementById('bulkDeleteStatus'); 4904 if (deleteStatusElement) { 4905 deleteStatusElement.textContent = 'Aktualisiere Ansicht...'; 4906 } 4907 const deleteProgressElement = document.getElementById('bulkDeleteProgress'); 4908 if (deleteProgressElement) { 4909 deleteProgressElement.textContent = 'Lade aktualisierte Kontaktliste...'; 4910 } 4911 4912 // Speichere die aktuelle Sortierung vor dem Neuladen (ohne die gelöschten Kontakte) 4913 const deletedIds = new Set(selectedContactIds); 4914 const currentContactOrder = (this.currentFilteredContacts || this.contacts) 4915 .filter(c => !deletedIds.has(c.id)) 4916 .map(c => c.id); 4917 console.log('[handleBulkDelete] Aktuelle Sortierung gespeichert (ohne gel
4917öschte):', currentContactOrder.length, 'Kontakte'); 4918 4919 // console.log('[handleBulkDelete] Cache leeren und Kontakte neu laden...'); 4920 await this.clearContactsCache(); 4921 await this.loadContacts(true); // Lädt alle Kontakte neu mit forceRefresh 4922 4923 // Wende die aktive Smart List erneut an, falls eine aktiv war 4924 if (this.activeSmartListId) { 4925 // console.log(`[handleBulkDelete] Wende aktive Smart List (${this.activeSmartListId}) erneut an...`); 4926 await this.applySmartList(this.activeSmartListId); 4927 } else { 4928 // console.log('[handleBulkDelete] Keine aktive Smart List, zeige alle Kontakte.'); 4929 // Filter anwenden falls vorhanden 4930 await this.applyFilters(); 4931 } 4932 4933 // Stelle die ursprüngliche Sortierung wieder her 4934 if (currentContactOrder.length > 0) { 4935 console.log('[handleBulkDelete] Stelle ursprüngliche Sortierung wieder her...'); 4936 await this.restoreContactOrder(currentContactOrder); 4937 } 4938 4939 this.deselectAllContacts(); 4940 this.updateBulkActionsVisibility(); 4941 this.updateSmartListsUI(); // Hinzugefügt: Smart-Listen UI wiederherstellen 4942 4943 // Entferne Ladeindikator erst jetzt, nachdem alles fertig ist 4944 const deleteOverlay = document.getElementById('bulkDeleteLoadingOverlay'); 4945 if (deleteOverlay) { 4946 deleteOverlay.remove(); 4947 } 4948 4949 // console.log('[handleBulkDelete] Abgeschlossen.'); 4950 4951 } catch (error) { // Fängt Fehler beim Holen der IDs ab 4952 console.error('[handleBulkDelete] Unerwarteter Fehler in der Funktion:', error); 4953 UI.showError('Ein unerwarteter Fehler ist bei der Massenlöschung aufgetreten.'); 4954 // Entferne Ladeindikator im Fehlerfall 4955 const deleteErrorOverlay = document.getElementById('bulkDeleteLoadingOverlay'); 4956 if (deleteErrorOverlay) { 4957 deleteErrorOverlay.remove(); 4958 } 4959 } 4960 } 4961 4962 async deleteContact(id) { 4963 try { 4964 /* phase8c */ await ContactsApi.delete(id); 4965 // Clear contacts cache (both mobile and desktop versions) 4966 const user = auth.currentUser; 4967 if (user) { 4968 const userDoc = await _phase8UserSnap(user.uid); /* phase8 */ 4969 const userData = userDoc.data(); 4970 const tenantId = userData.tenantID || userData.tenant_id; 4971 if (tenantId) { 4972 // Clear both mobile and desktop cache keys 4973 const desktopCacheKey = `contacts_${user.uid}_${tenantId}_desktop`; 4974 const mobileCacheKey = `contacts_${user.uid}_${tenantId}_mobile`; 4975 const oldCacheKey = `contacts_${user.uid}_${tenantId}`; // Legacy cache key 4976 4977 localStorage.removeItem(desktopCacheKey); 4978 localStorage.removeItem(mobileCacheKey); 4979 localStorage.removeItem(oldCacheKey); 4980 4981 // Also clear last cache key 4982 const lastCacheKey = localStorage.getItem('last_contacts_cache_key'); 4983 if (lastCacheKey) { 4984 localStorage.removeItem(lastCacheKey); 4985 } 4986 } 4987 } 4988 await this.loadContacts(); 4989 UI.showSuccess('Kontakt wurde gelöscht'); 4990 } catch (error) { 4991 console.error('Error deleting contact:', error); 4992 UI.showError('Error deleting contact. Please try again.'); 4993 } 4994 } 4995 4996 async createContact(contactData) { 4997 try { 4998 /* phase8c */ const _newC_phase8c = await ContactsApi.create({...contactData}); const docRef = { id: _newC_phase8c.id }; 4999 5000 // Clear contacts cache (both mobile and desktop versions) 5001 const user = auth.currentUser; 5002 if (user) { 5003 const userDoc = await _phase8UserSnap(user.uid); /* phase8 */ 5004 const userData = userDoc.data(); 5005 const tenantId = userData.tenantID || userData.tenant_id; 5006 if (tenantId) { 5007 // Clear both mobile and desktop cache keys 5008 const desktopCacheKey = `contacts_${user.uid}_${tenantId}_desktop`; 5009 const mobileCacheKey = `contacts_${user.uid}_${tenantId}_mobile`; 5010 const oldCacheKey = `contacts_${user.uid}_${tenantId}`; // Legacy cache key 5011 5012 localStorage.removeItem(desktopCacheKey); 5013 localStorage.removeItem(mobileCacheKey); 5014 localStorage.removeItem(oldCacheKey); 5015 5016 // Also clear last cache key 5017 const lastCacheKey = localStorage.getItem('last_contacts_cache_key'); 5018 if (lastCacheKey) { 5019 localStorage.removeItem(lastCacheKey); 5020 } 5021 } 5022 } 5023 5024 return docRef.id; 5025 } catch (error) { 5026 console.error('Error creating contact:', error); 5027 throw error; 5028 } 5029 } 5030 5031 async updateContact(contactId, contactData) { 5032 try { 5033 /* phase8c */ await ContactsApi.update(contactId, {...contactData}); 5034 5035 // Clear contacts cache after successful update 5036 const user = auth.currentUser; 5037 if (user) { 5038 // Need to fetch userDoc again or ensure it's available 5039 // For simplicity, let's fetch it if not present on 'this' 5040 let tenantId = this.userDoc?.tenantID || this.userDoc?.tenant_id; 5041 if (!tenantId) { 5042 const userDocSnap = await _phase8UserSnap(user.uid); /* phase8 */ 5043 if (userDocSnap.exists()) { 5044 const userData = userDocSnap.data(); 5045 tenantId = userData.tenantID || userData.tenant_id; 5046 } 5047 } 5048 5049 if (tenantId) { 5050 const cacheKey = `contacts_${user.uid}_${tenantId}`; 5051 localStorage.removeItem(cacheKey);
5052 // console.log('Contacts cache cleared due to update for key:', cacheKey); 5053 // Zusätzlich den In-Memory Cache leeren 5054 this.contacts = []; 5055 this.currentFilteredContacts = null; 5056 // console.log('In-memory contacts array cleared.'); 5057 } else { 5058 console.warn('Could not clear contacts cache after update: Tenant ID not found.'); 5059 } 5060 } else { 5061 console.warn('Could not clear contacts cache after update: User not logged in.'); 5062 } 5063 5064 } catch (error) { 5065 console.error('Error updating contact:', error); 5066 throw error; 5067 } 5068 } 5069 5070 async updateLeadStatus(contactId, newStatus) { 5071 try { 5072 const user = auth.currentUser; // From firebase-init.js 5073 if (!user) { 5074 throw new Error("User not authenticated for updating lead status."); 5075 } 5076 const idToken = await user.getIdToken(); 5077 5078 const response = await fetch(`/api/contacts/${contactId}`, { 5079 method: 'PUT', 5080 headers: { 5081 'Content-Type': 'application/json', 5082 'Authorization': `Bearer ${idToken}` 5083 }, 5084 body: JSON.stringify({ lead_status: newStatus }) 5085 }); 5086 5087 if (!response.ok) { 5088 const errorData = await response.json().catch(() => ({ message: response.statusText })); 5089 throw new Error(`Failed to update lead status: ${errorData.message || response.statusText}`); 5090 } 5091 5092 // Optional: Die UI könnte hier direkt mit den Antwortdaten aktualisiert werden, 5093 // oder man verlässt sich auf ein Neuladen/Firestore-Listener. 5094 // const updatedContact = await response.json(); 5095 // console.log('Lead status updated via API for contact:', contactId); 5096 5097 // Die lokale Datenhaltung (this.contacts, this.currentFilteredContacts) 5098 // muss möglicherweise manuell aktualisiert oder neu geladen werden, 5099 // um die Ãnderungen in der UI sofort widerzuspiegeln, wenn man sich nicht auf 5100 // ein komplettes Neuladen der Kontaktliste verlässt. 5101 // Ein gängiges Muster ist es, this.loadContacts() oder eine ähnliche Methode aufzurufen, 5102 // nachdem eine solche Operation abgeschlossen ist, oder zumindest den betroffenen Kontakt 5103 // in den lokalen Arrays zu aktualisieren. 5104 5105 } catch (error) { 5106 console.error('Error updating lead status via API:', error); 5107 // Zeige dem Benutzer einen Fehler an, falls UI-Modul vorhanden 5108 if (typeof UI !== 'undefined' && UI.showError) { 5109 UI.showError(t('Fehler beim Aktualisieren des Lead-Status: ') + error.message); 5110 } 5111 throw error; // Fehler weiterwerfen, damit der Aufrufer ihn behandeln kann 5112 } 5113 } 5114 5115 async addConversation(contactId, conversationData) { 5116 if (!contactId || !conversationData) { 5117 console.error('Ungültige Eingabe für addConversation'); 5118 UI.showError('Ungültige Eingabe zum Hinzufügen des Gesprächs.'); 5119 return; 5120 } 5121 try { 5122 /* phase8c addConversation */ const _existing = (await ContactsApi.get(contactId).catch(()=>({}))).conversations || []; 5123 const _newConv = { ...conversationData, id: Date.now().toString(), createdAt: new Date().toISOString() }; 5124 await ContactsApi.update(contactId, { conversations: [..._existing, _newConv] }); 5125 // console.log('Gespräch hinzugefügt für Kontakt:', contactId); 5126 UI.showSuccess('Gespräch erfolgreich hinzugefügt'); 5127 // Optional: Lokale Daten aktualisieren oder neu laden 5128 } catch (error) { 5129 console.error('Error adding conversation:', error); 5130 UI.showError('Fehler beim Hinzufügen des Gesprächs: ' + error.message); 5131 throw error; 5132 } 5133 } 5134 5135 async deleteConversation(contactId, conversationToDelete) { 5136 if (!contactId || !conversationToDelete || !conversationToDelete.id) { // Prüfe auf conversationToDelete.id 5137 console.error('Ungültige Daten zum Löschen des Gesprächs (ID fehlt?).'); 5138 UI.showError('Ungültige Daten zum Löschen des Gesprächs.'); 5139 return; 5140 } 5141 try { 5142 /* phase8c deleteConversation */ 5143 const _cd_full = await ContactsApi.get(contactId).catch(()=>null); 5144 const contactSnap = { exists: () => !!_cd_full, data: () => _cd_full || {} }; 5145 if (contactSnap.exists()) { 5146 const conversations = (_cd_full && _cd_full.conversations) || []; 5147 const conversationToRemove = conversations.find(conv => conv.id === conversationToDelete.id); 5148 if (conversationToRemove) { 5149 await ContactsApi.update(contactId, { conversations: conversations.filter(c => c.id !== conversationToDelete.id) }); 5150 // console.log('Gespräch gelöscht für Kontakt:', contactId); 5151 UI.showSuccess('Gespräch erfolgreich gelöscht'); 5152 } else { 5153 console.warn('Zu löschendes Gespräch nicht im Array gefunden (ID:', conversationToDelete.id, ').'); 5154 UI.showWarning('Gespräch konnte nicht gefunden werden.') 5155 } 5156 } 5157 } catch (error) { 5158 console.error('Error deleting conversation:', error); 5159 UI.showError('Fehler beim Löschen des Gesprächs: ' + error.message); 5160 throw error; 5161 } 5162 } 5163 5164 async addNote(contactId, noteText) { // noteData in noteText geändert 5165 if (!contactId || !noteText || typeof noteText !== 'string' || noteText.trim() === '') { // Ãberprüfe noteText 5166 console.error('Ungültige Eingabe für addNote'); 5167 UI.showError('Notiz darf nicht leer sein.'); 5168 return; 5169 } 5170 try { 5171/* phase8c */ await ContactsApi.addNote(contactId, noteText); 5172 // console.log('Notiz hinzugefügt für Kontakt:', contactId); 5173 UI.showSuccess('Notiz erfolgreich hinzugefügt'); 5174 } catch (error) { 5175 console.error('Error adding note:', error); 5176 UI.showError('Fehler beim Hinzufügen der Notiz: ' + error.message); 5177 throw error; 5178 } 5179 } 5180 5181 async deleteNote(contactId, noteToDelete) { // noteId zu noteToDelete geändert 5182 if (!contactId || !noteToDelete || !noteToDelete.id) { // Prüfe auf noteToDelete.id 5183 console.error('Ungültige Daten zum Löschen der Notiz (ID fehlt?).'); 5184 UI.showError('Ungültige Daten zum Löschen der Notiz.'); 5185 return; 5186 } 5187 try { 5188 /* phase8c */ 5189 await ContactsApi.deleteNote(contactId, noteToDelete.id); 5190 const noteToRemove = noteToDelete; const contactSnap = { exists: () => true, data: () => ({ notes: [] }) }; 5191 if (contactSnap.exists()) { 5192 const noteToRemove2 = noteToRemove; 5193 if (noteToRemove2) { 5194 // console.log('Notiz gelöscht für Kontakt:', contactId); 5195 UI.showSuccess('Notiz erfolgreich gelöscht'); 5196 } else { 5197 console.warn('Zu löschende Notiz nicht im Array gefunden (ID:', noteToDelete.id, ').'); 5198 UI.showWarning('Notiz konnte nicht gefunden werden.'); 5199 } 5200 } 5201 } catch (error) { 5202 console.error('Error deleting note:', error); 5203 UI.showError('Fehler beim Löschen der Notiz: ' + error.message); 5204 throw error; 5205 } 5206 } 5207 5208 async getContact(contactId) { 5209 try { 5210 /* phase8c */ const _c_phase8c = await ContactsApi.get(contactId).catch(()=>null); const docSnap = { exists: () => !!_c_phase8c, data: () => _c_phase8c || {} }; 5211 5212 if (docSnap.exists()) { 5213 return { id: docSnap.id, ...docSnap.data() }; 5214 } else { 5215 throw new Error('Contact not found'); 5216 } 5217 } catch (error) { 5218 console.error('Error getting contact:', error); 5219 throw error; 5220 } 5221 } 5222 5223 async createTasksHandler(selectedTemplateId, selectedContacts) { 5224 const user = auth.currentUser; 5225 if (!user) { 5226 throw new Error('Benutzer nicht eingeloggt.'); 5227 } 5228 5229 // Get user's tenant ID 5230 const userDoc = await _phase8UserSnap(user.uid); /* phase8 */ 5231 if (!userDoc.exists()) { 5232 throw new Error('Benutzerprofil nicht gefunden.'); 5233 } 5234 5235 const userData = userDoc.data(); 5236 const tenantId = userData.tenantID || userData.tenant_id; 5237 if (!tenantId) { 5238 throw new Error('Keine Tenant ID gefunden.'); 5239 } 5240 5241 // Get the selected template 5242 const templateDoc = await getDoc(doc(db, 'multicall', selectedTemplateId)); 5243 if (!templateDoc.exists()) { 5244 throw new Error('Ausgewählte Vorlage nicht gefunden.'); 5245 } 5246 5247 const selectedTemplate = templateDoc.data(); 5248 5249 // Get ID token for API calls 5250 const idToken = await user.getIdToken(); 5251 const headers = { 5252 'Content-Type': 'application/json', 5253 'Authorization': `Bearer ${idToken}` 5254 }; 5255 5256 // Welle 4: Backend-API statt direkter Firestore-Batch (siehe routes/contacts.py:multicall_start) 5257 const validContacts = selectedContacts.filter(c => c && c.customer_id); 5258 const contactIds = validContacts.map(c => c.id); 5259 let multicallResp; 5260 try { 5261 const startResp = await fetch('/api/contacts/multicall-start', { 5262 method: 'POST', 5263 headers, 5264 body: JSON.stringify({ 5265 contact_ids: contactIds, 5266 template_id: selectedTemplateId, 5267 }), 5268 }); 5269 if (!startResp.ok) { 5270 const txt = await startResp.text().catch(() => `HTTP ${startResp.status}`); 5271 throw new Error(txt || `HTTP ${startResp.status}`); 5272 } 5273 multicallResp = await startResp.json(); 5274 console.log(`[multicall] processed=${multicallResp.processed} skipped=${multicallResp.skipped} errors=${(multicallResp.errors || []).length}`); 5275 } catch (err) { 5276 console.error('[multicall] backend error:', err); 5277 throw new Error('Fehler beim Anlegen der Multicall-Aufgaben: ' + (err.message || err)); 5278 } 5279 const taskByContactId = {}; 5280 for (const t of (multicallResp.tasks || [])) { 5281 taskByContactId[t.contact_id] = t.task_id; 5282 } 5283 const taskRefs = validContacts 5284 .filter(c => taskByContactId[c.id]) 5285 .map(c => ({ 5286 contact: c, 5287 taskRef: { id: taskByContactId[c.id] }, 5288 contactRef: { id: c.id }, 5289 })); 5290 5291 // Konfiguration für Twilio API-Limits
5292 const CONCURRENT_CALLS = 1; // Maximale Anzahl gleichzeitiger Anrufe 5293 const CALL_DELAY = 30000; // 30 Sekunden Pause zwischen den Anrufen 5294 5295 // Starte die Anrufe sequentiell 5296 const results = []; 5297 const failedCalls = []; 5298 const maxRetries = 5; 5299 5300 // Verarbeite die Anrufe sequentiell mit begrenzter Parallelität 5301 for (let i = 0; i < taskRefs.length; i += CONCURRENT_CALLS) { 5302 const currentBatch = taskRefs.slice(i, i + CONCURRENT_CALLS); 5303 // console.log(`Processing calls ${i + 1} to ${Math.min(i + CONCURRENT_CALLS, taskRefs.length)} of ${taskRefs.length}`); 5304 5305 // Starte maximal CONCURRENT_CALLS Anrufe parallel 5306 const batchPromises = currentBatch.map(async ({ contact, taskRef, contactRef }) => { 5307 try { 5308 const requestBody = { 5309 destinationNumber: contact.phone, 5310 task_id: taskRef.id, 5311 calendar_id: document.getElementById('multiCallCalendarSelect')?.value || "1" 5312 // agent_id und tenant_id werden vom Server aus dem Token ermittelt 5313 }; 5314 5315 // console.log('Sending request to /api/start_ultravox_out with body:', requestBody); 5316 5317 const response = await fetch('/api/start_ultravox_out', { 5318 method: 'POST', 5319 headers: headers, 5320 body: JSON.stringify(requestBody) 5321 }); 5322 5323 if (!response.ok) { 5324 const errorData = await response.json(); 5325 throw new Error(errorData.message || 'Fehler beim Starten des Anrufs'); 5326 } 5327 5328 return { status: 'fulfilled', value: await response.json() }; 5329 } catch (error) { 5330 console.error('Error making call:', error); 5331 failedCalls.push({ contact, taskRef, contactRef, retries: 0 }); 5332 return { status: 'rejected', reason: error }; 5333 } 5334 }); 5335 5336 // Warte auf alle Anrufe im aktuellen Batch 5337 const batchResults = await Promise.all(batchPromises); 5338 results.push(...batchResults); 5339 5340 // Warte CALL_DELAY Millisekunden vor dem nächsten Batch 5341 if (i + CONCURRENT_CALLS < taskRefs.length) { 5342 await new Promise(resolve => setTimeout(resolve, CALL_DELAY)); 5343 } 5344 } 5345 5346 // Wiederhole fehlgeschlagene Anrufe 5347 let currentRetry = 0; 5348 while (failedCalls.length > 0 && currentRetry < maxRetries) { 5349 currentRetry++; 5350 // console.log(`Retry attempt ${currentRetry} for ${failedCalls.length} failed calls`); 5351 5352 // Verarbeite die fehlgeschlagenen Anrufe auch mit begrenzter Parallelität 5353 for (let i = 0; i < failedCalls.length; i += CONCURRENT_CALLS) { 5354 const retryBatch = failedCalls.slice(i, i + CONCURRENT_CALLS); 5355 const retryPromises = retryBatch.map(async (failedCall) => { 5356 try { 5357 const requestBody = { 5358 destinationNumber: failedCall.contact.phone, 5359 task_id: failedCall.taskRef.id 5360 // agent_id und tenant_id werden vom Server aus dem Token ermittelt 5361 }; 5362 5363 // console.log(`Retry ${currentRetry} for call to ${failedCall.contact.phone}`); 5364 5365 const response = await fetch('/api/start_ultravox_out', { 5366 method: 'POST', 5367 headers: headers, 5368 body: JSON.stringify(requestBody) 5369 }); 5370 5371 if (!response.ok) { 5372 const errorData = await response.json(); 5373 throw new Error(errorData.message || 'Fehler beim Starten des Anrufs'); 5374 } 5375 5376 // Entferne erfolgreichen Anruf aus failedCalls 5377 const index = failedCalls.indexOf(failedCall); 5378 if (index > -1) { 5379 failedCalls.splice(index, 1); 5380 } 5381 5382 return { status: 'fulfilled', value: await response.json() }; 5383 } catch (error) { 5384 console.error(`Retry ${currentRetry} failed:`, error); 5385 failedCall.retries++; 5386 return { status: 'rejected', reason: error }; 5387 } 5388 }); 5389 5390 const retryResults = await Promise.all(retryPromises); 5391 results.push(...retryResults); 5392 5393 // Warte CALL_DELAY Millisekunden vor dem nächsten Batch 5394 if (i + CONCURRENT_CALLS < failedCalls.length) { 5395 await new Promise(resolve => setTimeout(resolve, CALL_DELAY)); 5396 } 5397 } 5398 5399 // Warte 10 Sekunden vor dem nächsten Retry-Versuch 5400 if (failedCalls.length > 0 && currentRetry < maxRetries) { 5401 await new Promise(resolve => setTimeout(resolve, 10000)); 5402 } 5403 } 5404 5405 // Zähle erfolgreiche und fehlgeschlagene Anrufe 5406 const successful = results.filter(r => r.status === 'fulfilled').length; 5407 const failed = results.filter(r => r.status === 'rejected').length; 5408 5409 UI.showSuccess(`${successful} Anrufe wurden erfolgreich gestartet. Die Anrufe werden mit maximal ${CONCURRENT_CALLS} gleichzeitigen Anrufen und ${CALL_DELAY/1000} Sekunden Pause ausgeführt. Es können maximal 50 Anrufe gleichzeit ausgeführt werden.`); 5410 } 5411 5412 // Ãbersetze Email Template Modal 5413 translateEmailTemplateModal() { 5414 const modal = document.getElementById('emailTemplateModal'); 5415 if (!modal) return; 5416 5417 // Ãbersetze Modal-Titel 5418 const modalTitle = modal.querySelector('.modal-title'); 5419 if (modalTitle) { 5420 const titleKey = modalTitle.getAttribute('data-i18n'); 5421 if (titleKey) modalTitle.textContent = t(titleKey); 5422 } 5423 5424 // Ãbersetze Labels 5425 const labels = modal.querySelectorAll('label[data-i18n]');
5426 labels.forEach(label => { 5427 const key = label.getAttribute('data-i18n'); 5428 if (key) label.textContent = t(key); 5429 }); 5430 5431 // Ãbersetze Buttons 5432 const buttons = modal.querySelectorAll('button[data-i18n]'); 5433 buttons.forEach(button => { 5434 const key = button.getAttribute('data-i18n'); 5435 if (key) button.textContent = t(key); 5436 }); 5437 } 5438 5439 async handleSendGroupEmail() { 5440 const selectedContacts = this.getSelectedContacts(); 5441 5442 if (selectedContacts.length === 0) { 5443 UI.showError(t('Bitte wählen Sie mindestens einen Kontakt aus.')); 5444 return; 5445 } 5446 5447 const button = document.getElementById('sendGroupEmailBtn'); 5448 const originalContent = button.innerHTML; 5449 button.innerHTML = `<i class="fas fa-spinner fa-spin me-1"></i>${t('Vorbereitung...')}`; 5450 button.disabled = true; 5451 5452 try { 5453 // Load email templates 5454 const templates = await this.loadEmailTemplates(); 5455 5456 // Show template selection modal 5457 const modalElement = document.getElementById('emailTemplateModal'); 5458 if (!modalElement) { 5459 throw new Error(t('Email Template Modal nicht gefunden.')); 5460 } 5461 5462 // Translate modal content 5463 this.translateEmailTemplateModal(); 5464 5465 // Ensure the modal is properly initialized with Bootstrap 5466 let modal = bootstrap.Modal.getInstance(modalElement); 5467 if (!modal) { 5468 modal = new bootstrap.Modal(modalElement); 5469 } 5470 5471 // Update template select 5472 const templateSelect = document.getElementById('emailTemplateSelect'); 5473 templateSelect.innerHTML = templates.map(template => 5474 `<option value="${template.id}">${template.name}</option>` 5475 ).join(''); 5476 5477 // Update selected contacts count 5478 const selectedCount = document.getElementById('selectedContactsCount'); 5479 if (selectedCount) { 5480 selectedCount.textContent = `${selectedContacts.length} ${t('Kontakte ausgewählt')}`; 5481 } 5482 5483 // Set up send button handler 5484 const sendButton = document.getElementById('sendGroupEmailConfirm'); 5485 if (sendButton) { 5486 // Clone the button to remove previous listeners 5487 const newSendButton = sendButton.cloneNode(true); 5488 sendButton.parentNode.replaceChild(newSendButton, sendButton); 5489 5490 // Add the new event listener 5491 newSendButton.addEventListener('click', async () => { 5492 const sendBtn = newSendButton; // Use the new button reference 5493 const btnContent = sendBtn.innerHTML; 5494 sendBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Sende...'; 5495 sendBtn.disabled = true; 5496 5497 let totalSent = 0; 5498 let totalFailed = 0; 5499 const allFailedEmails = []; 5500 const batchSize = 10; // Batch size set to 10 5501 5502 try { 5503 const templateId = templateSelect.value; 5504 if (!templateId) { 5505 throw new Error('Bitte wählen Sie eine E-Mail-Vorlage aus.'); 5506 } 5507 5508 const user = auth.currentUser; 5509 if (!user) { 5510 throw new Error('Benutzer nicht eingeloggt.'); 5511 } 5512 const idToken = await user.getIdToken(); // Get token once 5513 5514 // Process contacts in batches 5515 for (let i = 0; i < selectedContacts.length; i += batchSize) { 5516 const batchContacts = selectedContacts.slice(i, i + batchSize); 5517 // console.log(`Sending batch ${i / batchSize + 1}: ${batchContacts.length} contacts`); 5518 5519 // Prepare the email data for the current batch 5520 const emailData = { 5521 templateId: templateId, 5522 contacts: batchContacts.map(contact => ({
5523 id: contact.id, 5524 customer_id: contact.customer_id, 5525 email: contact.email, 5526 name: contact.name || '', 5527 phone: contact.phone, 5528 tenant_id: contact.tenant_id, 5529 website: contact.website || '', 5530 company: contact.company || '', 5531 street: contact.street || '', 5532 zip: contact.zip || '', 5533 city: contact.city || '' 5534 })), 5535 userId: user.uid 5536 }; 5537 5538 // Send the request to the server for the current batch 5539 const response = await fetch('/api/sendGroupEmail', { 5540 method: 'POST', 5541 headers: { 5542 'Content-Type': 'application/json', 5543 'Authorization': `Bearer ${idToken}` 5544 }, 5545 body: JSON.stringify(emailData) 5546 }); 5547 5548 if (!response.ok) { 5549 const errorData = await response.json().catch(() => ({ error: 'Serverfehler ohne Details' })); 5550 console.error(`Fehler bei Batch ${i / batchSize + 1}:`, errorData); 5551 // Assume all in batch failed if response is not ok 5552 totalFailed += batchContacts.length;
5553 batchContacts.forEach(c => allFailedEmails.push({ email: c.email, error: errorData.error || 'Serverfehler' })); 5554 // Optionally add a small delay before the next batch on error 5555 // await new Promise(resolve => setTimeout(resolve, 500)); 5556 continue; // Continue to the next batch 5557 } 5558 5559 const result = await response.json(); 5560 const batchSuccessCount = batchContacts.length - (result.failedEmails?.length || 0); 5561 totalSent += batchSuccessCount; 5562 if (result.failedEmails && result.failedEmails.length > 0) { 5563 totalFailed += result.failedEmails.length; 5564 allFailedEmails.push(...result.failedEmails); 5565 } 5566 // console.log(`Batch ${i / batchSize + 1} result: ${batchSuccessCount} sent, ${result.failedEmails?.length || 0} failed.`); 5567 5568 // Optional: Add a small delay between batches 5569 // await new Promise(resolve => setTimeout(resolve, 200)); 5570 } 5571 5572 // Close modal and show summary message 5573 modal.hide(); 5574 if (totalFailed === 0) { 5575 UI.showSuccess(`E-Mails wurden erfolgreich an ${totalSent} Kontakte gesendet.`); 5576 } else { 5577 const errors = allFailedEmails.map(fail => 5578 `${fail.email}: ${fail.error}` 5579 ).join('\n'); // Corrected join with '\n' 5580 UI.showWarning(`${totalSent} E-Mails gesendet, ${totalFailed} fehlgeschlagen.\nDetails:\n${errors}`); // Corrected template literal for UI.showWarning 5581 } 5582 5583 } catch (error) { 5584 console.error('Error sending group email batches:', error); 5585 UI.showError('Fehler beim Senden der E-Mails: ' + error.message); 5586 } finally { 5587 sendBtn.innerHTML = btnContent; 5588 sendBtn.disabled = false; 5589 } 5590 }); 5591 } 5592 5593 modal.show(); 5594 } catch (error) { 5595 console.error('Error preparing group email:', error); 5596 UI.showError('Fehler beim Vorbereiten der Gruppen-E-Mail: ' + error.message); 5597 } finally { 5598 button.innerHTML = originalContent; 5599 button.disabled = false; 5600 } 5601 } 5602 5603 async loadEmailTemplates() { 5604 try { 5605 const userId = auth.currentUser.uid; 5606 const templatesRef = collection(db, 'users', userId, 'emailTemplates'); 5607 const querySnapshot = await getDocs(templatesRef); 5608 5609 const templates = []; 5610 querySnapshot.forEach(doc => { 5611 templates.push({ id: doc.id, ...doc.data() }); 5612 }); 5613 5614 return templates; 5615 } catch (error) { 5616 console.error('Fehler beim Laden der E-Mail-Vorlagen:', error); 5617 return []; 5618 } 5619 } 5620 5621 getSelectedContacts() { 5622 const checkboxes = document.querySelectorAll('input[name="contactSelect"]:checked'); 5623 const selectedContacts = Array.from(checkboxes).map(checkbox => { 5624 const contactId = checkbox.value; 5625 const contact = this.contacts.find(contact => contact.id === contactId); 5626 if (!contact) { 5627 console.warn(`Contact not found for ID: ${contactId}`); 5628 return null; 5629 } 5630 5631 return { 5632 id: contact.id, 5633 customer_id: contact.customer_id, 5634 email: contact.email, 5635 name: contact.name || '', 5636 phone: contact.phone_number, // Use phone_number field 5637 tenant_id: contact.tenantID || contact.tenant_id, 5638 website: contact.website || '', // Include website field 5639 company: contact.company || '', // Include company field 5640 street: contact.street || '', 5641 zip: contact.zip || '', 5642 city: contact.city || '' 5643 }; 5644 }).filter(contact => contact); 5645 5646 // console.log('Selected contacts:', selectedContacts); // Debug log 5647 return selectedContacts; 5648 } 5649 5650 async handleSofortAnruf() {
5651 const selectedContacts = this.getSelectedContacts(); 5652 5653 if (selectedContacts.length === 0) { 5654 UI.showError(t('Bitte wählen Sie mindestens einen Kontakt aus.')); 5655 return; 5656 } 5657 5658 if (selectedContacts.length > 50) { 5659 UI.showError('Es können maximal 50 Kontakte auf einmal ausgewählt werden.'); 5660 return; 5661 } 5662 5663 // Prüfe, ob alle ausgewählten Kontakte eine Telefonnummer haben 5664 const contactsWithoutPhone = selectedContacts.filter(contact => !contact.phone); 5665 if (contactsWithoutPhone.length > 0) { 5666 UI.showError(`${contactsWithoutPhone.length} ausgewählte Kontakte haben keine Telefonnummer.`); 5667 return; 5668 } 5669 5670 try { 5671 UI.showLoading('Lade MultiCall Vorlagen...'); 5672 5673 const currentUser = auth.currentUser; 5674 if (!currentUser) { 5675 throw new Error('Nicht eingeloggt'); 5676 } 5677 5678 const idToken = await currentUser.getIdToken(); 5679 5680 const userDoc = await _phase8UserSnap(currentUser.uid); /* phase8 */ 5681 if (!userDoc.exists()) { 5682 throw new Error('Benutzerprofil nicht gefunden'); 5683 return; 5684 } 5685 5686 const userData = userDoc.data(); 5687 const tenantId = userData.tenantID || userData.tenant_id; 5688 if (!tenantId) { 5689 throw new Error('Keine Mandanten-ID gefunden'); 5690 return; 5691 } 5692 5693/* phase8c */ const templates = await _c8c_listMulticallTemplates(); 5694 5695 if (templates.length === 0) { 5696 UI.showError('Keine MultiCall Vorlagen gefunden.'); 5697 return; 5698 } 5699 5700 UI.hideLoading(); 5701 5702 // Update the modal HTML to include calendar selection 5703 const modalHtml = ` 5704 <div class="modal fade" id="sofortAnrufTemplateModal" tabindex="-1" aria-labelledby="sofortAnrufTemplateModalLabel" aria-hidden="true"> 5705 <div class="modal-dialog"> 5706 <div class="modal-content"> 5707 <div class="modal-header"> 5708 <h5 class="modal-title" id="sofortAnrufTemplateModalLabel">${t('Sofort Anruf - Vorlage auswählen')}</h5> 5709 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> 5710 </div> 5711 <div class="modal-body"> 5712 <div class="mb-3"> 5713 <label for="sofortAnrufTemplateSelect" class="form-label">${t('Vorlage')}</label> 5714 <select class="form-select" id="sofortAnrufTemplateSelect" required> 5715 </select> 5716 </div> 5717 <div class="mb-3"> 5718 <label for="sofortAnrufAgentSelect" class="form-label">${t('Agent')}</label> 5719 <select class="form-select" id="sofortAnrufAgentSelect"> 5720 <option value="">${t('Wird geladen...')}</option> 5721 </select> 5722 </div> 5723 <div class="mb-3"> 5724 <label for="sofortAnrufCalendarSelect" class="form-label">${t('Kalender')}</label> 5725 <select class="form-select" id="sofortAnrufCalendarSelect"> 5726 <option value="">${t('Kein Kalender')}</option> 5727 ${await this.getCalendarOptionsHtml()} 5728 </select> 5729 </div> 5730 <div id="selectedContactsCountSofortAnruf" class="text-muted small"></div> 5731 </div> 5732 <div class="modal-footer"> 5733 <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-i18n="Abbrechen">${t('Abbrechen')}</button> 5734 <button type="button" class="btn btn-warning" id="startSofortAnrufBtn" data-i18n="Sofort anrufen"><i class="fas fa-phone me-1"></i>${t('Sofort anrufen')}</button> 5735 </div> 5736 </div> 5737 </div> 5738 </div> 5739 `; 5740 5741 // Add modal to document if it doesn't exist 5742 if (!document.getElementById('sofortAnrufTemplateModal')) { 5743 document.body.insertAdjacentHTML('beforeend', modalHtml); 5744 } 5745 5746 const modalElement = document.getElementById('sofortAnrufTemplateModal'); 5747 const modal = new bootstrap.Modal(modalElement); 5748 5749 // Initialize modal before using it 5750 modal.show(); 5751 modal.hide(); 5752 5753 // Update template select 5754 const templateSelect = document.getElementById('sofortAnrufTemplateSelect'); 5755 templateSelect.innerHTML = templates.map(template => 5756 `<option value="${template.id}">${template.task || 'Unbenannte Vorlage'}</option>` 5757 ).join(''); 5758 5759 // Load agents for dropdown 5760 const agentSelect = document.getElementById('sofortAnrufAgentSelect'); 5761 try { 5762 /* phase8c */ const _ag = await _c8c_listAgents();
5762 const agentsSnap = { forEach: (cb) => _ag.forEach(d => cb({ id: d.id, data: () => d })) }; 5763 5764 const agents = []; 5765 agentsSnap.forEach(docSnap => { 5766 const data = docSnap.data(); 5767 if (data.name) { 5768 agents.push({ id: docSnap.id, name: data.name, selfHosted: !!data.selfHosted }); 5769 } 5770 }); 5771 5772 // Sort: selfHosted first, then alphabetically 5773 agents.sort((a, b) => { 5774 if (a.selfHosted && !b.selfHosted) return -1; 5775 if (!a.selfHosted && b.selfHosted) return 1; 5776 return (a.name || '').localeCompare(b.name || ''); 5777 }); 5778 5779 if (agentSelect) { 5780 agentSelect.innerHTML = agents.map(agent => 5781 `<option value="${agent.id}">${agent.name}${agent.selfHosted ? ' (Gemini)' : ''}</option>` 5782 ).join(''); 5783 } 5784 } catch (err) { 5785 console.error('Error loading agents for Sofort-Anruf:', err); 5786 if (agentSelect) { 5787 agentSelect.innerHTML = `<option value="">${t('Fehler beim Laden')}</option>`; 5788 } 5789 } 5790 5791 // Update selected contacts count 5792 const selectedCount = document.getElementById('selectedContactsCountSofortAnruf'); 5793 if (selectedCount) { 5794 selectedCount.textContent = `${selectedContacts.length} Kontakte ausgewählt`; 5795 } 5796 5797 // Set up send button handler 5798 const startSofortAnrufBtn = document.getElementById('startSofortAnrufBtn'); 5799 if (startSofortAnrufBtn) { 5800 const sendHandler = async () => { 5801 const sendBtn = document.getElementById('startSofortAnrufBtn'); 5802 if (!sendBtn) { 5803 console.error('startSofortAnrufBtn not found'); 5804 return; 5805 } 5806 const btnContent = sendBtn.innerHTML; 5807 sendBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Anrufe starten...'; 5808 sendBtn.disabled = true; 5809 5810 try { 5811 const templateId = templateSelect.value; 5812 if (!templateId) { 5813 throw new Error('Bitte wählen Sie eine Vorlage aus.'); 5814 } 5815 5816 const selectedTemplate = templates.find(t => t.id === templateId); 5817 5818 if (!selectedTemplate) { 5819 modal.hide(); 5820 UI.showError('Ausgewählte Vorlage nicht gefunden.'); 5821 sendBtn.innerHTML = btnContent; 5822 sendBtn.disabled = false; 5823 return; 5824 } 5825 5826 // console.log('Selected Template:', selectedTemplate); 5827 5828 // Headers for API calls 5829 const headers = { 5830 'Content-Type': 'application/json', 5831 'Authorization': `Bearer ${idToken}` 5832 }; 5833 // console.log('Request headers:', headers); 5834 5835 // phase8c sofort-anruf via ContactsApi 5836 const taskRefs = []; 5837 for (const contact of selectedContacts) { 5838 try { 5839 await ContactsApi.update(contact.id, { lead_status: 'Anrufen', task: selectedTemplate.task || '' }); 5840 const taskData = { 5841 attempt: 0, 5842 customer_id: parseInt(contact.customer_id), 5843 message: selectedTemplate.message, 5844 message2: selectedTemplate.message2, 5845 greeting: (selectedTemplate.greeting || '').replace('{name}', contact.name || ''), 5846 status: 'Erledigt', 5847 task: selectedTemplate.task || '', 5848 task_id: Date.now(), 5849 tenant_id: tenantId.toString(), 5850 uid: currentUser.uid, 5851 templateId: selectedTemplate.id, 5852 calendar_id: document.getElementById('sofortAnrufCalendarSelect')?.value || "1" 5853 }; 5854 const created = await ContactsApi.createTask(contact.id, taskData); 5855 taskRefs.push({ contact, taskRef: { id: created.id || created.task_id }
5855, contactRef: { id: contact.id } }); 5856 } catch (e) { console.error('phase8c sofort-anruf failed for ' + contact.id, e); } 5857 } 5858 5859 // Konfiguration für Ultravox API-Limits 5860 const CONCURRENT_CALLS = 1; // Maximale Anzahl gleichzeitiger Anrufe 5861 const CALL_DELAY = 30000; // 30 Sekunden Pause zwischen den Anrufen 5862 5863 // Starte die Anrufe sequentiell 5864 const results = []; 5865 const failedCalls = []; 5866 const maxRetries = 5; 5867 5868 // Verarbeite die Anrufe sequentiell mit begrenzter Parallelität 5869 for (let i = 0; i < taskRefs.length; i += CONCURRENT_CALLS) { 5870 const currentBatch = taskRefs.slice(i, i + CONCURRENT_CALLS); 5871 // console.log(`Processing calls ${i + 1} to ${Math.min(i + CONCURRENT_CALLS, taskRefs.length)} of ${taskRefs.length}`); 5872 5873 // Starte maximal CONCURRENT_CALLS Anrufe parallel 5874 const batchPromises = currentBatch.map(async ({ contact, taskRef, contactRef }) => { 5875 try { 5876 const selectedAgentId = document.getElementById('sofortAnrufAgentSelect')?.value || ''; 5877 const requestBody = { 5878 destinationNumber: contact.phone, 5879 task_id: taskRef.id, 5880 calendar_id: document.getElementById('sofortAnrufCalendarSelect')?.value || "1", 5881 ...(selectedAgentId && { agent_id: selectedAgentId }) 5882 }; 5883 5884 const response = await fetch('/api/start_ultravox_out', { 5885 method: 'POST', 5886 headers: headers, 5887 body: JSON.stringify(requestBody) 5888 }); 5889 5890 if (!response.ok) { 5891 const errorData = await response.json(); 5892 5893 // Spezielle Behandlung für "Keine verfügbare ausgehende Nummer" 5894 if (errorData.message && errorData.message.includes('Keine verfügbare ausgehende Nummer')) { 5895 throw new Error('Keine ausgehende Telefonnummer konfiguriert. Bitte kontaktieren Sie den Administrator.'); 5896 } 5897 5898 throw new Error(errorData.message || 'Fehler beim Starten des Anrufs'); 5899 } 5900 5901 // Anruf erfolgreich gestartet - jetzt den Lead-Status aktualisieren 5902 /* phase8c */ await ContactsApi.update(contact.id, { lead_status: 'Angerufen' }); 5903 5904 return { status: 'fulfilled', value: await response.json() }; 5905 } catch (error) { 5906 console.error('Error making call:', error); 5907 failedCalls.push({ contact, taskRef, contactRef, retries: 0 }); 5908 return { status: 'rejected', reason: error }; 5909 } 5910 }); 5911 5912 // Warte auf alle Anrufe im aktuellen Batch 5913 const batchResults = await Promise.all(batchPromises); 5914 results.push(...batchResults); 5915 5916 // Warte CALL_DELAY Millisekunden vor dem nächsten Batch 5917 if (i + CONCURRENT_CALLS < taskRefs.length) { 5918 await new Promise(resolve => setTimeout(resolve, CALL_DELAY)); 5919 } 5920 } 5921
5922 // Wiederhole fehlgeschlagene Anrufe 5923 let currentRetry = 0; 5924 while (failedCalls.length > 0 && currentRetry < maxRetries) { 5925 currentRetry++; 5926 // console.log(`Retry attempt ${currentRetry} for ${failedCalls.length} failed calls`); 5927 5928 // Verarbeite die fehlgeschlagenen Anrufe auch mit begrenzter Parallelität 5929 for (let i = 0; i < failedCalls.length; i += CONCURRENT_CALLS) { 5930 const retryBatch = failedCalls.slice(i, i + CONCURRENT_CALLS); 5931 const retryPromises = retryBatch.map(async (failedCall) => { 5932 try { 5933 const requestBody = { 5934 destinationNumber: failedCall.contact.phone, 5935 task_id: failedCall.taskRef.id 5936 // agent_id und tenant_id werden vom Server aus dem Token ermittelt 5937 }; 5938 5939 // console.log(`Retry ${currentRetry} for call to ${failedCall.contact.phone}`); 5940 5941 const response = await fetch('/api/start_ultravox_out', { 5942 method: 'POST', 5943 headers: headers, 5944 body: JSON.stringify(requestBody) 5945 }); 5946 5947 if (!response.ok) { 5948 const errorData = await response.json(); 5949 throw new Error(errorData.message || 'Fehler beim Starten des Anrufs'); 5950 } 5951 5952 // Anruf erfolgreich gestartet - jetzt den Lead-Status aktualisieren 5953 /* phase8c */ await ContactsApi.update(failedCall.contact.
5953id, { lead_status: 'Angerufen' }); 5954 5955 // Entferne erfolgreichen Anruf aus failedCalls 5956 const index = failedCalls.indexOf(failedCall); 5957 if (index > -1) { 5958 failedCalls.splice(index, 1); 5959 } 5960 5961 return { status: 'fulfilled', value: await response.json() }; 5962 } catch (error) { 5963 console.error(`Retry ${currentRetry} failed:`, error); 5964 failedCall.retries++; 5965 return { status: 'rejected', reason: error }; 5966 } 5967 }); 5968 5969 const retryResults = await Promise.all(retryPromises); 5970 results.push(...retryResults); 5971 5972 // Warte CALL_DELAY Millisekunden vor dem nächsten Batch 5973 if (i + CONCURRENT_CALLS < failedCalls.length) { 5974 await new Promise(resolve => setTimeout(resolve, CALL_DELAY)); 5975 } 5976 } 5977 5978 // Warte 10 Sekunden vor dem nächsten Retry-Versuch 5979 if (failedCalls.length > 0 && currentRetry < maxRetries) { 5980 await new Promise(resolve => setTimeout(resolve, 10000)); 5981 } 5982 } 5983 5984 // Zähle erfolgreiche und fehlgeschlagene Anrufe 5985 const successful = results.filter(r => r.status === 'fulfilled').length; 5986 const failed = results.filter(r => r.status === 'rejected').length; 5987 5988 modal.hide(); 5989 if (failed === 0) { 5990 UI.showSuccess(`${successful} Anrufe wurden erfolgreich gestartet. Die Anrufe werden mit maximal ${CONCURRENT_CALLS} gleichzeitigen Anrufen und ${CALL_DELAY/1000} Sekunden Pause ausgeführt.`); 5991 } else { 5992 const retriedMessage = currentRetry > 0 ? ` Nach ${currentRetry} Wiederholungsversuchen` : ''; 5993 UI.showWarning(`${successful} Anrufe erfolgreich gestartet, ${failed} fehlgeschlagen.${retriedMessage} Die Anrufe werden mit maximal ${CONCURRENT_CALLS} gleichzeitigen Anrufen und ${CALL_DELAY/1000} Sekunden Pause ausgeführt.`); 5994 } 5995 } catch (error) { 5996 console.error('Error starting calls:', error); 5997 UI.showError('Fehler beim Starten der Anrufe: ' + error.message); 5998 modal.hide(); // Stelle sicher, dass das Modal geschlossen wird 5999 } finally { 6000 // Stelle sicher, dass der Button wieder aktiviert wird 6001 const finalBtn = document.getElementById('startSofortAnrufBtn'); 6002 if (finalBtn) { 6003 finalBtn.innerHTML = btnContent || '<i class="fas fa-phone me-1"></i>Sofort anrufen'; 6004 finalBtn.disabled = false; 6005 } 6006 } 6007 }; 6008 6009 // Remove existing click listeners and add new one 6010 const newSendButton = startSofortAnrufBtn.cloneNode(true); 6011 startSofortAnrufBtn.parentNode.replaceChild(newSendButton, startSofortAnrufBtn); 6012 newSendButton.addEventListener('click', sendHandler); 6013 } 6014 6015 modal.show(); 6016 6017 } catch (error) { 6018 console.error('Error in handleSofortAnruf:', error); 6019 UI.showError('Fehler: ' + error.message); 6020 UI.hideLoading(); 6021 } 6022 } 6023 6024 async handleNewMultiCall() { 6025 // console.log('handleNewMultiCall() called'); // Debug log 6026 6027 // Check permissions first 6028 if (!this.hasMultiCallPermission()) { 6029 // console.log('MultiCall permission denied'); // Debug log 6030 UI.showError('Sie haben keine Berechtigung für MultiCall. Bitte wenden Sie sich an den Administrator.'); 6031 return; 6032 } 6033
6034 const selectedContacts = this.getSelectedContacts(); 6035 // console.log('Selected contacts:', selectedContacts.length); // Debug log 6036 6037 if (selectedContacts.length === 0) { 6038 UI.showError(t('Bitte wählen Sie mindestens einen Kontakt aus.')); 6039 return; 6040 } 6041 6042 const selectedContact = selectedContacts[0]; 6043 if (!selectedContact.phone) { 6044 UI.showError('Der ausgewählte Kontakt hat keine Telefonnummer.'); 6045 return; 6046 } 6047 6048 try { 6049 UI.showLoading('Lade MultiCall Vorlagen...'); 6050 6051 // Get current user's ID token 6052 const currentUser = auth.currentUser; 6053 // console.log('Current user:', currentUser?.email); 6054 6055 if (!currentUser) { 6056 throw new Error('Nicht eingeloggt'); 6057 } 6058 6059 // Get ID token for API calls 6060 // console.log('Getting ID token...'); 6061 const idToken = await currentUser.getIdToken(); 6062 // console.log('Got ID token:', idToken.substring(0, 20) + '...'); 6063 6064 // Get user's tenant ID 6065 const userDoc = await _phase8UserSnap(currentUser.uid); /* phase8 */ 6066 if (!userDoc.exists()) { 6067 console.error('User profile not found'); 6068 return; 6069 } 6070 6071 const userData = userDoc.data(); 6072 const tenantId = userData.tenantID || userData.tenant_id; 6073 if (!tenantId) { 6074 console.error('No tenant ID found for user'); 6075 return; 6076 } 6077 6078 // console.log('Using tenant ID:', tenantId); 6079/* phase8c */ const templates = await _c8c_listMulticallTemplates(); 6080 6081 if (templates.length === 0) { 6082 UI.showError('Keine MultiCall Vorlagen gefunden.'); 6083 return; 6084 } 6085 6086 UI.hideLoading(); 6087 6088 // Show template selection modal 6089 // console.log('Looking for multiCallTemplateModal...'); // Debug log 6090 const modalElement = document.getElementById('multiCallTemplateModal'); 6091 if (!modalElement) { 6092 console.error('MultiCall Template Modal element not found!'); // Debug log 6093 UI.showError('MultiCall Template Modal nicht gefunden.'); 6094 return; 6095 } 6096 // console.log('Modal element found, creating Bootstrap modal...'); // Debug log 6097 6098 const modal = new bootstrap.Modal(modalElement); 6099 6100 // Update template select 6101 // console.log('Updating template select...'); // Debug log 6102 const templateSelect = document.getElementById('multiCallTemplateSelect'); 6103 if (!templateSelect) { 6104 console.error('multiCallTemplateSelect not found!'); // Debug log 6105 UI.showError('Template Select Element nicht gefunden.'); 6106 return; 6107 } 6108 templateSelect.innerHTML = templates.map(template => 6109 `<option value="${template.id}">${template.task || 'Unbenannte Vorlage'}</option>` 6110 ).join(''); 6111 6112 // Update selected contacts count 6113 // console.log('Updating selected contacts count...'); // Debug log 6114 const selectedCount = document.getElementById('selectedContactsCountMultiCall'); 6115 if (selectedCount) { 6116 selectedCount.textContent = `${selectedContacts.length} Kontakte ausgewählt`; 6117 // console.log(`Set contact count to: ${selectedContacts.length}`); // Debug log 6118 } else { 6119 console.warn('selectedContactsCountMultiCall element not found'); // Debug log 6120 } 6121 6122 // Set up send button handler 6123 // console.log('Setting up button handler...'); // Debug log 6124 const createTasksBtn = document.getElementById('createMultiCallTasksBtn'); 6125 if (createTasksBtn) { 6126 // console.log('createMultiCallTasksBtn found, adding event listener'); // Debug log 6127 const sendHandler = async () => { 6128 const templateId = templateSelect.value; 6129 if (!templateId) { 6130 UI.showError('Bitte wählen Sie eine Vorlage aus.'); 6131 return; 6132 } 6133 6134 const selectedTemplate = templates.find(t => t.id === templateId); 6135 6136 if (!selectedTemplate) { 6137 modal.hide(); 6138 UI.showError('Ausgewählte Vorlage nicht gefunden.'); 6139 return; 6140 } 6141 6142 try { 6143 UI.showLoading('Erstelle Tasks...'); 6144 await this.createTasksHandler(templateId, selectedContacts); 6145 modal.hide(); 6146 UI.showSuccess('Tasks erfolgreich erstellt'); 6147 } catch (error) { 6148 console.error('Error creating tasks:', error); 6149 UI.showError('Fehler beim Erstellen der Tasks: ' + error.message); 6150 } finally { 6151 UI.hideLoading(); 6152 } 6153 }; 6154 6155 // Remove existing click listeners and add new one 6156 const newSendButton = createTasksBtn.cloneNode(true); 6157 createTasksBtn.parentNode.replaceChild(newSendButton, createTasksBtn); 6158 newSendButton.addEventListener('click', sendHandler); 6159 } else { 6160 console.error('createMultiCallTasksBtn not found!'); // Debug log 6161 UI.showError('MultiCall Button nicht gefunden.'); 6162 return; 6163 } 6164 6165 // console.log('Showing modal...'); // Debug log 6166 modal.show(); 6167 6168 } catch (error) { 6169 console.error('Error in handleNewMultiCall:', error); 6170 UI.showError('Fehler: ' + error.message); 6171 UI.hideLoading(); 6172 } 6173 } 6174 6175 6176 6177 async handleAutoCall() { 6178 // Ãffne das AutoCall Rules Modal 6179 await this.showAutoCallRulesModal(); 6180 } 6181 6182 async handleSmartListSubmit(e) { 6183 e.preventDefault(); 6184 6185 try { 6186 const user = auth.currentUser; 6187 if (!user) return; 6188 6189 const formData = new FormData(e.target); 6190 const name = formData.get('listName'); 6191 6192 if (!name) { 6193 UI.showError(t('Bitte geben Sie einen Namen für die Liste ein')); 6194 return; 6195 } 6196 6197 // Sammle alle Filter 6198 const filters = []; 6199 document.querySelectorAll('.filter-row').forEach(row => { 6200 const field = row.querySelector('.filter-field')?.value; 6201 const operator = row.querySelector('.filter-operator')?.value; 6202 const value = row.querySelector('.filter-value')?.value; 6203 6204 if (field && operator && value) { 6205 filters.push({ field, operator, value }); 6206 } 6207 }); 6208 6209 if (filters.length === 0) { 6210 UI.showError(t('Bitte fügen Sie mindestens einen Filter hinzu')); 6211 return; 6212 } 6213 6214/* phase8c */ await SmartListsApi.create({ name, filters }); 6215 6216 // Verstecke das Modal 6217 if (this.smartListModal) { 6218 this.smartListModal.hide(); 6219 } 6220 6221 // Warte kurz, bis das Modal versteckt ist 6222 await new Promise(resolve => setTimeout(resolve, 150)); 6223 6224 // Force rebinding of events 6225 this.eventsBound = false;
6226 this.smartListEventsBound = false; 6227 6228 // Aktualisiere die UI und binde Events neu 6229 await this.loadSmartLists(); 6230 await this.bindEvents(); 6231 this.setupContactEventListeners(); 6232 6233 UI.showSuccess(t('Smart-Liste erfolgreich erstellt')); 6234 6235 // Reset form 6236 e.target.reset(); 6237 const filterContainer = document.getElementById('filterContainer'); 6238 if (filterContainer) { 6239 filterContainer.innerHTML = ''; 6240 this.addFilterRow(); // Add one empty filter row 6241 } 6242 6243 } catch (error) { 6244 console.error('Error creating smart list:', error); 6245 UI.showError(t('Fehler beim Erstellen der Smart-Liste')); 6246 } 6247 } 6248 6249 addFilterRow() { 6250 const filterContainer = document.getElementById('filterContainer'); 6251 const template = filterContainer.querySelector('.filter-row')?.cloneNode(true); 6252 6253 if (template) { 6254 // Setze die Werte zurück 6255 template.querySelectorAll('select, input').forEach(el => { 6256 if (el.tagName === 'SELECT') { 6257 el.selectedIndex = 0; 6258 } else { 6259 el.value = ''; 6260 } 6261 }); 6262 6263 // Stelle sicher, dass die Ort-Option im Filter-Feld vorhanden ist 6264 const fieldSelect = template.querySelector('.filter-field'); 6265 if (fieldSelect && !fieldSelect.querySelector('option[value="city"]')) { 6266 const cityOption = document.createElement('option'); 6267 cityOption.value = 'city'; 6268 cityOption.textContent = t('Ort'); 6269 fieldSelect.appendChild(cityOption); 6270 } 6271 6272 const operatorSelect = template.querySelector('.filter-operator'); 6273 const valueContainer = template.querySelector('.filter-value-container'); 6274 6275 if (fieldSelect && operatorSelect && valueContainer) { 6276 fieldSelect.addEventListener('change', () => this.updateFilterOperators(fieldSelect, operatorSelect, valueContainer)); 6277 operatorSelect.addEventListener('change', () => this.updateFilterValueInput(fieldSelect, operatorSelect, valueContainer)); 6278 } 6279 6280 filterContainer.appendChild(template); 6281 6282 // Initialisiere die Operatoren für das ausgewählte Feld 6283 this.updateFilterOperators(fieldSelect, operatorSelect, valueContainer); 6284 } else { 6285 console.error('Filter template not found'); 6286 UI.showError('Fehler beim Hinzufügen eines neuen Filters'); 6287 } 6288 } 6289 6290 updateFilterOperators(fieldSelect, operatorSelect, valueContainer) { 6291 const dateFields = ['lastContact', 'createdAt', 'updatedAt']; 6292 const statusFields = ['status']; 6293 const industryFields = ['industry']; 6294 6295 // Verstecke alle Operatoren 6296 operatorSelect.querySelectorAll('option').forEach(option => { 6297 option.style.display = 'none'; 6298 }); 6299 6300 if (dateFields.includes(fieldSelect.value)) { 6301 // Zeige nur Datums-Operatoren 6302 operatorSelect.querySelectorAll('option[value="before"], option[value="after"], option[value="equals"]').forEach(option => { 6303 option.style.display = ''; 6304 }); 6305 operatorSelect.value = 'before'; 6306 } else if (statusFields.includes(fieldSelect.value)) { 6307 // Zeige nur Gleichheits-Operatoren 6308 operatorSelect.querySelectorAll('option[value="equals"], option[value="notEquals"]').forEach(option => { 6309 option.style.display = ''; 6310 }); 6311 operatorSelect.value = 'equals'; 6312 } else { 6313 // Zeige Standard-Operatoren 6314 operatorSelect.querySelectorAll('option:not([value="before"]):not([value="after"])').forEach(option => { 6315 option.style.display = ''; 6316 }); 6317 operatorSelect.value = 'contains'; 6318 } 6319 6320 // Aktualisiere das Wertefeld 6321 this.updateFilterValueInput(fieldSelect, operatorSelect, valueContainer); 6322 } 6323 6324 updateFilterValueInput(fieldSelect, operatorSelect, valueContainer) { 6325 const dateFields = ['lastContact', 'createdAt', 'updatedAt']; 6326 const statusFields = ['status']; 6327 const industryFields = ['industry']; 6328 6329 // Erstelle das passende Eingabefeld 6330 let input; 6331 if (dateFields.includes(fieldSelect.value)) { 6332 const inputGroup = document.createElement('div'); 6333 inputGroup.className = 'input-group date'; 6334 6335 input = document.createElement('input'); 6336 input.type = 'text'; 6337 input.className = 'form-control filter-value';
6338 input.required = true; 6339 input.readOnly = true; 6340 6341 const inputGroupAppend = document.createElement('span'); 6342 inputGroupAppend.className = 'input-group-text'; 6343 inputGroupAppend.innerHTML = '<i class="fas fa-calendar"></i>'; 6344 6345 inputGroup.appendChild(input); 6346 inputGroup.appendChild(inputGroupAppend); 6347 6348 // Ersetze das alte Eingabefeld 6349 const oldInput = valueContainer.querySelector('.filter-value, .input-group'); 6350 if (oldInput) { 6351 valueContainer.replaceChild(inputGroup, oldInput); 6352 } else { 6353 valueContainer.appendChild(inputGroup); 6354 } 6355 6356 // Initialisiere den Datepicker mit lokalisierter Konfiguration 6357 const dpConfig = getDatepickerConfig(); 6358 (window.loadLib ? window.loadLib('datepicker') : Promise.resolve()).then(() => $(input).datepicker({ 6359 format: dpConfig.format, 6360 language: dpConfig.language, 6361 autoclose: true, 6362 todayHighlight: true, 6363 clearBtn: true 6364 })); 6365 6366 return; 6367 } else if (statusFields.includes(fieldSelect.value)) { 6368 input = document.createElement('select'); 6369 input.className = 'form-select filter-value'; 6370 input.innerHTML = ` 6371 <option value="">Bitte wählen</option> 6372 ${(this.leadStatuses || DEFAULT_LEAD_STATUSES).map(status => 6373 `<option value="${status}">${t(status)}</option>` 6374 ).join('')} 6375 `; 6376 } else if (industryFields.includes(fieldSelect.value)) { 6377 input = document.createElement('select'); 6378 input.className = 'form-select filter-value'; 6379 input.innerHTML = ` 6380 <option value="">Bitte wählen</option> 6381 ${DEFAULT_INDUSTRIES.map(industry => 6382 `<option value="${industry}">${t(industry)}</option>` 6383 ).join('')} 6384 `; 6385 } else { 6386 input = document.createElement('input'); 6387 input.type = 'text'; 6388 input.className = 'form-control filter-value'; 6389 input.placeholder = 'Wert eingeben...'; 6390 } 6391 6392 // Ersetze das alte Eingabefeld 6393 const oldInput = valueContainer.querySelector('.filter-value, .input-group'); 6394 if (oldInput) { 6395 valueContainer.replaceChild(input, oldInput); 6396 } else { 6397 valueContainer.appendChild(input); 6398 } 6399 } 6400 6401 initializeFilterRow(row) { 6402 const fieldSelect = row.querySelector('.filter-field'); 6403 const operatorSelect = row.querySelector('.filter-operator'); 6404 const valueContainer = row.querySelector('.filter-value-container'); 6405 6406 // Stelle sicher, dass die Ort-Option im Feld-Dropdown vorhanden ist 6407 if (fieldSelect && !fieldSelect.querySelector('option[value="city"]')) { 6408 // Füge "Ort" zur Auswahl hinzu, nach "industry" Option 6409 const industryOption = fieldSelect.querySelector('option[value="industry"]'); 6410 if (industryOption) { 6411 const cityOption = document.createElement('option'); 6412 cityOption.value = 'city'; 6413 cityOption.textContent = 'Ort'; 6414 industryOption.insertAdjacentElement('afterend', cityOption); 6415 } else { 6416 // Falls "industry" nicht gefunden wird, füge einfach am Ende hinzu 6417 const cityOption = document.createElement('option'); 6418 cityOption.value = 'city'; 6419 cityOption.textContent = 'Ort'; 6420 fieldSelect.appendChild(cityOption); 6421 } 6422 } 6423 6424 if (fieldSelect && operatorSelect && valueContainer) { 6425 // Event-Listener für Ãnderungen am Feld-Typ 6426 fieldSelect.addEventListener('change', () => { 6427 this.updateFilterOperators(fieldSelect, operatorSelect, valueContainer); 6428 6429 // Sofort den richtigen Wert-Input initialisieren, damit die Branchenauswahl 6430 // korrekt erstellt wird, wenn "Branche" ausgewählt ist 6431 if (fieldSelect.value === 'industry') { 6432 const input = document.createElement('select'); 6433 input.className = 'form-select filter-value';
6434 input.innerHTML = ` 6435 <option value="">Bitte wählen</option> 6436 ${DEFAULT_INDUSTRIES.map(industry => 6437 `<option value="${industry}">${t(industry)}</option>` 6438 ).join('')} 6439 `; 6440 6441 // Ersetze das alte Eingabefeld 6442 const oldInput = valueContainer.querySelector('.filter-value, .input-group'); 6443 if (oldInput) { 6444 valueContainer.replaceChild(input, oldInput); 6445 } else { 6446 valueContainer.appendChild(input); 6447 } 6448 } 6449 }); 6450 6451 // Bestehender Event-Listener für Operator-Ãnderungen 6452 operatorSelect.addEventListener('change', () => this.updateFilterValueInput(fieldSelect, operatorSelect, valueContainer)); 6453 6454 // Initialisiere die Operatoren für das ausgewählte Feld 6455 this.updateFilterOperators(fieldSelect, operatorSelect, valueContainer); 6456 } 6457 } 6458 6459 async applySmartList(listId) { 6460 // console.log(`[applySmartList] Attempting to apply smart list with ID: ${listId}`); // DEBUG LOG 6461 try { 6462 UI.showLoading('Smart List wird geladen...'); 6463 6464 const user = auth.currentUser; 6465 if (!user) { 6466 console.error("[applySmartList] User not logged in."); // DEBUG LOG 6467 throw new Error('Benutzer nicht eingeloggt'); 6468 } 6469 6470 // Speichere die aktive Smart-Listen-ID im localStorage 6471 localStorage.setItem('activeSmartListId', listId); 6472 // console.log(`[applySmartList] Saved active smart list ID to localStorage: ${listId}`); 6473 6474 // Get the smart list document from the user's subcollection 6475 /* phase8c */ const _smData = await SmartListsApi.get(listId).catch(()=>null); const smartListDoc = { exists: () => !!_smData, data: () => _smData || {} }; 6476 if (!smartListDoc.exists()) { 6477 console.error(`[applySmartList] Smart list document not found for ID: ${listId}`); // DEBUG LOG 6478 throw new Error('Smart List nicht gefunden'); 6479 } 6480 6481 const smartList = { id: smartListDoc.id, ...smartListDoc.data() }; 6482 // console.log('[applySmartList] Applying smart list definition:', smartList); // DEBUG LOG 6483 6484 // Speichere die ID der aktiven Smart List INTERN 6485 this.activeSmartListId = listId; 6486 6487 // Filter contacts based on smart list criteria 6488 this.currentFilteredContacts = this.contacts.filter(contact => { 6489 // console.log(`\n[applySmartList] Checking contact ID: ${contact.id}, Name: ${contact.name}`); // DEBUG LOG 6490 return smartList.filters.every(filter => { 6491 // console.log(` [applySmartList] Applying filter - Field: ${filter.field}, Operator: ${filter.operator}, Value: "${filter.value}"`); // DEBUG LOG 6492 6493 // Get the correct field value based on field mapping 6494 let value; 6495 switch(filter.field) { 6496 case 'status': 6497 value = contact.lead_status; 6498 break; 6499 case 'updatedAt': 6500 value = contact.updated_at; 6501 break; 6502 case 'createdAt': 6503 value = contact.created_at; 6504 break; 6505 case 'industry': 6506 value = contact.industry; 6507 break; 6508 case 'city': 6509 value = contact.city; 6510 break; 6511 default: 6512 value = contact[filter.field]; 6513 } 6514 6515 // console.log(` [applySmartList] Contact value for field "${filter.field}":`, value); // DEBUG LOG 6516 6517 if (!value && value !== 0 && value !== false) { 6518 // console.log(' [applySmartList] -> Filter failed (value is missing/null/undefined)'); // DEBUG LOG 6519 return false;
6520 } 6521 6522 // Spezielle Behandlung für Datumsfelder 6523 if (['lastContact', 'createdAt', 'updatedAt'].includes(filter.field)) { 6524 if (!filter.value) { 6525 return false; 6526 } 6527 6528 // Verbesserte Datumsumwandlung für Firestore Timestamp 6529 let contactDate; 6530 if (value?.toDate) { 6531 // Firestore Timestamp 6532 contactDate = value.toDate(); 6533 // console.log('Converted from Firestore timestamp:', contactDate); 6534 } else if (value instanceof Date) { 6535 contactDate = value; 6536 } else if (typeof value === 'object' && value.seconds) { 6537 // Manuelle Umwandlung von Timestamp-ähnlichem Objekt 6538 contactDate = new Date(value.seconds * 1000); 6539 // console.log('Converted from seconds timestamp:', contactDate); 6540 } else if (typeof value === 'number') { 6541 contactDate = new Date(value); 6542 } else if (typeof value === 'string') { 6543 contactDate = new Date(value); 6544 } else { 6545 // console.log('Unbekannter Datumstyp:', typeof value); 6546 return false; 6547 } 6548 6549 // Filtern nach Datum 6550 // console.log('Contact date before parsing:', contactDate); 6551 6552 // Filter-Datum parsen - erwartet Format: DD.MM.YYYY 6553 const parts = filter.value.split('.'); 6554 if (parts.length !== 3) { 6555 // console.log('Invalid date format in filter:', filter.value); 6556 return false; 6557 } 6558 6559 const day = parseInt(parts[0], 10);
6560 const month = parseInt(parts[1], 10) - 1; // JS-Monate beginnen bei 0 6561 const year = parseInt(parts[2], 10); 6562 6563 if (isNaN(day) || isNaN(month) || isNaN(year)) { 6564 // console.log('Invalid date parts:', { day, month, year }); 6565 return false; 6566 } 6567 6568 const filterDate = new Date(year, month, day); 6569 // console.log('Filter date:', filterDate); 6570 6571 if (isNaN(contactDate.getTime()) || isNaN(filterDate.getTime())) { 6572 // console.log('Invalid date conversion'); 6573 return false; 6574 } 6575 6576 // Setze die Uhrzeiten auf Mitternacht für den reinen Datumsvergleich 6577 contactDate = new Date(contactDate.getFullYear(), contactDate.getMonth(), contactDate.getDate()); 6578 6579 // console.log('Comparing dates:', { 6580 // contactDate, 6581 // filterDate, 6582 // operator: filter.operator 6583 // }); 6584 6585 switch (filter.operator) { 6586 case 'equals': 6587 return contactDate.getTime() === filterDate.getTime(); 6588 case 'notEquals': 6589 return contactDate.getTime() !== filterDate.getTime(); 6590 case 'before': 6591 return contactDate.getTime() < filterDate.getTime(); 6592 case 'after': 6593 return contactDate.getTime() > filterDate.getTime(); 6594 case 'onOrBefore': 6595 return contactDate.getTime() <= filterDate.getTime(); 6596 case 'onOrAfter': 6597 return contactDate.getTime() >= filterDate.getTime(); 6598 default: 6599 return false; 6600 } 6601 } 6602 6603 // Behandlung für String-Felder 6604 const contactValue = String(value).toLowerCase(); 6605 const filterValue = String(filter.value).toLowerCase(); 6606 6607 // console.log(` [applySmartList] Comparing string values: "${contactValue}" ${filter.operator} "${filterValue}"`); // DEBUG LOG 6608 6609 let result = false; // DEBUG LOG 6610 switch (filter.operator) { 6611 case 'contains': 6612 result = contactValue.includes(filterValue); // DEBUG LOG 6613 break; // DEBUG LOG 6614 case 'equals': 6615 result = contactValue === filterValue; // DEBUG LOG 6616 break; // DEBUG LOG 6617 case 'notEquals': 6618 result = contactValue !== filterValue; // DEBUG LOG 6619 break; // DEBUG LOG 6620 case 'startsWith': 6621 result = contactValue.startsWith(filterValue); // DEBUG LOG 6622 break; // DEBUG LOG 6623 case 'endsWith': 6624 result = contactValue.endsWith(filterValue); // DEBUG LOG 6625 break; // DEBUG LOG 6626 default: 6627 result = false; // DEBUG LOG 6628 } 6629 // console.log(' [applySmartList] -> Filter result (string/other):', result); // DEBUG LOG 6630 return result; // DEBUG LOG 6631 }); 6632 }); 6633 6634 // console.log('[applySmartList] Filtered contacts count:', this.currentFilteredContacts.length); // DEBUG LOG 6635 // console.log('[applySmartList] Filtered contacts sample:', this.currentFilteredContacts.slice(0, 5)); // DEBUG LOG 6636 6637 // Aktualisiere die Kontaktliste 6638 // WICHTIG: renderContacts erstellt den Placeholder neu! 6639 await this.renderContacts(this.currentFilteredContacts); 6640 6641 // Binde Listener neu, NACHDEM die gefilterte Liste gerendert wurde 6642 this.setupContactEventListeners(); 6643 6644 // Stelle das Smart-Listen-Dropdown wieder her, da renderContacts es entfernt hat 6645 this.updateSmartListsUI(); 6646 6647 // Aktualisiere den Badge-Counter 6648 const clickedButton = document.querySelector(`button[data-list-id="${listId}"]`); 6649 const badge = clickedButton?.querySelector('.badge'); 6650 if (badge) { 6651 badge.textContent = this.currentFilteredContacts.length; 6652 } 6653 6654 // Zeige Feedback 6655 UI.showSuccess(`Smart List "${smartList.name}" wurde angewendet. ${this.currentFilteredContacts.length} Kontakte gefunden.`); 6656 6657 // Speichere den aktualisierten Zustand nach Smart List Anwendung 6658 this.storeContactsState(); 6659 6660 } catch (error) { 6661 console.error('[applySmartList] Error applying smart list:', error); // DEBUG LOG 6662 UI.showError('Fehler beim Anwenden der Smart List: ' + error.message); 6663 } finally { 6664 UI.hideLoading(); 6665 } 6666 } 6667 6668 async showNewContactModal() { 6669 // Initialize modal if not already done 6670 this.initializeModal(); 6671 6672 // Apply translations to modal elements 6673 const modalTitle = document.querySelector('#addContactModal .modal-title'); 6674 if (modalTitle) { 6675 modalTitle.textContent = t('Neuer Kontakt'); 6676 } 6677 6678 // Translate all labels 6679 const labels = document.querySelectorAll('#addContactModal label');
6680 labels.forEach(label => { 6681 const i18nKey = label.getAttribute('data-i18n'); 6682 if (i18nKey) { 6683 label.textContent = t(i18nKey); 6684 } 6685 }); 6686 6687 // Translate buttons in modal footer 6688 const modalFooterButtons = document.querySelectorAll('#addContactModal .modal-footer button'); 6689 modalFooterButtons.forEach(button => { 6690 const i18nKey = button.getAttribute('data-i18n'); 6691 if (i18nKey) { 6692 button.textContent = t(i18nKey); 6693 } 6694 }); 6695 6696 // Translate "Bitte wählen" options in all selects 6697 const brancheSelect = document.querySelector('#addContactModal select[name="industry"]'); 6698 if (brancheSelect) { 6699 const firstOption = brancheSelect.querySelector('option[value=""]'); 6700 if (firstOption) { 6701 firstOption.textContent = t('Bitte wählen'); 6702 } 6703 } 6704 6705 // Dynamically populate the aufgabe dropdown 6706 const aufgabeSelect = document.querySelector('#addContactModal select[name="aufgabe"]'); 6707 if (aufgabeSelect) { 6708 aufgabeSelect.innerHTML = `<option value="">${t('Bitte wählen')}</option>`; 6709 AUFGABE_OPTIONS.forEach(option => { 6710 const optionElement = document.createElement('option'); 6711 optionElement.value = option; 6712 optionElement.textContent = t(option); // Translate aufgabe option 6713 aufgabeSelect.appendChild(optionElement); 6714 }); 6715 } 6716 6717 // Dynamically populate the lead status dropdown 6718 const leadStatusSelect = document.querySelector('#addContactModal select[name="lead_status"]'); 6719 if (leadStatusSelect && this.leadStatuses && this.leadStatuses.length > 0) { 6720 leadStatusSelect.innerHTML = `<option value="">${t('Bitte wählen...')}</option>`; // Add default option with translation 6721 this.leadStatuses.forEach(status => { 6722 const option = document.createElement('option'); 6723 option.value = status; 6724 option.textContent = t(status); // Translate lead status 6725 leadStatusSelect.appendChild(option); 6726 }); 6727 } else if (leadStatusSelect) { 6728 // Fallback if leadStatuses are not loaded 6729 leadStatusSelect.innerHTML = `<option value="">${t('Laden...')}</option>`; 6730 console.warn("Lead statuses not loaded yet for add contact modal."); 6731 // Attempt to load them again if needed, or provide a default set 6732 } 6733 6734 // kundenfelder-v1: eigene Felder ins Anlage-Modal einblenden 6735 try { 6736 const cfDefs = await ladeKundenfelder(); 6737 this._cfDefs = cfDefs; 6738 const mb = document.querySelector('#addContactModal .modal-body'); 6739 if (mb) { 6740 let host = mb.querySelector('#addContactCustomFields'); 6741 if (!host) { 6742 host = document.createElement('div'); 6743 host.id = 'addContactCustomFields'; 6744 mb.appendChild(host); 6745 } 6746 host.innerHTML = cfDefs.length 6747 ? `<hr><p class="fw-bold small mb-2">${t('Eigene Felder')}</p><div class="row g-2">${renderKundenfelder(cfDefs, {}, { colClass: 'col-12' })}</div>` 6748 : ''; 6749 } 6750 } catch (e) { console.warn('Kundenfelder im Anlage-Modal nicht geladen:', e); } 6751 6752 // Show modal 6753 if (this.addContactModal) { 6754 this.addContactModal.show(); 6755 } 6756 } 6757 6758 getCallStatusEmoji(callSummary) { 6759 if (!callSummary) { 6760 return '<i class="fas fa-question text-secondary" style="font-size: 0.8em;"></i>'; 6761 } 6762 6763 // Anrufbeantworter check 6764 const isAnrufbeantworter = callSummary.anrufbeantworter === true; 6765 6766 // Prüfe zuerst Anrufbeantworter 6767 if (isAnrufbeantworter) { 6768 return '<i class="fas fa-meh text-warning" style="font-size: 1.2em;"></i>'; 6769 } 6770 6771 if (callSummary.call_successful === true && callSummary.duration >= 15) {
6772 // Suche nach laufenden Tasks für diesen Kontakt und setze sie auf "Erledigt" 6773 if (callSummary.contactId) { // Only proceed if we have a contact ID 6774 /* phase8c */ ContactsApi.listTasks(callSummary.contactId).then(tasks => { 6775 const running = (tasks || []).filter(t => (t.status || '') === 'Läuft' || (t.status || '').toLowerCase() === 'läuft'); 6776 return Promise.all(running.map(t => ContactsApi.updateTask(callSummary.contactId, t.id, { status: 'Erledigt' }))); 6777 }).catch(err => console.error('Fehler beim Aktualisieren der Tasks:', err)); 6778 } 6779 6780 return '<i class="fas fa-smile text-success" style="font-size: 1.2em;"></i>'; 6781 } 6782 6783 // Wenn der Anruf eine Dauer > 0 hat, aber nicht erfolgreich oder zu kurz war 6784 if (callSummary.duration > 0) { 6785 return '<i class="fas fa-frown text-info" style="font-size: 1.2em;"></i>'; 6786 } 6787 6788 return '<i class="fas fa-times-circle text-danger" style="font-size: 0.8em;"></i>'; 6789 } 6790 6791 getCallStatusTitle(callSummary) { 6792 if (!callSummary) { 6793 return t('Kein Anrufstatus verfügbar'); 6794 } 6795 6796 const isAnrufbeantworter = callSummary.anrufbeantworter === true; 6797 6798 // Prüfe zuerst Anrufbeantworter 6799 if (isAnrufbeantworter) { 6800 return t('Anrufbeantworter erreicht'); 6801 } 6802 6803 // Prüfe ob der Anruf erfolgreich UND lang genug war 6804 if (callSummary.call_successful === true && callSummary.duration >= 15) { 6805 return t('Anruf erfolgreich'); 6806 } 6807 6808 // Wenn der Anruf eine Dauer > 0 hat 6809 if (callSummary.duration > 0) { 6810 return t('Anruf nicht erfolgreich'); 6811 } 6812 6813 return t('Kein Kontakt'); 6814 } 6815 6816 getCallStatusColor(callSummary) { 6817 if (!callSummary) { 6818 return 'text-secondary'; 6819 } 6820 6821 const isAnrufbeantworter = callSummary.anrufbeantworter === true; 6822 6823 // Prüfe zuerst Anrufbeantworter 6824 if (isAnrufbeantworter) { 6825 return 'text-warning'; 6826 } 6827 6828 // Prüfe ob der Anruf erfolgreich UND lang genug war 6829 if (callSummary.call_successful === true && callSummary.duration >= 15) { 6830 return 'text-success'; 6831 } 6832 6833 // Wenn der Anruf eine Dauer > 0 hat 6834 if (callSummary.duration > 0) { 6835 return 'text-info'; 6836 } 6837 6838 return 'text-danger'; 6839 } 6840 6841 async getLatestCallSummary(contactId) { 6842 try { 6843 const user = auth.currentUser; 6844 if (!user) return null; 6845 6846 /* phase8c */ const _c = await ContactsApi.get(contactId).catch(()=>null); 6847 if (!_c) return null; 6848 const customerId = String(_c.customer_id || ''); 6849 const _csResp = await _c8c_authedFetch('/api/call-summary/list', { method: 'POST', body: JSON.stringify({ customer_id: customerId, limit: 1 }) }); 6850 if (!_csResp.ok) return null; 6851 const _csJ = await _csResp.json(); 6852 const _csList = _csJ.summaries || []; 6853 return _csList.length > 0 ? _csList[0] : null; 6854 6855 } catch (error) { 6856 console.error('Error getting latest call summary:', error); 6857 return null; 6858 } 6859 } 6860 6861 hasMultiCallPermission() { 6862 const user = auth.currentUser; 6863 if (!user) return false; 6864 6865 // Hole das User-Dokument aus dem Cache wenn möglich 6866 const userDoc = this.userDoc; 6867 if (!userDoc) return false; 6868 6869 // Prüfe ob multicall aktiviert ist 6870 return userDoc.multicall === true; 6871 } 6872 6873 hasPersonalAssistantPermission() { 6874 const user = auth.currentUser; 6875 if (!user) return false; 6876 6877 const userDoc = this.userDoc; 6878 if (!userDoc) return false; 6879 6880 return userDoc.personal_assistent === true; 6881 } 6882 6883 // Vollständige Implementierung der Kalenderoptions-Funktion 6884 async getCalendarOptionsHtml() { 6885 try { 6886 const user = auth.currentUser; 6887 if (!user) { 6888 // console.log('No user logged in'); 6889 return ''; // Gib einen leeren String zurück, wenn kein Benutzer angemeldet ist 6890 } 6891 6892 // Kalendertypen aus der Datenbank laden 6893 const tenantId = this.userDoc?.tenant_id || this.userDoc?.tenantId || window.currentTenantId; 6894 6895 if (!tenantId) { 6896 // console.log('No tenant ID available'); 6897 return ''; 6898 } 6899 6900 // phase8c calendar-types via API 6901 const _ct3 = await _c8c_getCalendarTypes();
6902 const querySnapshot = { empty: _ct3.length === 0, forEach: (cb) => _ct3.forEach(d => cb({ id: d.id || d.name, data: () => d })) }; 6903 let calendarOptions = ''; 6904 6905 // Erstelle die Optionen 6906 if (!querySnapshot.empty) { 6907 calendarOptions += '<option value="">Kalender auswählen...</option>'; 6908 6909 querySnapshot.forEach((doc) => { 6910 const calendarData = doc.data(); 6911 calendarOptions += `<option value="${doc.id}" data-color="${calendarData.color || '#3788d8'}">${calendarData.name}</option>`; 6912 }); 6913 } else { 6914 // Wenn keine Kalender gefunden, zeige einen Platzhalter 6915 calendarOptions += '<option value="">Kein Kalender verfügbar</option>'; 6916 } 6917 6918 return calendarOptions; 6919 } catch (error) { 6920 console.error('Error loading calendar options:', error); 6921 return '<option value="">Fehler beim Laden der Kalender</option>'; 6922 } 6923 } 6924 6925 // Ãbersetze alle Modals bei Sprachwechsel 6926 translateAllModals() { 6927 this.translateSmartListModal(); 6928 this.translateEmailTemplateModal(); 6929 this.updateAutoCallModalTexts(); // AutoCall Rules Modal übersetzen 6930 6931 // Ãbersetze auch das "Neuer Kontakt" Modal wenn es existiert 6932 const addContactModal = document.getElementById('addContactModal'); 6933 if (addContactModal) { 6934 // Modal-Titel 6935 const modalTitle = addContactModal.querySelector('.modal-title'); 6936 if (modalTitle) { 6937 const titleKey = modalTitle.getAttribute('data-i18n'); 6938 if (titleKey) modalTitle.textContent = t(titleKey); 6939 } 6940 6941 // Alle Labels 6942 const labels = addContactModal.querySelectorAll('label[data-i18n]'); 6943 labels.forEach(label => { 6944 const key = label.getAttribute('data-i18n'); 6945 if (key) label.textContent = t(key); 6946 }); 6947 6948 // Buttons 6949 const buttons = addContactModal.querySelectorAll('button[data-i18n]'); 6950 buttons.forEach(button => { 6951 const key = button.getAttribute('data-i18n'); 6952 if (key) button.textContent = t(key); 6953 }); 6954 } 6955 } 6956 6957 // Ãbersetze Smart List Modal 6958 translateSmartListModal() { 6959 console.log('translateSmartListModal aufgerufen - Version 2024-10-30-14:00'); 6960 const modal = document.getElementById('smartListModal'); 6961 if (!modal) { 6962 console.log('SmartList Modal nicht gefunden!'); 6963 return; 6964 } 6965 console.log('SmartList Modal gefunden, starte Ãbersetzung...'); 6966 6967 // Ãbersetze Modal-Titel 6968 const modalTitle = modal.querySelector('.modal-title'); 6969 if (modalTitle) { 6970 const titleKey = modalTitle.getAttribute('data-i18n'); 6971 console.log('Modal Title Key:', titleKey, '-> Ãbersetzung:', t(titleKey)); 6972 if (titleKey) modalTitle.textContent = t(titleKey); 6973 } 6974 6975 // Ãbersetze alle Labels 6976 const labels = modal.querySelectorAll('label[data-i18n]'); 6977 console.log('Gefundene Labels:', labels.length); 6978 labels.forEach(label => { 6979 const key = label.getAttribute('data-i18n'); 6980 if (key) { 6981 console.log('Label:', key, '-> Ãbersetzung:', t(key)); 6982 label.textContent = t(key); 6983 } 6984 }); 6985 6986 // Ãbersetze alle Options in Selects 6987 const options = modal.querySelectorAll('option[data-i18n]'); 6988 console.log('Gefundene Options:', options.length); 6989 options.forEach(option => { 6990 const key = option.getAttribute('data-i18n'); 6991 if (key) option.textContent = t(key); 6992 }); 6993 6994 // Ãbersetze Placeholder - ALLE inputs mit placeholder 6995 const placeholderInputs = modal.querySelectorAll('input[placeholder]'); 6996 console.log('Gefundene Inputs mit Placeholder:', placeholderInputs.length);
6997 placeholderInputs.forEach(input => { 6998 const placeholderKey = input.getAttribute('data-i18n-placeholder'); 6999 if (placeholderKey) { 7000 console.log('Placeholder:', placeholderKey, '-> Ãbersetzung:', t(placeholderKey)); 7001 input.placeholder = t(placeholderKey); 7002 } else if (input.placeholder) { 7003 // Versuche den Placeholder-Text direkt zu übersetzen 7004 const translated = t(input.placeholder); 7005 console.log('Direkter Placeholder:', input.placeholder, '-> Ãbersetzung:', translated); 7006 if (translated !== input.placeholder) { 7007 input.placeholder = translated; 7008 } 7009 } 7010 }); 7011 7012 // Ãbersetze Buttons 7013 const buttons = modal.querySelectorAll('button[data-i18n], button[data-i18n-html]'); 7014 console.log('Gefundene Buttons:', buttons.length); 7015 buttons.forEach(button => { 7016 const key = button.getAttribute('data-i18n') || button.getAttribute('data-i18n-html'); 7017 console.log('Button Key:', key, '-> Ãbersetzung:', t(key)); 7018 if (key) { 7019 // Für Buttons mit Icons behalte das Icon 7020 if (button.querySelector('i')) { 7021 const icon = button.querySelector('i').outerHTML; 7022 button.innerHTML = icon + ' ' + t(key); 7023 } else { 7024 button.textContent = t(key); 7025 } 7026 } 7027 }); 7028 7029 console.log('SmartList Modal Ãbersetzung abgeschlossen'); 7030 } 7031 7032 // Smart List Modal vorbereiten und anzeigen 7033 createSmartListModal() { 7034 // Stelle sicher, dass das Modal-Element existiert 7035 let modalElement = document.getElementById('smartListModal'); 7036 7037 // Wenn Modal existiert, übersetze alle Elemente 7038 if (modalElement) { 7039 this.translateSmartListModal(); 7040 } 7041 7042 // Falls nicht vorhanden, erstelle es 7043 if (!modalElement) { 7044 const modalHtml = ` 7045 <div class="modal fade" id="smartListModal" tabindex="-1" role="dialog" aria-labelledby="smartListModalLabel" aria-hidden="true"> 7046 <div class="modal-dialog modal-lg" role="document"> 7047 <div class="modal-content"> 7048 <div class="modal-header"> 7049 <h5 class="modal-title" id="smartListModalLabel">${t('Smart-Liste erstellen')}</h5> 7050 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> 7051 </div> 7052 <div class="modal-body"> 7053 <form id="smartListForm"> 7054 <div class="mb-3"> 7055 <label for="listName" class="form-label">${t('Name der Liste')}</label> 7056 <input type="text" class="form-control" id="listName" name="listName" required /> 7057 </div> 7058 <div class="mb-3"> 7059 <label class="form-label">${t('Filter')}</label> 7060 <div id="filterContainer"> 7061 <div class="filter-row mb-3 d-flex"> 7062 <div class="flex-grow-1 me-2"> 7063 <select class="form-select filter-field"> 7064 <option value="name">${t('Name')}</option> 7065 <option value="email">${t('E-Mail')}</option> 7066 <option value="company">${t('Firma')}</option> 7067 <option value="phone_number">${t('Telefon')}</option> 7068 <option value="industry">${t('Branche')}</option> 7069 <option value="city">${t('Ort')}</option> 7070 <option value="status">${t('Lead-Status')}</option> 7071 <option value="lastContact">${t('Letzter Kontakt')}</option> 7072 <option value="createdAt">${t('Erstellt am')}</option> 7073 <option value="updatedAt">${t('Aktualisiert am')}</option> 7074 </select> 7075 </div> 7076 <div class="flex-grow-1 me-2"> 7077 <select class="form-select filter-operator"> 7078 <option value="contains">${t('enthält')}</option> 7079 <option value="equals">${t('ist gleich')}</option> 7080 <option value="notEquals">${t('ist nicht gleich')}</option> 7081 <option value="startsWith">${t('beginnt mit')}</option> 7082 <option value="endsWith">${t('endet mit')}</option> 7083 <option value="before">${t('vor dem')}</option> 7084 <option value="after">${t('nach dem')}</option> 7085 </select> 7086 </div> 7087 <div class="flex-grow-1 me-2 filter-value-container"> 7088 <input type="text" class="form-control filter-value" placeholder="${t('Wert eingeben...')}" /> 7089 </div> 7090 <button type="button" class="btn btn-outline-danger remove-filter"> 7091 <i class="fas fa-times"></i> 7092 </button> 7093 </div> 7094 </div> 7095 <button type="button" class="btn btn-outline-secondary" id="addFilterBtn"> 7096 <i class="fas fa-plus"></i> ${t('Filter hinzufügen')} 7097 </button> 7098 </div> 7099 <div class="modal-footer"> 7100 <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-i18n="Abbrechen">${t('Abbrechen')}</button> 7101 <button type="submit" class="btn btn-primary" data-i18n="Liste erstellen">${t('Liste erstellen')}</button> 7102 </div> 7103 </form> 7104 </div> 7105 </div> 7106 </div> 7107 </div> 7108 `; 7109 7110 document.body.insertAdjacentHTML('beforeend', modalHtml); 7111 modalElement = document.getElementById('smartListModal'); 7112 } 7113 7114 // Initialisiere das Modal 7115 if (!this.smartListModal) { 7116 this.smartListModal = new bootstrap.Modal(modalElement); 7117 } 7118 7119 // Event-Listener für das Formular 7120 const form = document.getElementById('smartListForm'); 7121 if (form) { 7122 // Entferne vorherige Event-Listener 7123 const newForm = form.cloneNode(true); 7124 form.parentNode.replaceChild(newForm, form); 7125 newForm.addEventListener('submit', (e) => this.handleSmartListSubmit(e)); 7126 7127 // Initialisiere die erste Filter-Zeile 7128 const firstRow = newForm.querySelector('.filter-row'); 7129 if (firstRow) { 7130 this.initializeFilterRow(firstRow); 7131 } 7132 } 7133 7134 // Event-Listener für Filter hinzufügen 7135 const addFilterBtn = document.getElementById('addFilterBtn'); 7136 if (addFilterBtn) { 7137 // Entferne vorherige Event-Listener 7138 const newBtn = addFilterBtn.cloneNode(true); 7139 addFilterBtn.parentNode.replaceChild(newBtn, addFilterBtn); 7140 newBtn.addEventListener('click', () => this.addFilterRow()); 7141 } 7142 7143 // Event-Listener für Filter entfernen 7144 const filterContainer = document.getElementById('filterContainer'); 7145 if (filterContainer) { 7146 // Entferne vorherige Event-Listener 7147 const newContainer = filterContainer.cloneNode(true); 7148 filterContainer.parentNode.replaceChild(newContainer, filterContainer); 7149 newContainer.addEventListener('click', (e) => { 7150 if (e.target.closest('.remove-filter')) { 7151 e.target.closest('.filter-row').remove(); 7152 } 7153 }); 7154 } 7155 7156 // Zeige das Modal an 7157 this.smartListModal.show(); 7158 } 7159 7160 renderVirtualizedContacts() { 7161 const container = document.getElementById('contactsContainer');
7162 const viewportHeight = container.clientHeight; 7163 const rowHeight = 50; // Angenommene Höhe einer Zeile 7164 7165 const visibleRowsCount = Math.ceil(viewportHeight / rowHeight); 7166 const bufferRowsCount = 5; // Zusätzliche Reihen oben und unten 7167 7168 const scrollTop = container.scrollTop; 7169 const startIndex = Math.max(0, Math.floor(scrollTop / rowHeight) - bufferRowsCount); 7170 const endIndex = Math.min( 7171 this.filteredContacts.length, 7172 Math.ceil((scrollTop + viewportHeight) / rowHeight) + bufferRowsCount 7173 ); 7174 7175 // Nur die sichtbaren Kontakte rendern 7176 const visibleContacts = this.filteredContacts.slice(startIndex, endIndex); 7177 7178 // Container für virtuelle Liste setzen 7179 const totalHeight = this.filteredContacts.length * rowHeight; 7180 const topPadding = startIndex * rowHeight; 7181 7182 container.innerHTML = ` 7183 <div style="height: ${topPadding}px"></div> 7184 <div id="visibleRows"></div> 7185 <div style="height: ${totalHeight - topPadding - (endIndex - startIndex) * rowHeight}px"></div> 7186 `; 7187 7188 // Sichtbare Reihen rendern 7189 const visibleRowsContainer = document.getElementById('visibleRows'); 7190 visibleRowsContainer.innerHTML = visibleContacts.map(contact => this.createContactRow(contact)).join(''); 7191 7192 // Nur einen Event-Listener für den Container mit Event-Delegation 7193 this.setupEventDelegation(); 7194 } 7195 7196 setupEventDelegation() { 7197 const container = document.getElementById('contactsContainer'); 7198 7199 // Entferne vorherige Event-Listener, falls vorhanden 7200 const newContainer = container.cloneNode(true); 7201 container.parentNode.replaceChild(newContainer, container); 7202 7203 // Nur ein Event-Listener für den Container 7204 newContainer.addEventListener('click', (e) => { 7205 const contactLink = e.target.closest('.contact-link'); 7206 if (contactLink) { 7207 // Prüfen ob Checkbox geklickt wurde 7208 if (e.target.closest('.checkbox-column') || (e.target.closest('.contact-noclick') && !e.target.closest('.contact-open-item'))) { 7209 return; 7210 } 7211 7212 e.preventDefault(); 7213 const contactId = contactLink.dataset.id; 7214 if (contactId) { 7215 // NEUE FUNKTION: Speichere den aktuellen Zustand vor der Navigation 7216 this.storeContactsState(); 7217 7218 // Update URL without page reload 7219 window.history.pushState({}, '', `?page=contact-detail&id=${contactId}`); 7220 // Load the contact detail page 7221 Pages.loadPage('contact-detail'); 7222 } 7223 } 7224 }); 7225 7226 // Ebenso für Checkboxen 7227 newContainer.addEventListener('change', (e) => { 7228 if (e.target.matches('.contact-checkbox')) { 7229 e.stopPropagation(); 7230 this.updateBulkActionsVisibility(); 7231 } 7232 }); 7233 } 7234 7235 // Verbesserte Ladestrategie in Chunks 7236 async loadAllContacts() { 7237 this.isLoading = true; 7238 this.showLoadingIndicator(); 7239 7240 try { 7241 // Hole zunächst eine kleine Menge für schnelle Anzeige 7242 await this.loadContactsPage(1, 50); 7243 this.renderContacts(); 7244 7245 // Lade dann den Rest im Hintergrund 7246 this.loadRemainingContactsInBackground(); 7247 } catch (error) { 7248 console.error('Fehler beim Laden der Kontakte:', error); 7249 this.showError('Fehler beim Laden der Kontakte'); 7250 } finally { 7251 this.isLoading = false; 7252 this.hideLoadingIndicator(); 7253 } 7254 } 7255 7256 // Laden im Hintergrund (nur für Desktop) 7257 async loadRemainingContactsInBackground(tenantId) { 7258 if (!tenantId || !this.lastVisible || !this.hasMore) { 7259 return; 7260 } 7261
7262 // Starte das Laden im Hintergrund nach kurzer Verzögerung 7263 setTimeout(async () => { 7264 let currentLastVisible = this.lastVisible; 7265 let hasMore = this.hasMore; 7266 7267 while (hasMore) { 7268 try { 7269 /* phase8c bg-pagination noop */ const snapshot = { empty: true, forEach: () => {}, docs: [], size: 0 }; 7270 7271 if (snapshot.empty) { 7272 hasMore = false; 7273 this.allContactsLoaded = true; 7274 this.backgroundLoadingComplete = true; 7275 break; 7276 } 7277 7278 // Füge neue Kontakte zum Array hinzu 7279 const newContacts = []; 7280 snapshot.forEach(doc => { 7281 const fullData = doc.data(); 7282 7283 // Speichere vollständige Daten im Cache 7284 this.fullContactsCache.set(doc.id, { id: doc.id, ...fullData }); 7285 7286 // Minimale Daten für die Liste 7287 newContacts.push({ 7288 id: doc.id, 7289 name: fullData.name || '', 7290 email: fullData.email || '', 7291 phone_number: fullData.phone_number || '', 7292 company: fullData.company || '', 7293 updated_at: fullData.updated_at, 7294 lead_status: fullData.lead_status || '', 7295 task: fullData.task || '', 7296 tenantID: fullData.tenantID || '', 7297 industry: fullData.industry || '', 7298 team: fullData.team || [], team_uids: fullData.team_uids || [], liste: fullData.liste || '', // kontakt-team-v1 7299 created_at: fullData.created_at 7300 }); 7301 }); 7302 7303 // Füge neue Kontakte hinzu 7304 this.contacts = [...this.contacts, ...newContacts]; 7305 7306 // Aktualisiere last visible 7307 currentLastVisible = snapshot.docs[snapshot.docs.length - 1]; 7308 hasMore = snapshot.docs.length === 500; 7309 7310 // console.log(`Weitere ${newContacts.length} Kontakte im Hintergrund geladen. Total: ${this.contacts.length}`); 7311 7312 // Kurze Pause zwischen den Batches 7313 await new Promise(resolve => setTimeout(resolve, 200)); 7314 7315 } catch (error) { 7316 console.error('Fehler beim Hintergrundladen:', error); 7317 break; 7318 } 7319 } 7320 7321 this.allContactsLoaded = true; 7322 this.backgroundLoadingComplete = true; 7323 // console.log('Hintergrundladen abgeschlossen. Total Kontakte:', this.contacts.length); 7324 7325 // Aktualisiere Cache mit allen Kontakten 7326 const user = auth.currentUser; 7327 if (user) { 7328 // Desktop cache - nur für Desktop speichern 7329 const cacheKey = `contacts_${user.uid}_${tenantId}_desktop`; 7330 localStorage.setItem(cacheKey, JSON.stringify({ 7331 data: this.contacts, 7332 timestamp: Date.now() 7333 })); 7334 } 7335 }, 1000); // Starte nach 1 Sekunde 7336 } 7337 7338 // Effizienteres Filtern und Sortieren 7339 filterAndSortContacts(searchQuery) { 7340 // Verzögerung für Eingabeeffizienz 7341 clearTimeout(this.filterTimeout); 7342 7343 this.filterTimeout = setTimeout(() => { 7344 console.time('filterAndSort'); 7345 7346 // Arbeite mit einer Teilmenge, wenn wir viele Kontakte haben 7347 const contactsToProcess = this.contacts.length > 1000 7348 ? this.contacts.slice(0, 1000) // Verarbeite zunächst nur die ersten 1000 7349 : this.contacts; 7350 7351 let filtered = contactsToProcess; 7352 7353 // Filter nur anwenden, wenn eine Suchanfrage vorhanden ist 7354 if (searchQuery && searchQuery.trim()) { 7355 const query = searchQuery.toLowerCase().trim(); 7356 filtered = contactsToProcess.filter(contact => { 7357 // Optimierte Suche mit frühem Abbruch 7358 // kontakte-filter-v1: optional chaining schuetzt nur gegen 7359 // null/undefined, nicht gegen Objekt-Felder. 7360 return (suchText(contact.name).includes(query) || 7361 suchText(contact.email).includes(query) || 7362 suchText(contact.phone_number).includes(query) || 7363 suchText(contact.company).includes(query)); 7364 }); 7365 } 7366 7367 // Effizienter sortieren durch Caching der Sortierwerte 7368 if (this.sortField && this.sortDirection) { 7369 filtered.sort((a, b) => { 7370 const aValue = (a[this.sortField] || '').toString().toLowerCase(); 7371 const bValue = (b[this.sortField] || '').toString().toLowerCase(); 7372 7373 return this.sortDirection === 'asc' 7374 ? aValue.localeCompare(bValue) 7375 : bValue.localeCompare(aValue); 7376 }); 7377 } 7378 7379 this.filteredContacts = filtered; 7380 console.timeEnd('filterAndSort'); 7381 7382 // Virtuelle Liste aktualisieren 7383 if (this.contacts.length > 100) { 7384 this.renderVirtualizedContacts(); 7385 } else { 7386 this.renderContacts(); 7387 } 7388 7389 // Weitere Kontakte im Hintergrund verarbeiten, wenn wir viele haben 7390 if (this.contacts.length > 1000 && searchQuery && searchQuery.trim()) {
7391 this.processRemainingContactsInBackground(searchQuery); 7392 } 7393 }, 200); 7394 } 7395 7396 // Neue Methode zum schnellen Laden von gecachten Kontakten 7397 async loadLastCachedContacts() { 7398 // this.showLoadingIndicator(); // <-- Entfernt 7399 7400 try { 7401 // Versuche zuerst den spezifischen Cache mit User und Tenant ID 7402 const user = auth.currentUser; 7403 if (!user) return false; 7404 7405 // Verwende gespeicherte Tenant-ID falls vorhanden, um Firestore-Abfrage zu vermeiden 7406 let tenantId = this.userDoc?.tenantID || this.userDoc?.tenant_id; 7407 7408 // Nur wenn noch keine Tenant-ID vorhanden, lade sie von Firestore 7409 if (!tenantId) { 7410 const userDoc = await _phase8UserSnap(user.uid); /* phase8 */ 7411 tenantId = userDoc.data()?.tenantID || userDoc.data()?.tenant_id; 7412 } 7413 7414 const primaryCacheKey = `contacts_${user.uid}_${tenantId}`; 7415 7416 // Versuche primären Cache 7417 let cachedData = localStorage.getItem(primaryCacheKey); 7418 7419 // Falls nicht vorhanden, versuche letzten Cache-Key 7420 if (!cachedData) { 7421 const lastCacheKey = localStorage.getItem('last_contacts_cache_key'); 7422 if (lastCacheKey) { 7423 cachedData = localStorage.getItem(lastCacheKey); 7424 } 7425 } 7426 7427 if (cachedData) { 7428 const parsed = JSON.parse(cachedData); 7429 if (parsed.data && parsed.data.length > 0) { 7430 // Prüfe ob Cache noch gültig ist (15 Minuten) 7431 const cacheAge = Date.now() - parsed.timestamp; 7432 if (cacheAge < 15 * 60 * 1000) { 7433 // console.log(`Lade ${parsed.data.length} Kontakte aus Cache (Alter: ${Math.round(cacheAge/1000)}s)`); 7434 this.contacts = parsed.data; 7435 7436 // Sortiere Kontakte konsistent (updated_at desc) 7437 this.contacts.sort((a, b) => { 7438 const dateA = this.parseContactDate(a.updated_at); 7439 const dateB = this.parseContactDate(b.updated_at); 7440 return dateB - dateA; // Neueste zuerst 7441 }); 7442 7443 // Zeige die gecachten Kontakte sofort an 7444 await this.renderContacts(this.contacts); 7445 7446 // KEIN automatisches Neuladen im Hintergrund mehr! 7447 // Das wird jetzt von init() gesteuert 7448 7449 return true; 7450 } 7451 } 7452 } 7453 // Indikator hier nicht ausblenden 7454 return false; 7455 } catch (error) { 7456 console.warn('Error loading cached contacts:', error); // Log error 7457 // Indikator hier nicht ausblenden 7458 return false; 7459 } 7460 } 7461 7462 // Hintergrund-Synchronisation für Kontakte
7463 scheduleBackgroundSync() { 7464 // Synchronisiere Kontakte alle 10 Minuten im Hintergrund 7465 setInterval(() => { 7466 // Nur synchronisieren, wenn die Seite aktiv ist und die Kontakte angezeigt werden 7467 if (document.visibilityState === 'visible' && 7468 document.getElementById('contactsContainer')) { 7469 // console.log('Starte Hintergrund-Synchronisation der Kontakte...'); 7470 this.silentUpdate(); 7471 } 7472 }, 10 * 60 * 1000); 7473 } 7474 7475 // Stille Aktualisierung ohne UI-Unterbrechung 7476 async silentUpdate() { 7477 try { 7478 const freshContacts = await this.fetchContactsFromServer(); 7479 if (freshContacts && freshContacts.length > 0) { 7480 // Aktualisiere die Kontakte im Hintergrund 7481 this.contacts = freshContacts; 7482 7483 // Aktualisiere die UI nur, wenn sich die Anzahl der Kontakte geändert hat 7484 const currentlyDisplayed = document.querySelectorAll('.contact-row').length; 7485 if (currentlyDisplayed !== freshContacts.length) { 7486 // console.log('Kontakte im Hintergrund aktualisiert, UI wird aktualisiert'); 7487 this.renderContacts(freshContacts); 7488 } else { 7489 // console.log('Kontakte im Hintergrund aktualisiert, UI unverändert'); 7490 } 7491 } 7492 } catch (error) { 7493 console.warn('Fehler bei der stillen Aktualisierung:', error); 7494 } 7495 } 7496 7497 // Stelle sicher, dass die renderInstantCallModal-Methode die Kalenderoptionen korrekt lädt 7498 async renderInstantCallModal(contactId) { 7499 try { 7500 // Bisheriger Code zum Abrufen der Kontaktdaten... 7501 7502 // Hier die Kalenderoptionen laden 7503 const calendarOptionsHtml = await this.getCalendarOptionsHtml(); 7504 7505 // Modal-Inhalt aktualisieren und sicherstellen, dass das Kalender-Dropdown korrekt befüllt wird 7506 const calendarSelect = document.getElementById('eventCalendar'); 7507 if (calendarSelect) { 7508 calendarSelect.innerHTML = calendarOptionsHtml; 7509 } 7510 7511 // Rest des Codes... 7512 } catch (error) { 7513 console.error('Error rendering instant call modal:', error); 7514 } 7515 } 7516 7517 // KORRIGIERTE/NEUE Bulk Action Funktionen 7518 getSelectedContactIds() { 7519 // Wenn alle gefilterten Kontakte ausgewählt wurden 7520 if (this.allFilteredContactsSelected) { 7521 // console.log("Alle gefilterten Kontakte ausgewählt:", this.selectedFilteredContactIds.length); 7522 return this.selectedFilteredContactIds; 7523 } 7524 7525 // Ansonsten nur die manuell ausgewählten Checkboxen 7526 const checkboxes = document.querySelectorAll('input[name="contactSelect"]:checked'); 7527 const selectedContactIds = Array.from(checkboxes).map(checkbox => checkbox.value); 7528 // console.log("Ausgewählte Kontakt-IDs:", selectedContactIds); 7529 return selectedContactIds; 7530 } 7531 7532 async executeBulkAction() { 7533 const selectedContactIds = this.getSelectedContactIds(); 7534 if (selectedContactIds.length === 0) { 7535 UI.showError('Keine Kontakte ausgewählt.'); 7536 return; 7537 } 7538 7539 const actionSelect = document.getElementById('bulkActionSelect'); // Annahme: Es gibt ein Select-Element mit dieser ID 7540 if (!actionSelect) { 7541 console.error("Bulk Action Select Element ('bulkActionSelect') nicht gefunden."); 7542 UI.showError("Bulk Action Steuerelement fehlt."); 7543 return; 7544 } 7545 const selectedAction = actionSelect.value; 7546 // console.log("Ausgewählte Bulk Action:", selectedAction); 7547 7548 if (selectedAction === 'update') { 7549 const fieldSelect = document.getElementById('bulkFieldSelect'); // Annahme: Feld-Auswahl 7550 if (!fieldSelect) { 7551 console.error("Bulk Field Select Element ('bulkFieldSelect') nicht gefunden."); 7552 UI.showError("Bulk Action Feld-Auswahl fehlt."); 7553 return; 7554 } 7555 const selectedField = fieldSelect.value; 7556 let updateData = {}; 7557 let newValueDescription = ''; // Für Bestätigungsdialog 7558 7559 // console.log("Ausgewähltes Feld für Update:", selectedField); 7560 7561 if (selectedField === 'lead_status') { 7562 const statusSelect = document.getElementById('bulkLeadStatus'); // ID aus vorherigem Code 7563 if (!statusSelect) { UI.showError("Lead Status Auswahl nicht gefunden."); return; } 7564 const newValue = statusSelect.value; 7565 if (!newValue) { 7566 UI.showError('Bitte einen neuen Lead-Status auswählen.'); 7567 return; 7568 } 7569 updateData.lead_status = newValue; 7570 newValueDescription = `Lead-Status auf '${newValue}'`; 7571 }
7571 else if (selectedField === 'industry') { 7572 const industrySelect = document.getElementById('bulkIndustry'); // ID aus vorherigem Code 7573 if (!industrySelect) { UI.showError("Branchen Auswahl nicht gefunden."); return; } 7574 const newValue = industrySelect.value; 7575 if (!newValue) { 7576 UI.showError('Bitte eine Branche auswählen.'); 7577 return; 7578 } 7579 updateData.industry = newValue; 7580 newValueDescription = `Branche auf '${newValue}'`; 7581 } else if (selectedField === 'task') { // Aufgabe hinzufügen 7582 const taskSelect = document.getElementById('bulkTask'); // ID aus vorherigem Code 7583 if (!taskSelect) { UI.showError("Aufgaben Auswahl nicht gefunden."); return; } 7584 const newValue = taskSelect.value; 7585 if (!newValue) { 7586 UI.showError('Bitte eine Aufgabe auswählen.'); 7587 return; 7588 } 7589 updateData.task = newValue; 7590 newValueDescription = `Aufgabe auf '${newValue}'`; 7591 } 7592 // HINWEIS: Bulk Tag Add/Remove wird aktuell nicht über diese UI gesteuert, daher hier entfernt. 7593 // Kann bei Bedarf wieder hinzugefügt werden, wenn die UI Elemente (bulkTags) existieren. 7594 /* else if (selectedField === 'tags_add') { ... } 7595 else if (selectedField === 'tags_remove') { ... } */ 7596 else { 7597 UI.showError('Ungültiges Feld für Update ausgewählt.'); 7598 return; 7599 } 7600 7601 // Bestätigungsdialog 7602 if (confirm(`${t('Möchten Sie für')} ${selectedContactIds.length} ${t('Kontakte wirklich')} ${newValueDescription} ${t('setzen')}?`)) { 7603 await this.handleBulkUpdate(selectedContactIds, updateData); // Verwende die korrigierte handleBulkUpdate 7604 } 7605 7606 } else if (selectedAction === 'delete') { 7607 // Bestätigungsdialog 7608 if (confirm(`${t('Möchten Sie die ausgewählten')} ${selectedContactIds.length} ${t('Kontakte wirklich löschen? Dieser Vorgang kann nicht rückgängig gemacht werden!')}`)) { 7609 await this.handleBulkDelete(selectedContactIds); // Verwende die korrigierte handleBulkDelete 7610 } 7611 } else { 7612 UI.showError('Bitte wählen Sie eine Aktion aus.'); 7613 } 7614 } 7615 7616 // HINZUGEFÃGTE Hilfsfunktion 7617 deselectAllContacts() { 7618 // Setze die Flags für gefilterte Auswahl zurück 7619 this.allFilteredContactsSelected = false; 7620 this.selectedFilteredContactIds = []; 7621 7622 const selectAllCheckbox = document.getElementById('selectAll'); // Desktop Checkbox 7623 if (selectAllCheckbox) { 7624 selectAllCheckbox.checked = false; 7625 } 7626 // Mobile Button Text zurücksetzen (falls vorhanden) 7627 const selectAllMobileBtn = document.getElementById('selectAllMobile'); 7628 if (selectAllMobileBtn) { 7629 selectAllMobileBtn.innerHTML = '<i class="fas fa-check-square me-1"></i>Alle auswählen'; 7630 } 7631 // Alle einzelnen Checkboxen abwählen 7632 const checkboxes = document.querySelectorAll('input[name="contactSelect"]'); 7633 checkboxes.forEach(checkbox => checkbox.checked = false); 7634 // Sichtbarkeit der Bulk Actions aktualisieren 7635 this.updateBulkActionsVisibility(); 7636 } 7637 7638 // NEU: Methode zum Abrufen der Lead-Status (lädt sie bei Bedarf) 7639 async getLeadStatuses() { 7640 // Prüfe, ob Status bereits geladen sind und gültig erscheinen 7641 if (this.leadStatuses && Array.isArray(this.leadStatuses) && this.leadStatuses.length > 0) { 7642 // console.log("[getLeadStatuses] Returning already loaded statuses:", this.leadStatuses); 7643 return this.leadStatuses; 7644 } 7645 7646 // Wenn nicht geladen, rufe die Ladefunktion auf 7647 // console.log("[getLeadStatuses] Statuses not loaded, calling loadLeadStatuses..."); 7648 await this.loadLeadStatuses(); // Diese Funktion speichert das Ergebnis in this.leadStatuses 7649 7650 // Gib die frisch geladenen Status zurück 7651 // console.log("[getLeadStatuses] Returning newly loaded statuses:", this.leadStatuses); 7652 return this.leadStatuses; 7653 } 7654 7655 // Neue Funktion zum Aktualisieren der Filter-Dropdowns mit dynamischen Lead-Status 7656 updateFilterDropdowns() { 7657 const leadStatusFilter = document.getElementById('leadStatusFilter'); 7658 if (leadStatusFilter && this.leadStatuses) { 7659 // Aktueller ausgewählter Wert speichern 7660 const currentValue = leadStatusFilter.value; 7661 7662 // Dropdown leeren und neu füllen 7663 leadStatusFilter.innerHTML = ` 7664 <option value="">${t('Lead-Status: Alle')}</option> 7665 ${this.leadStatuses.map(status => 7666 `<option value="${status}">${t(status)}</option>` 7667 ).join('')} 7668 `; 7669 7670 // Vorherigen Wert wiederherstellen, wenn möglich 7671 if (currentValue && this.leadStatuses.includes(currentValue)) { 7672 leadStatusFilter.value = currentValue; 7673 } 7674 } 7675 } 7676 7677 async loadLeadStatuses() { 7678 try { 7679 const user = auth.currentUser; 7680 if (!user) { 7681 console.warn("Kein Benutzer angemeldet, Standard-Lead-Status werden verwendet."); 7682 // Fallback zu einem minimalen Set oder leer lassen? 7683 this.leadStatuses = DEFAULT_LEAD_STATUSES; 7684 return; 7685 } 7686 7687 // Stelle sicher, dass userDoc geladen ist (sollte durch init geschehen sein) 7688 if (!this.userDoc) { 7689 console.warn("Benutzerdokument nicht geladen, versuche es jetzt zu laden..."); 7690 const userDocSnap = await _phase8UserSnap(user.uid); /* phase8 (was userDocRef + getDoc) */ 7691 if (userDocSnap.exists()) { 7692 this.userDoc = userDocSnap.data(); 7693 } else { 7694 console.error("Benutzerdokument konnte nicht geladen werden."); 7695 this.leadStatuses = DEFAULT_LEAD_STATUSES; // Fallback 7696 return; 7697 } 7698 } 7699 7700 const tenantId = this.userDoc.tenantID || this.userDoc.tenant_id; 7701 7702 if (!tenantId) { 7703 console.error("Keine Tenant-ID im Benutzerdokument gefunden."); 7704 this.leadStatuses = DEFAULT_LEAD_STATUSES; // Fallback 7705 return; 7706 } 7707 7708 // Rufe die zentrale Funktion aus app.js auf 7709 this.leadStatuses = await loadCustomLeadStatuses(tenantId); 7710 // console.log('Geladene Lead-Status (über app.js):', this.leadStatuses); 7711 7712 } catch (error) { 7713 console.error('Fehler beim Laden der Lead-Status über loadCustomLeadStatuses:', error); 7714 // Fallback zu Standard-Status bei Fehlern 7715 this.leadStatuses = DEFAULT_LEAD_STATUSES; 7716 } 7717 } 7718 7719 // SIP Outbound Call Handler 7720 async handleSipOutboundCall() { 7721 if (!this.hasMultiCallPermission()) { 7722 UI.showError('Sie haben keine Berechtigung für Voice AI Outbound Calls. Bitte wenden Sie sich an den Administrator.'); 7723 return; 7724 } 7725
7726 const selectedContacts = this.getSelectedContacts(); 7727 7728 if (selectedContacts.length === 0) { 7729 UI.showError(t('Bitte wählen Sie mindestens einen Kontakt aus.')); 7730 return; 7731 } 7732 7733 if (selectedContacts.length > 50) { 7734 UI.showError('Es können maximal 50 Kontakte auf einmal ausgewählt werden.'); 7735 return; 7736 } 7737 7738 const contactsWithoutPhone = selectedContacts.filter(contact => !contact.phone); 7739 if (contactsWithoutPhone.length > 0) { 7740 UI.showError(`${contactsWithoutPhone.length} ausgewählte Kontakte haben keine Telefonnummer.`); 7741 return; 7742 } 7743 7744 try { 7745 // Get user's tenant ID 7746 const currentUser = auth.currentUser; 7747 if (!currentUser) { 7748 throw new Error('Nicht eingeloggt'); 7749 } 7750 7751 const userDoc = await _phase8UserSnap(currentUser.uid); /* phase8 */ 7752 if (!userDoc.exists()) { 7753 throw new Error('Benutzerprofil nicht gefunden'); 7754 } 7755 7756 const userData = userDoc.data(); 7757 const tenantId = userData.tenantID || userData.tenant_id; 7758 if (!tenantId) { 7759 throw new Error('Keine Mandanten-ID gefunden'); 7760 } 7761 7762 // Templates laden (wie bei Sofort Anruf) 7763/* phase8c */ const templates = await _c8c_listMulticallTemplates(); 7764 7765 if (templates.length === 0) { 7766 throw new Error('Keine Voice AI Templates gefunden. Bitte erstellen Sie zuerst eine MultiCall-Vorlage.'); 7767 } 7768 7769 // Calendar Options laden 7770 const calendarOptionsHtml = await this.getCalendarOptionsHtml(); 7771 7772 // Modal HTML erstellen (wie bei Sofort Anruf) 7773 const modalHtml = ` 7774 <div class="modal fade" id="voiceAiOutboundModal" tabindex="-1" aria-labelledby="voiceAiOutboundModalLabel" aria-hidden="true"> 7775 <div class="modal-dialog"> 7776 <div class="modal-content"> 7777 <div class="modal-header"> 7778 <h5 class="modal-title" id="voiceAiOutboundModalLabel">${t('Voice AI Outbound Call - Vorlage auswählen')}</h5> 7779 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> 7780 </div> 7781 <div class="modal-body"> 7782 <div class="mb-3"> 7783 <label for="voiceAiTemplateSelect" class="form-label">${t('MultiCall Vorlage')}</label> 7784 <select class="form-select" id="voiceAiTemplateSelect" required> 7785 </select> 7786 </div> 7787 <div class="mb-3"> 7788 <label for="voiceAiCalendarSelect" class="form-label">${t('Kalender')}</label> 7789 <select class="form-select" id="voiceAiCalendarSelect"> 7790 <option value="">${t('Kein Kalender')}</option> 7791 ${calendarOptionsHtml} 7792 </select> 7793 </div> 7794 <div id="selectedContactsCountVoiceAi" class="text-muted small"></div> 7795 </div> 7796 <div class="modal-footer"> 7797 <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-i18n="Abbrechen">${t('Abbrechen')}</button> 7798 <button type="button" class="btn btn-primary" id="startVoiceAiCallBtn" data-i18n="Voice AI Anrufe starten"><i class="fas fa-phone-volume me-1"></i>${t('Voice AI Anrufe starten')}</button> 7799 </div> 7800 </div> 7801 </div> 7802 </div> 7803 `; 7804 7805 // Entferne existierendes Modal falls vorhanden 7806 const existingModal = document.getElementById('voiceAiOutboundModal'); 7807 if (existingModal) { 7808 existingModal.remove(); 7809 } 7810 7811 // Modal zum DOM hinzufügen 7812 document.body.insertAdjacentHTML('beforeend', modalHtml); 7813 7814 const modalElement = document.getElementById('voiceAiOutboundModal'); 7815 const modal = new bootstrap.Modal(modalElement); 7816 7817 // Template Select füllen 7818 const templateSelect = document.getElementById('voiceAiTemplateSelect'); 7819 templateSelect.innerHTML = templates.map(template => 7820 `<option value="${template.id}">${template.task || template.name || 'Unbenannte Vorlage'}</option>` 7821 ).join(''); 7822 7823 // Kontakt-Anzahl anzeigen 7824 const selectedCount = document.getElementById('selectedContactsCountVoiceAi'); 7825 if (selectedCount) { 7826 selectedCount.textContent = `${selectedContacts.length}
7826 Kontakte ausgewählt für Voice AI Calls`; 7827 } 7828 7829 // Start Button Handler 7830 const startVoiceAiCallBtn = document.getElementById('startVoiceAiCallBtn'); 7831 if (startVoiceAiCallBtn) { 7832 const callHandler = async () => { 7833 const startBtn = document.getElementById('startVoiceAiCallBtn'); 7834 if (!startBtn) return; 7835 7836 const btnContent = startBtn.innerHTML; 7837 startBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Voice AI Calls starten...'; 7838 startBtn.disabled = true; 7839 7840 try { 7841 const templateId = templateSelect.value; 7842 const calendarId = document.getElementById('voiceAiCalendarSelect').value; 7843 7844 if (!templateId) { 7845 throw new Error('Bitte wählen Sie eine MultiCall-Vorlage aus.'); 7846 } 7847 7848 const selectedTemplate = templates.find(t => t.id === templateId); 7849 if (!selectedTemplate) { 7850 modal.hide(); 7851 UI.showError('Ausgewählte Vorlage nicht gefunden.'); 7852 return; 7853 } 7854 7855 modal.hide(); 7856 7857 // Voice AI Calls starten (wie bei Sofort Anruf Success Message) 7858 UI.showLoading('Starte Voice AI Outbound Calls...'); 7859 7860 const callPromises = selectedContacts.map(contact => 7861 this.initiateVoiceAiCall(contact.id, contact.phone, selectedTemplate, calendarId) 7862 ); 7863 7864 await Promise.all(callPromises); 7865 7866 // Success-Meldung wie bei Sofort Anruf 7867 UI.showSuccess(`Voice AI Outbound Calls werden an ${selectedContacts.length} Kontakte gestartet.`); 7868 7869 this.deselectAllContacts(); 7870 this.updateBulkActionsVisibility(); 7871 7872 } catch (error) { 7873 console.error('Error starting Voice AI calls:', error); 7874 UI.showError('Fehler: ' + error.message); 7875 } finally { 7876 const finalBtn = document.getElementById('startVoiceAiCallBtn'); 7877 if (finalBtn) { 7878 finalBtn.innerHTML = btnContent; 7879 finalBtn.disabled = false; 7880 } 7881 UI.hideLoading(); 7882 } 7883 }; 7884 7885 // Event Listener hinzufügen 7886 const newCallButton = startVoiceAiCallBtn.cloneNode(true); 7887 startVoiceAiCallBtn.parentNode.replaceChild(newCallButton, startVoiceAiCallBtn); 7888 newCallButton.addEventListener('click', callHandler); 7889 } 7890 7891 modal.show(); 7892 7893 } catch (error) { 7894 console.error('Error in handleSipOutboundCall:', error); 7895 UI.showError('Fehler: ' + error.message); 7896 UI.hideLoading(); 7897 } 7898 } 7899 7900 // Voice AI Call Function mit Template und Kalender 7901 async initiateVoiceAiCall(contactId, phoneNumber, template, calendarId) { 7902 try { 7903 // Get current user and auth token 7904 const currentUser = auth.currentUser; 7905 if (!currentUser) { 7906 throw new Error('Nicht eingeloggt'); 7907 } 7908 const idToken = await currentUser.getIdToken(); 7909 7910 // Get user's tenant ID 7911 const userDoc = await _phase8UserSnap(currentUser.uid); /* phase8 */ 7912 if (!userDoc.exists()) { 7913 throw new Error('Benutzerprofil nicht gefunden'); 7914 } 7915 7916 const userData = userDoc.data(); 7917 const tenantId = userData.tenantID || userData.tenant_id; 7918 if (!tenantId) { 7919 throw new Error('Keine Mandanten-ID gefunden'); 7920 } 7921 7922 // Agent-Konfiguration mit Template-Daten (wie bei Sofort Anruf) 7923 const agentConfig = { 7924 template: template, 7925 calendarId: calendarId, 7926 prompt: template.message || template.prompt || template.description, 7927 greeting: template.greeting || "Guten Tag, hier ist Ihr VoiceOne Assistent", 7928 message: template.message || "", 7929 message2: template.message2 || "", 7930 agent_id: template.agent_id || template.agentId, // Agent-ID aus Template 7931 purpose: "multicall_outbound", 7932 voice: template.voice || "Susi-German" 7933 }; 7934 7935 const response = await fetch('/api/outbound/initiate-call', { 7936 method: 'POST', 7937 headers: { 7938 'Content-Type': 'application/json', 7939 'Authorization': `Bearer ${idToken}` 7940 }, 7941 body: JSON.stringify({ 7942 contactId: contactId, 7943 phoneNumber: phoneNumber, 7944 tenantId: tenantId, 7945 callType: 'voice_ai_outbound', 7946 agentConfig: agentConfig, 7947 templateId: template.id, 7948 calendarId: calendarId 7949 }) 7950 }); 7951 7952 const result = await response.json(); 7953 7954 if (result.success) { 7955 // Success-Meldung wie bei Sofort Anruf 7956 console.log(`Voice AI Call zu ${phoneNumber} wird gestartet (Template: ${template.name})`); 7957 this.showCallStatus(result.callId, phoneNumber, template.name); 7958 } else { 7959 throw new Error(result.error || 'Unbekannter Fehler'); 7960 } 7961 } catch (error) { 7962 console.error('Fehler beim Voice AI Call:', error); 7963 throw error; // Propagate error for Promise.all handling 7964 } 7965 } 7966 7967 // Legacy Instant Call Function (für Kompatibilität) 7968 async initiateInstantCall(contactId, phoneNumber) { 7969 try { 7970 // Get current user and auth token 7971 const currentUser = auth.currentUser; 7972 if (!currentUser) { 7973 throw new Error('Nicht eingeloggt'); 7974 } 7975 const idToken = await currentUser.getIdToken(); 7976 7977 // Get user's tenant ID 7978 const userDoc = await _phase8UserSnap(currentUser.uid); /* phase8 */ 7979 if (!userDoc.exists()) { 7980 throw new Error('Benutzerprofil nicht gefunden'); 7981 } 7982 7983 const userData = userDoc.data(); 7984 const tenantId = userData.tenantID || userData.tenant_id; 7985 if (!tenantId) { 7986 throw new Error('Keine Mandanten-ID gefunden'); 7987 } 7988 7989 const response = await fetch('/api/outbound/initiate-call', { 7990 method: 'POST', 7991 headers: { 7992 'Content-Type': 'application/json', 7993 'Authorization': `Bearer ${idToken}` 7994 }, 7995 body: JSON.stringify({ 7996 contactId: contactId, 7997 phoneNumber: phoneNumber, 7998 tenantId: tenantId, 7999 callType: 'outbound_voice_ai', 8000 agentConfig: { 8001 greeting: "Hallo, hier ist Ihr VoiceOne Assistent...", 8002 purpose: "customer_outreach" 8003 } 8004 }) 8005 }); 8006 8007 const result = await response.json(); 8008 8009 if (result.success) {
8010 UI.showSuccess(`Voice KI-Anruf zu ${phoneNumber} wird initiiert...`); 8011 this.showCallStatus(result.callId, phoneNumber); 8012 } else { 8013 UI.showError(`Anruf fehlgeschlagen: ${result.error}`); 8014 } 8015 } catch (error) { 8016 console.error('Fehler beim Initiieren des Anrufs:', error); 8017 UI.showError('Fehler beim Initiieren des Anrufs'); 8018 } 8019 } 8020 8021 showCallStatus(callId, phoneNumber) { 8022 console.log(`Call ${callId} zu ${phoneNumber} initiiert`); 8023 } 8024} 8025 8026export const contacts = new Contacts(); 8027window.contacts = contacts;
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.