1// // phase8-multicall-migrated 2import { multiCallApi } from './multiCallApi.js'; 3import { Auth } from './auth.js'; 4import UI from './ui.js?v=20260820-dd1-imperr1-bann1-impauth1'; 5import { Language } from './language.js'; 6import { db } from '../firebase-init.js'; 7import { usersApi } from './usersApi.js'; 8import { 9 collection, 10 query, 11 where, 12 getDocs, 13 orderBy, 14 doc, 15 getDoc, 16 deleteDoc, 17 addDoc, 18 serverTimestamp, 19 updateDoc 20} from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js'; 21 22// Translation helper function 23function t(text) { 24 return Language.translate(text); 25} 26 27// Add global styles 28const styleSheet = document.createElement('style'); 29styleSheet.textContent = ` 30 /* Desktop Table Styles */ 31 .multicall-container.table-view .multicall-table { /* Klasse für Desktop-Tabelle */ 32 width: 100%; 33 border-collapse: collapse; 34 table-layout: fixed; /* Wichtig für feste Spaltenbreiten */ 35 } 36 .multicall-container.table-view .multicall-table th, 37 .multicall-container.table-view .multicall-table td { 38 vertical-align: middle; 39 overflow: hidden; 40 word-wrap: break-word; 41 padding: 8px; 42 border-top: 1px solid #dee2e6; /* Linie hinzufügen */ 43 } 44 .multicall-container.table-view .multicall-table thead th { 45 background-color: #f8f9fa; /* Leichter Hintergrund für Header */ 46 border-bottom: 2px solid #dee2e6; 47 white-space: nowrap; /* Header-Text nicht umbrechen */ 48 } 49 .multicall-container.table-view .multicall-table tbody tr:hover { 50 cursor: pointer; 51 background-color: rgba(0, 0, 0, 0.05); 52 } 53 54 /* Spaltenbreiten für Desktop - Spezifischere Selektoren */ 55 .multicall-container.table-view .multicall-table .date-column { width: 10%; } 56 .multicall-container.table-view .multicall-table .task-column { width: 15%; } 57 .multicall-container.table-view .multicall-table .greeting-column { width: 15%; } 58 .multicall-container.table-view .multicall-table .multicall-message-column { width: 25%; } 59 .multicall-container.table-view .multicall-table .details-column { width: 25%; } 60 .multicall-container.table-view .multicall-table .status-column { width: 5%; text-align: center; } 61 .multicall-container.table-view .multicall-table .actions-column { width: 10%; text-align: right; } 62 63 /* Mobile Card Styles */ 64 @media (max-width: 767.98px) { 65 /* Container für Karten */ 66 .multicall-container.card-view { 67 display: flex; 68 flex-direction: column; 69 gap: 15px; /* Abstand zwischen Karten */ 70 } 71 72 /* Einzelne Karte */ 73 .multicall-card { 74 background-color: #fff; 75 border-radius: 8px; 76 border: 1px solid #dee2e6; 77 box-shadow: 0 2px 4px rgba(0,0,0,0.05); 78 overflow: hidden; /* Verhindert, dass Inhalte überlaufen */ 79 cursor: pointer; /* Zeigt Klickbarkeit an */ 80 } 81 82 /* Karten-Header */ 83 .multicall-card .card-header { 84 display: flex; 85 justify-content: space-between; 86 align-items: center; 87 padding: 10px 15px; 88 background-color: #f8f9fa; 89 border-bottom: 1px solid #dee2e6; 90 font-size: 0.9em; 91 } 92 .multicall-card .card-date { 93 color: #6c757d; 94 flex-shrink: 0; /* Verhindert das Schrumpfen des Datums */ 95 margin-right: 10px; 96 } 97 .multicall-card .card-task { 98 font-weight: 500; 99 white-space: nowrap; 100 overflow: hidden; 101 text-overflow: ellipsis; 102 flex-grow: 1; /* Nimmt verfügbaren Platz ein */ 103 margin-right: 10px; 104 } 105 .multicall-card .card-status .badge { 106 font-size: 0.8em; /* Kleinere Badge Schrift */ 107 padding: 0.3em 0.6em; 108 } 109 110 /* Karten-Body */ 111 .multicall-card .card-body { 112 padding: 15px; 113 } 114 .multicall-card .card-section { 115 margin-bottom: 10px; 116 } 117 .multicall-card .card-section:last-child { 118 margin-bottom: 0; 119 } 120 .multicall-card .card-section strong { 121 display: block; 122 margin-bottom: 3px; 123 font-size: 0.85em; 124 color: #495057; 125 } 126 .multicall-card .card-section span { 127 font-size: 0.95em; 128 color: #343a40; 129 line-height: 1.4; 130 white-space: pre-wrap; /* Zeilenumbrüche beibehalten */ 131 word-break: break-word; /* Lange Wörter umbrechen */ 132 display: block; /* Sorgt für korrekten Umbruch */ 133 } 134 135 /* Karten-Footer */ 136 .multicall-card .card-footer { 137 display: flex; 138 justify-content: flex-end; /* Buttons rechts ausrichten */ 139 align-items: center; 140 padding: 10px 15px; 141 background-color: #f8f9fa; 142 border-top: 1px solid #dee2e6; 143 } 144 .multicall-card .card-actions { 145 display: flex; 146 gap: 8px; /* Abstand zwischen Buttons */ 147 align-items: center; 148 } 149 .multicall-card .card-actions .btn { 150 width: 32px; /* Feste Breite für Buttons */ 151 height: 32px; /* Feste Höhe für Buttons */ 152 padding: 0; /* Kein Innenabstand */ 153 display: flex; 154 align-items: center; 155 justify-content: center; 156 font-size: 0.9rem; /* Icon-GröÃe anpassen */ 157 } 158 .multicall-card .copy-btn { 159 background-color: #6b7280; /* Mittelgrau */ 160 } 161 .multicall-card .delete-btn { 162 background-color: #374151; /* Dunkelgrau */ 163 } 164 .multicall-card .edit-btn { /* Optionaler Edit-Button */ 165 background-color: #2e2e2e; /* Hauptfarbe */ 166 } 167 168 /* Hide table if card view is active */ 169 .multicall-container.table-view .multicall-table { 170 display: none; 171 } 172 } 173 174 /* Styles für Textumbruch und Scrollbar (gelten für beide Ansichten, wo .text-wrap verwendet wird) */ 175 .text-wrap { 176 white-space: pre-wrap; 177 word-break: break-word; 178 max-height: 100px; /* Begrenzung für Desktop */ 179 overflow-y: auto; 180 display: block; 181 } 182 .text-wrap::-webkit-scrollbar {
183 width: 6px; 184 } 185 .text-wrap::-webkit-scrollbar-track { 186 background: #f1f1f1; 187 border-radius: 3px; 188 } 189 .text-wrap::-webkit-scrollbar-thumb { 190 background: #888; 191 border-radius: 3px; 192 } 193 .text-wrap::-webkit-scrollbar-thumb:hover { 194 background: #555; 195 } 196`; 197document.head.appendChild(styleSheet); 198 199export class MultiCall { 200 static isMobileView = window.matchMedia('(max-width: 767.98px)').matches; // BildschirmgröÃe prüfen 201 202 // Outbound-Templates für MultiCall 203 static outboundTemplates = { 204 coldCall: { 205 task: 'Kaltakquise - Erstkontakt', 206 greeting: 'Guten Tag {name}, mein Name ist [Ihr Name] von VoiceOne. Ich hoffe, ich erwische Sie gerade nicht ungünstig?', 207 message: `## **Grund des Anrufs** 208Der Grund meines Anrufs: Wir arbeiten mit vielen Unternehmen in Ihrer Branche zusammen, die durch unsere KI-Telefonassistenten **bis zu 60% ihrer Anrufe automatisieren** und dadurch ihr Team massiv entlasten. 209 210## **Vorteile & Bedarf klären** 211Bei [Branche des Kunden] sehen wir oft diese Herausforderungen: 212- **Ãberlastete Telefonleitungen** in StoÃzeiten 213- **Wiederkehrende Anfragen** die viel Zeit kosten 214- **Mitarbeiter** die für wichtigere Aufgaben gebraucht werden 215 216Unsere Kunden berichten von: 217â **80% weniger Routineanfragen** am Telefon 218â **24/7 Erreichbarkeit** ohne Mehrkosten 219â **Zufriedenere Kunden** durch sofortige Antworten 220 221Wie handhaben Sie aktuell die telefonische Erreichbarkeit? Gibt es Zeiten, wo das Team überlastet ist?`, 222 message2: `## **Interesse & nächste Schritte** 223Eine **15-minütige Live-Demo** zeigt am besten, wie das konkret bei Ihnen aussehen könnte. Völlig unverbindlich natürlich. Hätten Sie diese Woche 15 Minuten - Dienstag oder Donnerstag? 224 225## **ð Einwandbehandlung** 226â **"Keine Zeit/kein Bedarf"** 227â "Verstehe ich. Nur aus Neugier: Wenn Sie **1 Stunde pro Tag** bei Telefonaten sparen könnten - wäre das interessant?" 228 229â **"Zu teuer/Budget"** 230â "Die Investition amortisiert sich meist in 2-3 Monaten durch die Zeitersparnis. Aber lassen Sie uns erst schauen, ob es überhaupt passt. Die Demo kostet nichts auÃer 15 Minuten Ihrer Zeit." 231 232â **"Haben bereits eine Lösung"** 233â "Super! Unsere KI ergänzt bestehende Systeme perfekt. Viele Kunden nutzen beides parallel. Soll ich Ihnen zeigen wie?"` 234 }, 235 appointment: { 236 task: 'Terminbestätigung', 237 greeting: 'Guten Tag {name}, hier ist [Ihr Name] von VoiceOne. Ich rufe wegen unseres Termins an.', 238 message: `## **Terminbestätigung** 239Ich wollte unseren Termin für **[Datum] um [Uhrzeit]** bestätigen und sicherstellen, dass bei Ihnen alles passt. 240 241## **Vorbereitung** 242Für unser Gespräch habe ich vorbereitet: 243â **Live-Demo** unseres KI-Telefonassistenten 244â **Anpassungsmöglichkeiten** für Ihre Branche 245â **ROI-Berechnung** basierend auf Ihrem Anrufvolumen 246 247## **Wichtige Fragen vorab** 248Damit ich die Demo optimal auf Sie abstimmen kann: 249- Wie viele Anrufe erhalten Sie durchschnittlich pro Tag? 250- Welche Art von Anfragen wiederholen sich am häufigsten? 251- Nutzen Sie bereits eine Telefonanlage oder CRM-System?`, 252 message2: `## **Teilnehmer & Technik** 253Wer wird von Ihrer Seite am Gespräch teilnehmen? Gerne können Sie Kollegen einladen, die davon profitieren würden. 254 255Für die Demo sende ich Ihnen einen Link - funktioniert Zoom bei Ihnen oder bevorzugen Sie Teams? 256 257## **ð Mögliche Ãnderungen** 258â **"Termin verschieben"** 259â "Kein Problem! Wann würde es diese oder nächste Woche besser passen?" 260 261â **"Termin absagen"** 262â "Schade, aber ich verstehe. Darf ich fragen, was sich geändert hat? Vielleicht kann ich Ihre Bedenken vorab klären?"` 263 }, 264 followUp: { 265 task: 'Nachfass-Anruf', 266 greeting: 'Guten Tag {name}, hier ist wieder [Ihr Name] von VoiceOne. Ich hoffe, es geht Ihnen gut!', 267 message: `## **Bezug zum letzten Gespräch** 268Nach unserer Demo letzte Woche wollte ich Ihnen Zeit geben, alles mit Ihrem Team zu besprechen. 269 270## **Häufige Ãberlegungen ansprechen** 271Erfahrungsgemäà beschäftigen unsere Interessenten diese Fragen: 272- **"Wie aufwendig ist die Einrichtung?"** â Wir übernehmen alles, Sie müssen nur die Anforderungen nennen 273- **"Funktioniert das mit unserem System?"** â Wir integrieren uns
273in alle gängigen Telefonanlagen 274- **"Was kostet das langfristig?"** â Die meisten Kunden sparen ab Monat 2 mehr ein, als sie investieren 275 276## **Status & Interesse abfragen** 277Welche Fragen sind bei Ihnen aufgekommen? Was hat Ihr Team gesagt?`, 278 message2: `## **Nächste Schritte vorschlagen** 279Viele Kunden starten mit einem **Pilotprojekt** für einen Bereich - z.B. nur Terminvereinbarungen. So sehen Sie schnell den Nutzen ohne groÃes Risiko. 280 281Soll ich Ihnen ein unverbindliches Angebot für einen Testlauf erstellen? 282 283## **ð Typische Einwände** 284â **"Noch nicht entschieden"** 285â "Völlig verständlich! Was bräuchten Sie für eine Entscheidung? Referenzen? Ein Testlauf? Gespräch mit einem bestehenden Kunden?" 286 287â **"Kein Budget dieses Jahr"** 288â "Verstehe! Viele starten deshalb erst im neuen Geschäftsjahr. Soll ich Sie im [Monat] nochmal kontaktieren? Dann können wir rechtzeitig planen."` 289 }, 290 reactivation: { 291 task: 'Kundenreaktivierung', 292 greeting: 'Guten Tag {name}, hier ist [Ihr Name] von VoiceOne. Es ist schon eine Weile her!', 293 message: `## **Grund der Kontaktaufnahme** 294Ich melde mich, weil wir in den letzten Monaten unser System komplett überarbeitet haben - basierend auf Feedback von Kunden wie Ihnen. 295 296## **Was ist neu?** 297Die wichtigsten Verbesserungen: 298â **Noch natürlichere Stimmen** - kaum vom Menschen zu unterscheiden 299â **Branchenspezifische Vorlagen** - sofort einsatzbereit 300â **50% günstigere Preise** durch effizientere Technologie 301â **Integration in 200+ Systeme** - CRM, Kalender, Ticketsysteme 302 303## **Interesse wecken** 304Damals war [genanntes Problem/Einwand] ein Thema. Das haben wir speziell adressiert. Wäre es interessant zu sehen, wie die Lösung jetzt aussieht?`, 305 message2: `## **Angebot unterbreiten** 306Als **"Welcome Back"-Angebot** bekommen ehemalige Interessenten: 307- **3 Monate 30% Rabatt** 308- **Kostenlose Premium-Features** 309- **Persönlicher Success Manager** 310 311Soll ich Ihnen die neuen Möglichkeiten in einer kurzen Demo zeigen? 312 313## **ð Reaktivierungs-Einwände** 314â **"Haben uns anders entschieden"** 315â "Das respektiere ich! Läuft die aktuelle Lösung gut? Oft ergänzen wir bestehende Systeme perfekt." 316 317â **"Schlechte Erfahrungen mit KI"** 318â "Das höre ich öfter. Die Technologie hat sich enorm entwickelt. Würde ein kurzer Test Sie überzeugen?"` 319 }, 320 survey: { 321 task: 'Kundenzufriedenheitsumfrage', 322 greeting: 'Guten Tag {name}, hier ist [Ihr Name] vom VoiceOne Qualitätsteam.', 323 message: `## **Zweck des Anrufs** 324Wir führen gerade unsere jährliche Kundenbefragung durch und Ihre Meinung ist uns besonders wichtig. 325 326## **Was wir wissen möchten** 327Nur 3 kurze Fragen: 3281. **Wie zufrieden sind Sie** mit unserem KI-Assistenten? (Skala 1-10) 3292. **Was funktioniert besonders gut** in Ihrem Alltag? 3303. **Was könnten wir verbessern?** 331 332## **Incentive anbieten** 333Als Dankeschön für Ihre Zeit: 334â **Gutschein über 50â¬** für Ihren nächsten Monat 335â **Exklusiver Einblick** in neue Features vor allen anderen 336â **Persönliches Optimierungsgespräch** mit unserem Expertenteam 337 338Haben Sie jetzt 3 Minuten oder passt Ihnen ein anderer Zeitpunkt besser?`, 339 message2: `## **Zusätzliche Erkenntnisse** 340Ãbrigens: 87% unserer Kunden wissen gar nicht, dass sie diese Features nutzen können: 341- **Mehrsprachigkeit** (20+ Sprachen) 342- **SMS-Versand** nach Anrufen 343- **Automatische Termine** im Kalender 344 345Nutzen Sie diese Funktionen bereits? 346 347## **ð Umfrage-Einwände** 348â **"Keine Zeit für Umfragen"** 349â "Verstehe ich! Wie wäre es mit einer 60-Sekunden-Blitz-Umfrage? Nur eine Frage: Was ist Ihr gröÃter Wunsch an uns?" 350 351â **"Nichts zu verbessern"** 352â "Das ist groÃartig! Dann würde uns interessieren: Was macht uns in Ihren Augen besonders? Das hilft uns, diese Stärken auszubauen."` 353 }, 354 setter: { 355 task: 'Setter - Terminvereinbarung', 356 greeting: 'Guten Tag {name}, hier ist [Ihr Name]. Ich habe eine wichtige Information für Sie!', 357 message: `## **Exklusivität schaffen** 358Unser Geschäftsführer [Name] ist nächste Woche persönlich in [Stadt/Region] und hat noch **2 Termine** für Strategiegespräche frei. 359 360## **Qualifikation & Nutzen** 361Er trifft sich nur mit Unternehmen, die: 362- Mehr als **50 Anrufe täglich** erhalten 363- **Wachstumspläne** für die nächsten 12 Monate haben 364- Bereit sind, **innovative Lösungen** einzusetzen 365 366In diesem Gespräch erhalten Sie: 367â **Analyse Ihrer Telefonie-Kosten** (Wert: 500â¬) 368â **3 konkrete Optimierungsvorschläge**
369â **Branchen-Benchmark** Ihrer Erreichbarkeit 370 371## **Termin-Alternativen** 372Welcher Termin würde bei Ihnen passen: 373- Dienstag, 14:00 oder 16:00 Uhr? 374- Donnerstag, 10:00 oder 15:00 Uhr?`, 375 message2: `## **Dringlichkeit unterstreichen** 376Diese Termine sind wirklich die einzigen in den nächsten 3 Monaten. Normalerweise macht er nur Termine mit Bestandskunden. 377 378Soll ich Ihnen einen der Slots reservieren? 379 380## **ð Termin-Einwände** 381â **"Geschäftsführer hat keine Zeit"** 382â "Verstehe! Alternativ könnte auch Ihr Leiter Kundenservice teilnehmen. Hauptsache jemand, der die Telefonie-Herausforderungen kennt." 383 384â **"Erst mal Infos zusenden"** 385â "Mache ich gerne! Aber die wertvollsten Insights gibt's nur im persönlichen Gespräch. Die 30 Minuten lohnen sich - versprochen!"` 386 }, 387 closer: { 388 task: 'Closer - Verkaufsabschluss', 389 greeting: 'Guten Tag {name}, schön dass wir heute zum Abschluss kommen können!', 390 message: `## **Zusammenfassung & Bestätigung** 391Lassen Sie uns kurz rekapitulieren, was wir besprochen haben: 392- Ihre aktuelle Herausforderung: **[Problem/Bedarf]** 393- Unsere Lösung: **[Produkt/Service]** 394- Ihr Nutzen: **[konkreter Vorteil]** 395- Zeitrahmen: **[Implementierung/Lieferung]** 396 397Habe ich alles richtig zusammengefasst? Gibt es noch Punkte, die unklar sind? 398 399## **Investment & Wertversprechen**
400Die Investition beträgt **[Betrag]**. 401Dafür erhalten Sie: 402â **[Hauptleistung]** im Wert von [Vergleichswert] 403â **[Zusatzleistung]** inklusive 404â **[Garantie/Support]** für [Zeitraum] 405 406Basierend auf Ihrer Situation rechnet sich das bereits nach **[ROI-Zeitraum]**. 407 408## **Entscheidung herbeiführen** 409Ich sehe drei Möglichkeiten für Sie: 4101. **Starter-Paket**: [Beschreibung] - ideal zum Testen 4112. **Professional**: [Beschreibung] - unser Bestseller 4123. **Premium**: [Beschreibung] - für maximale Ergebnisse 413 414Welche Variante passt am besten zu Ihren Zielen?`, 415 message2: `## **Verbindlichkeit schaffen** 416Perfekt! Für den Start brauche ich nur noch: 417- **Ihre Rechnungsadresse** für den Vertrag 418- **Gewünschter Starttermin** 419- **Ansprechpartner** für die Umsetzung 420 421Soll ich das gleich aufnehmen und Ihnen die Unterlagen heute noch zusenden? 422 423## **ð Universelle Abschluss-Einwände** 424â **"Muss nochmal drüber schlafen"** 425â "Absolut verständlich! Was genau beschäftigt Sie noch? Oft kann ich letzte Unsicherheiten direkt klären." 426 427â **"Budget ist knapp"**
428â "Das verstehe ich. Deshalb frage ich: Was kostet es Sie, wenn Sie NICHT handeln? Oft ist Nichtstun teurer als die Investition." 429 430â **"Muss Rücksprache halten"** 431â "Natürlich! Wer muss noch überzeugt werden? Ich stelle Ihnen gerne Unterlagen zusammen oder bin beim Gespräch dabei." 432 433â **"Ist mir zu schnell"** 434â "Kein Problem! Was würde Ihnen mehr Sicherheit geben? Referenzen? Testphase? Garantieverlängerung?"` 435 } 436 }; 437 438 static async load() { 439 const mainContent = document.getElementById('mainContent'); 440 // Initial-HTML abhängig von der Ansicht setzen 441 mainContent.innerHTML = ` 442 <style> 443 .multicall-header { 444 display: flex; 445 justify-content: space-between; 446 align-items: center; 447 margin-bottom: 2rem; 448 padding: 1.5rem; 449 background: linear-gradient(135deg, #ffffff 0%, #f5f5f5 100%); 450 border-radius: 8px; 451 box-shadow: 0 2px 4px rgba(0,0,0,0.05); 452 } 453 .multicall-header h1 { 454 color: #2e2e2e; 455 font-size: 1.5rem; 456 margin: 0; 457 } 458 .multicall-actions { 459 display: flex; 460 gap: 10px; 461 align-items: center; 462 } 463 </style> 464 <div class="container-fluid"> 465 <div class="multicall-header"> 466 <h1>${t('Outbound Ãbersicht')}</h1> 467 <div class="multicall-actions"> 468 <button class="btn btn-primary" onclick="MultiCall.showNewModal()"> 469 <i class="fas fa-plus"></i> ${t('Neuer Eintrag')} 470 </button> 471 </div> 472 </div> 473 474 <div class="card shadow-sm mb-4" style="border-radius: 15px; background: white;"> 475 <div class="card-body p-4"> 476 <div class="d-flex justify-content-between align-items-center flex-wrap"> 477 <div class="flex-grow-1 me-3 mb-2 mb-md-0" style="min-width: 250px;"> 478 <div class="input-group"> 479 <span class="input-group-text" style="background: #F8F9FA; border-right: none;"> 480 <i class="fas fa-search text-muted"></i> 481 </span> 482 <input type="text" class="form-control border-start-0" id="multicallSearch" 483 placeholder="${t('Suche in MultiCall Einträgen...')}" 484 style="border-left: none;"> 485 </div> 486 </div> 487 </div> 488 </div> 489 </div> 490 491 <!-- Container für Einträge (Tabelle oder Karten) --> 492 <div id="multicallContainer" class="multicall-container"> 493 ${this.isMobileView ? 494 '<!-- Karten werden hier geladen -->' : 495 `<div class="table-responsive"> 496 <table class="table table-hover multicall-table"> 497 <thead> 498 <tr> 499 <th class="date-column">${t('Datum')}</th> 500 <th class="task-column">${t('Aufgabe')}</th> 501 <th class="greeting-column">${t('BegrüÃung')}</th> 502 <th class="multicall-message-column">${t('Nachricht')}</th> 503 <th class="details-column">${t('Details')}</th> 504 <th class="status-column">${t('Status')}</th> 505 <th class="actions-column"></th> 506 </tr> 507 </thead> 508 <tbody> 509 <!-- Tabellenzeilen werden hier geladen --> 510 </tbody> 511 </table> 512 </div>` 513 } 514 </div> 515 </div> 516 `; 517 518 // Add search event listener 519 const searchInput = document.getElementById('multicallSearch'); 520 searchInput.addEventListener('input', () => { 521 this.search(); 522 }); 523 524 // Listener für BildschirmgröÃenänderung hinzufügen 525 window.matchMedia('(max-width: 767.98px)').addEventListener('change', async (e) => { 526 const isNowMobile = e.matches; 527 if (isNowMobile !== this.isMobileView) { 528 console.log(`Ansicht geändert zu: ${isNowMobile ? 'Mobile' : 'Desktop'}. Lade MultiCall neu...`); 529 this.isMobileView = isNowMobile; 530 await this.load(); // Lade die gesamte Ansicht neu, um die Struktur korrekt zu ändern 531 } 532 }); 533 534 // Event-Listener für Klicks auf Einträge (Bearbeiten) und Buttons (Kopieren/Löschen) 535 this.bindEntryEvents(); 536 537 await this.loadEntries(); 538 } 539 540 // Funktion zum Binden der Event-Listener (Event Delegation) 541 static bindEntryEvents() { 542 const container = document.getElementById('multicallContainer'); 543 if (!container) return; 544 545 // Alten Listener entfernen, falls vorhanden, um Duplikate zu vermeiden 546 container.removeEventListener('click', this.handleEntryClick); 547 // Neuen Listener hinzufügen 548 container.addEventListener('click', this.handleEntryClick); 549 } 550 551 // Handler für Klick-Events im Container 552 static handleEntryClick = async (event) => { // Arrow function to keep 'this' context 553 const target = event.target; 554 const entryElement = target.closest('.multicall-row, .multicall-card'); // Finde Zeile oder Karte
555 556 if (!entryElement) return; // Klick war auÃerhalb eines Eintrags 557 558 const entryId = entryElement.dataset.entryId; 559 if (!entryId) return; 560 561 // Prüfen, ob auf einen Button geklickt wurde 562 const copyButton = target.closest('.copy-btn'); 563 const deleteButton = target.closest('.delete-btn'); 564 565 if (copyButton) { 566 event.stopPropagation(); // Verhindert, dass der Edit-Modal ausgelöst wird 567 await MultiCall.copy(entryId); 568 } else if (deleteButton) { 569 event.stopPropagation(); // Verhindert, dass der Edit-Modal ausgelöst wird 570 await MultiCall.delete(entryId); 571 } else { 572 // Welle 4: Backend-API statt direkter Firestore-Read 573 try { 574 const entries = await multiCallApi.list(); 575 const entry = (entries || []).find(e => e && e.id === entryId); 576 if (entry) { 577 MultiCall.showEditModal(entryId, entry); 578 } else { 579 UI.showError('Eintrag nicht gefunden.'); 580 } 581 } catch (error) { 582 console.error('Fehler beim Laden des Eintrags für Bearbeitung:', error); 583 UI.showError('Fehler beim Laden des Eintrags.'); 584 } 585 } 586 } 587 588 589 static async loadEntries(filters = {}) { 590 this.isMobileView = window.matchMedia('(max-width: 767.98px)').matches; // Aktuellen Status prüfen 591 console.log(`Lade Einträge (isMobileView: ${this.isMobileView})`); 592 593 const user = Auth.getCurrentUser(); 594 if (!user) { 595 UI.showError('Benutzer ist nicht angemeldet'); 596 return; 597 } 598 599 try { 600 // Welle 4: Backend-API statt direkter Firestore-Read 601 const userData = await usersApi.me(); 602 if (!userData) { 603 UI.showError('Benutzerdaten nicht gefunden'); 604 return; 605 } 606 const tenantId = userData.tenant_id || userData.tenantID || userData.tenantId; 607 if (!tenantId) { 608 UI.showError('Keine Tenant-ID gefunden'); 609 return; 610 } 611 612 /* phase8: was getDocs(multicall, tenantID==tenantId) */ 613 const __mcArr = await multiCallApi.list(); 614 const querySnapshot = { docs: __mcArr.map(m => ({ id: m.id, data: () => m })), forEach: function(cb) { this.docs.forEach(cb); } }; 615 const container = document.getElementById('multicallContainer'); 616 if (!container) { 617 console.error('Container #multicallContainer nicht gefunden'); 618 return; 619 } 620 621 // Container leeren und Klasse entsprechend setzen 622 container.innerHTML = ''; // Leeren 623 container.className = 'multicall-container'; // Reset classes 624 if (this.isMobileView) { 625 container.classList.add('card-view'); 626 } else { 627 container.classList.add('table-view'); 628 // Tabelle neu erstellen, wenn im Desktop-Modus 629 container.innerHTML = ` 630 <div class="table-responsive"> 631 <table class="table table-hover multicall-table"> 632 <thead> 633 <tr> 634 <th class="date-column">${t('Datum')}</th> 635 <th class="task-column">${t('Aufgabe')}</th> 636 <th class="greeting-column">${t('BegrüÃung')}</th> 637 <th class="multicall-message-column">${t('Nachricht')}</th> 638 <th class="details-column">${t('Details')}</th> 639 <th class="status-column">${t('Status')}</th> 640 <th class="actions-column"></th> 641 </tr> 642 </thead> 643 <tbody> 644 <!-- Zeilen werden hier eingefügt --> 645 </tbody> 646 </table> 647 </div>`; 648 } 649
650 if (querySnapshot.empty) { 651 // Nachricht in den passenden Container einfügen 652 const emptyMessage = `<div class="text-center p-3">${t('Keine MultiCall-Einträge gefunden')}</div>`; 653 if (this.isMobileView) { 654 container.innerHTML = emptyMessage; 655 } else { 656 const tbody = container.querySelector('tbody'); 657 if (tbody) tbody.innerHTML = `<tr><td colspan="7" class="text-center">${t('Keine MultiCall-Einträge gefunden')}</td></tr>`; 658 else container.innerHTML = emptyMessage; // Fallback 659 } 660 return; 661 } 662 663 querySnapshot.forEach((doc) => { 664 const entry = doc.data(); 665 const entryId = doc.id; 666 667 // Filter out entries with "Sprachbefehl:" in the task 668 if (entry.task && entry.task.startsWith('Sprachbefehl:')) { 669 return; // Skip this entry 670 } 671 672 if (this.isMobileView) { 673 const cardHtml = this.createEntryCardHTML(entryId, entry); 674 container.insertAdjacentHTML('beforeend', cardHtml); 675 } else { 676 const row = this.createEntryRow(entryId, entry); 677 const tbody = container.querySelector('tbody'); 678 if(tbody) tbody.appendChild(row); 679 } 680 }); 681 682 // Stelle sicher, dass die Event Listener (erneut) gebunden sind, falls die Struktur geändert wurde 683 this.bindEntryEvents(); 684 685 } catch (error) { 686 console.error('Error loading multicall entries:', error); 687 UI.showError('Fehler beim Laden der MultiCall-Einträge'); 688 // Bei Fehler ggf. Nachricht im Container anzeigen 689 const container = document.getElementById('multicallContainer'); 690 if(container) container.innerHTML = '<div class="alert alert-danger">Fehler beim Laden der Daten.</div>'; 691 } 692 } 693 694 // Behält die Logik zum Erstellen einer Tabellenzeile 695 static createEntryRow(entryId, entry) { 696 const date = entry.created_at?.seconds ? new Date(entry.created_at.seconds * 1000) : new Date(); 697 const formattedDate = `${date.getDate().toString().padStart(2, '0')}.${(date.getMonth() + 1).toString().padStart(2, '0')}.${date.getFullYear()}, ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`; 698 699 const row = document.createElement('tr'); 700 row.className = 'multicall-row'; // Klasse hinzugefügt 701 row.dataset.entryId = entryId; // Datenattribut für ID 702 703 // Die onclick-Logik wird jetzt über den Event-Listener am Container gehandhabt 704 // row.onclick = (e) => { ... }; // Entfernt 705 706 row.innerHTML = ` 707 <td class="date-column">${formattedDate}</td> 708 <td class="task-column"><div class="text-wrap">${entry.task || ''}</div></td> 709 <td class="greeting-column"><div class="text-wrap">${entry.greeting || ''}</div></td> 710 <td class="multicall-message-column"><div class="text-wrap">${entry.message || ''}</div></td> 711 <td class="details-column"><div class="text-wrap">${entry.message2 || ''}</div></td> 712 <td class="status-column"> 713 <span class="badge ${entry.status === 'aktiv' ? 'bg-success' : 'bg-secondary'}"> 714 ${entry.status || ''} 715 </span> 716 </td> 717 <td class="actions-column text-end"> 718 <button class="btn btn-sm copy-btn me-1" style="background-color: #6b7280; color: white; width: 32px; height: 32px; padding: 0;" title="Kopieren"> 719 <i class="fas fa-copy"></i> 720 </button> 721 <button class="btn btn-sm delete-btn" style="background-color: #374151; color: white; width: 32px; height: 32px; padding: 0;" title="Löschen"> 722 <i class="fas fa-trash"></i> 723 </button> 724 </td> 725 `; 726 727 return row; 728 } 729 730 // NEUE FUNKTION zum Erstellen von Karten-HTML 731 static createEntryCardHTML(entryId, entry) { 732 const date = entry.created_at?.seconds ? new Date(entry.created_at.seconds * 1000) : new Date(); 733 const formattedDate = `${date.getDate().toString().padStart(2, '0')}.${(date.getMonth() + 1).toString().padStart(2, '0')}.${date.getFullYear()}, ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`; 734 const statusBadgeClass = entry.status === 'aktiv' ? 'bg-success' : 'bg-secondary'; 735 const statusText = entry.status || 'Unbekannt';
736 737 // Die onclick-Logik wird jetzt über den Event-Listener am Container gehandhabt 738 // onclick="MultiCall.showEditModal('${entryId}', JSON.parse('${JSON.stringify(entry).replace(/'/g, "\'")}'))" // Komplexe Ãbergabe, anfällig 739 return ` 740 <div class="multicall-card" data-entry-id="${entryId}"> 741 <div class="card-header"> 742 <span class="card-date">${formattedDate}</span> 743 <span class="card-task">${entry.task || 'Unbenannte Aufgabe'}</span> 744 <span class="card-status"><span class="badge ${statusBadgeClass}">${statusText}</span></span> 745 </div> 746 <div class="card-body"> 747 ${entry.greeting ? `<div class="card-section"><strong>BegrüÃung:</strong><span>${entry.greeting}</span></div>` : ''} 748 ${entry.message ? `<div class="card-section"><strong>Nachricht:</strong><span>${entry.message}</span></div>` : ''} 749 ${entry.message2 ? `<div class="card-section"><strong>Details:</strong><span>${entry.message2}</span></div>` : ''} 750 </div> 751 <div class="card-footer"> 752 <div class="card-actions"> 753 <button class="btn copy-btn" title="Kopieren"> 754 <i class="fas fa-copy text-white"></i> 755 </button> 756 <button class="btn delete-btn" title="Löschen"> 757 <i class="fas fa-trash text-white"></i> 758 </button> 759 </div> 760 </div> 761 </div> 762 `; 763 } 764 765 // --- Bestehende Methoden (showEditModal, saveEdit, search, copy, delete, showNewModal, saveNew) bleiben weitgehend unverändert --- 766 // Es ist keine Anpassung in diesen Methoden nötig, da die Logik zum Laden der Daten für den Edit-Modal 767 // jetzt zentral im Event-Handler `handleEntryClick` liegt. 768 769 static async showEditModal(entryId, entry) { 770 const modalHtml = ` 771 <div class="modal fade" id="editMultiCallModal" tabindex="-1" aria-labelledby="editMultiCallModalLabel" aria-hidden="true"> 772 <div class="modal-dialog modal-lg"> 773 <div class="modal-content"> 774 <div class="modal-header"> 775 <h5 class="modal-title" id="editMultiCallModalLabel"> 776 <i class="fas fa-edit me-2"></i>${t('MultiCall bearbeiten')} 777 </h5> 778 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> 779 </div> 780 <div class="modal-body"> 781 <form id="editMultiCallForm"> 782 <input type="hidden" id="editEntryId" value="${entryId}"> 783 784 <div class="mb-3"> 785 <label for="editMultiCallTask" class="form-label">${t('Aufgabe')} <span class="text-danger">*</span></label> 786 <textarea class="form-control" id="editMultiCallTask" rows="2" required>${entry.task || ''}</textarea> 787 </div> 788 789 <div class="mb-3"> 790 <label for="editMultiCallGreeting" class="form-label">${t('BegrüÃung')}</label> 791 <textarea class="form-control" id="editMultiCallGreeting" rows="2">${entry.greeting || ''}</textarea> 792 <small class="form-text text-muted">${t('Verwenden Sie {name} als Variable für den Namen des Kontakts')}</small> 793 </div> 794 795 <div class="mb-3"> 796 <label for="editMultiCallMessage" class="form-label">${t('Nachricht')}</label> 797 <textarea class="form-control" id="editMultiCallMessage" rows="3">${entry.message || ''}</textarea> 798 </div> 799 800 <div class="mb-3"> 801 <label for="editMultiCallMessage2" class="form-label">${t('Details')}</label> 802 <textarea class="form-control" id="editMultiCallMessage2" rows="3">${entry.message2 || ''}</textarea> 803 </div> 804 805 <div class="mb-3"> 806 <label for="editMultiCallStatus" class="form-label">${t('Status')}</label> 807 <select class="form-select" id="editMultiCallStatus"> <!-- form-select statt form-control --> 808 <option value="aktiv" ${entry.status === 'aktiv' ? 'selected' : ''}>${t('Aktiv')}</option> 809 <option value="inaktiv" ${entry.status === 'inaktiv' ? 'selected' : ''}>Inaktiv</option> 810 </select> 811 </div> 812 813 <!-- Conversion Goal Section --> 814 <div class="mb-3"> 815 <label for="editMultiCallConversionGoal" class="form-label">${t('Conversion-Ziel')}</label> 816 <select class="form-select" id="editMultiCallConversionGoal"> 817 <option value="appointment" ${entry.conversion_goal === 'appointment' ? 'selected' : ''}>${t('Termin vereinbaren')}</option> 818 <option value="email" ${entry.conversion_goal === 'email' ? 'selected' : ''}>E-Mail versenden</option> 819 <option value="callback" ${entry.conversion_goal === 'callback' ? 'selected' : ''}>Rückruf vereinbaren</option> 820 <option value="sale" ${entry.conversion_goal === 'sale' ? 'selected' : ''}>Verkaufsabschluss</option> 821 <option value="survey" ${entry.conversion_goal === 'survey' ? 'selected' : ''}>Umfrage durchführen</option> 822 <option value="info" ${entry.conversion_goal === 'info' ? 'selected' : ''}>Information übermitteln</option> 823 <option value="custom" ${entry.conversion_goal === 'custom' ? 'selected' : ''}>${t('Benutzerdefiniert')}</option> 824 </select> 825 <small class="form-text text-muted">${t('Definiert, was als erfolgreiche Conversion zählt')}</small> 826 </div> 827 828 <div class="mb-3" id="customConversionGoalDiv" style="display: ${entry.conversion_goal === 'custom' ? 'block' : 'none'};"> 829 <label for="editMultiCallCustomGoal" class="form-label">Benutzerdefiniertes Ziel</label> 830 <input type="text" class="form-control" id="editMultiCallCustomGoal" 831 value="${entry.custom_conversion_goal || ''}" 832 placeholder="z.B. Webinar-Anmeldung"> 833 </div> 834 </form> 835 </div> 836 <div class="modal-footer"> 837 <button type="button" class="btn btn-info" onclick="MultiCall.analyzeScript('${entryId}', '${(entry.task || '').replace(/'/g, "\\'")}')"> 838 <i class="fas fa-chart-line"></i> Script analysieren 839 </button> 840 <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-i18n="Abbrechen">${t('Abbrechen')}</button> 841 <button type="button" class="btn btn-primary" onclick="MultiCall.saveEdit()" data-i18n="Speichern">${t('Speichern')}</button> 842 </div> 843 </div> 844 </div> 845 </div> 846 `; 847 848 // Remove existing modal if it exists 849 const existingModal = document.getElementById('editMultiCallModal'); 850 if (existingModal) { 851 const modalInstance = bootstrap.Modal.getInstance(existingModal); 852 if (modalInstance) { 853 modalInstance.hide(); // Verstecke zuerst, bevor es entfernt wird 854 } 855 existingModal.remove(); 856 } 857 858 // Add modal to document 859 document.body.insertAdjacentHTML('beforeend', modalHtml); 860 861 // Show modal 862 const modalElement = document.getElementById('editMultiCallModal'); 863 if (modalElement) { 864 const modal = new bootstrap.Modal(modalElement); 865 // Event listener hinzufügen, um das Modal aus dem DOM zu entfernen, nachdem es geschlossen wurde 866 modalElement.addEventListener('hidden.bs.modal', event => { 867 modalElement.remove(); 868 }); 869 modal.show(); 870 871 // Add event listener for conversion goal change 872 const conversionGoalSelect = document.getElementById('editMultiCallConversionGoal'); 873 const customGoalDiv = document.getElementById('customConversionGoalDiv'); 874 875 if (conversionGoalSelect && customGoalDiv) { 876 conversionGoalSelect.addEventListener('change', function() { 877 if (this.value === 'custom') { 878 customGoalDiv.style.display = 'block'; 879 } else { 880 customGoalDiv.style.display = 'none'; 881 } 882 }); 883 } 884 } else { 885 console.error("Konnte Modal-Element nicht finden nach dem Einfügen."); 886 } 887 } 888 889 static async saveEdit() { 890 try { 891 const entryId = document.getElementById('editEntryId').value; 892 const task = document.getElementById('editMultiCallTask').value; 893 const greeting = document.getElementById('editMultiCallGreeting').value; 894 const message = document.getElementById('editMultiCallMessage').value; 895 const message2 = document.getElementById('editMultiCallMessage2').value; 896 const status = document.getElementById('editMultiCallStatus').value; 897 const conversionGoal = document.getElementById('editMultiCallConversionGoal').value; 898 const customConversionGoal = document.getElementById('editMultiCallCustomGoal').value; 899 900 if (!task) { 901 UI.showError('Bitte geben Sie eine Aufgabe ein'); 902 return; 903 } 904 905 const updateData = { 906 task, 907 greeting, 908 message, 909 message2, 910 status, 911 conversion_goal: conversionGoal, 912 updated_at: serverTimestamp() 913 }; 914 915 // Only add custom_conversion_goal if conversion_goal is 'custom' 916 if (conversionGoal === 'custom' && customConversionGoal) { 917 updateData.custom_conversion_goal = customConversionGoal; 918 } 919 920 await multiCallApi.update(entryId, updateData); // phase8 921 922 // Close modal 923 const modalElement = document.getElementById('editMultiCallModal'); 924 if (modalElement) { 925 const modal = bootstrap.Modal.getInstance(modalElement); 926 if (modal) { 927 modal.hide(); 928 } else { 929 // Fallback, falls Instanz nicht gefunden wird 930 modalElement.classList.remove('show'); 931 modalElement.style.display = 'none'; 932 document.body.classList.remove('modal-open'); 933 const backdrop = document.querySelector('.modal-backdrop'); 934 if(backdrop) backdrop.remove(); 935 } 936 // Das Entfernen des Modals wird durch den 'hidden.bs.modal' Listener gehandhabt 937 } 938 939 940 // Show success message and refresh list 941 UI.showSuccess('MultiCall erfolgreich aktualisiert'); 942 await this.loadEntries(); // Lädt die Einträge neu (aktualisiert Tabelle oder Karten) 943 } catch (error) { 944 console.error('Error updating multicall:', error); 945 UI.showError('Fehler beim Aktualisieren des MultiCall-Eintrags'); 946 } 947 } 948 949 static search() { 950 const searchTerm = document.getElementById('multicallSearch').value.toLowerCase();
951 // Selektiert entweder Zeilen oder Karten, je nachdem was sichtbar ist 952 const items = document.querySelectorAll('#multicallContainer .multicall-row, #multicallContainer .multicall-card'); 953 954 items.forEach(item => { 955 const text = item.textContent.toLowerCase(); 956 // Prüft, ob das Element sichtbar ist (um versteckte Tabellenzeilen zu ignorieren, wenn Karten aktiv sind und umgekehrt) 957 const isVisible = item.offsetParent !== null; 958 if (isVisible) { 959 item.style.display = text.includes(searchTerm) ? '' : 'none'; 960 } else if (!text.includes(searchTerm)) { 961 // Wenn es versteckt ist UND nicht passt, explizit auf none setzen 962 item.style.display = 'none'; 963 } else { 964 // Wenn es versteckt ist ABER passt, Display-Style nicht ändern (bleibt durch CSS versteckt) 965 } 966 }); 967 } 968 969 970 static async copy(entryId) { 971 try { 972 // Welle 4: Backend-API statt direkter Firestore-Read 973 const entries = await multiCallApi.list(); 974 const entry = (entries || []).find(e => e && e.id === entryId); 975 if (!entry) { 976 UI.showError('MultiCall-Eintrag nicht gefunden'); 977 return; 978 } 979 980 // Erstelle eine Kopie des Eintrags 981 const copiedEntry = { 982 task: `${entry.task} (Kopie)`, 983 greeting: entry.greeting, 984 message: entry.message, 985 message2: entry.message2, 986 status: entry.status, 987 mulitcall_id: Date.now(), 988 tenantID: entry.tenantID, 989 uid: entry.uid, 990 created_at: serverTimestamp() 991 }; 992 993 await multiCallApi.create(copiedEntry); // phase8 994 995 UI.showSuccess('MultiCall-Eintrag wurde erfolgreich kopiert'); 996 await this.loadEntries(); // Lädt die Einträge neu 997 } catch (error) { 998 console.error('Error copying multicall entry:', error); 999 UI.showError('Fehler beim Kopieren des MultiCall-Eintrags'); 1000 } 1001 } 1002 1003 static async delete(entryId) { 1004 try { 1005 // Bestätigungsdialog anzeigen 1006 const isConfirmed = confirm('Möchten Sie diesen MultiCall-Eintrag wirklich löschen?'); 1007 1008 if (!isConfirmed) { 1009 return; // Abbrechen wenn der Benutzer nicht bestätigt 1010 } 1011 1012 await multiCallApi.delete(entryId); // phase8 1013 1014 // Eintrag aus der UI entfernen (statt neu zu laden) 1015 const itemToRemove = document.querySelector(`.multicall-row[data-entry-id="${entryId}"], .multicall-card[data-entry-id="${entryId}"]`); 1016 if (itemToRemove) { 1017 itemToRemove.remove(); 1018 // Prüfen, ob Container jetzt leer ist 1019 const container = document.getElementById('multicallContainer'); 1020 if (container && !container.querySelector('.multicall-row, .multicall-card')) { 1021 const emptyMessage = `<div class="text-center p-3">${t('Keine MultiCall-Einträge gefunden')}</div>`; 1022 if (this.isMobileView) { 1023 container.innerHTML = emptyMessage; 1024 } else { 1025 const tbody = container.querySelector('tbody'); 1026 if (tbody) tbody.innerHTML = `<tr><td colspan="7" class="text-center">${t('Keine MultiCall-Einträge gefunden')}</td></tr>`; 1027 else container.innerHTML = emptyMessage; // Fallback 1028 } 1029 } 1030 } else { 1031 // Fallback: Liste neu laden, wenn Element nicht gefunden wurde 1032 await this.loadEntries(); 1033 } 1034 1035 UI.showSuccess('MultiCall-Eintrag wurde erfolgreich gelöscht'); 1036 } catch (error) { 1037 console.error('Error deleting multicall entry:', error); 1038 UI.showError('Fehler beim Löschen des MultiCall-Eintrags'); 1039 } 1040 } 1041 1042 static showNewModal() { 1043 const modalHtml = ` 1044 <div class="modal fade" id="newMultiCallModal" tabindex="-1" aria-labelledby="newMultiCallModalLabel" aria-hidden="true"> 1045 <div class="modal-dialog modal-lg"> 1046 <div class="modal-content"> 1047 <div class="modal-header"> 1048 <h5 class="modal-title" id="newMultiCallModalLabel"> 1049 <i class="fas fa-plus me-2"></i>${t('Neuer MultiCall')} 1050 </h5> 1051 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> 1052 </div> 1053 <div class="modal-body"> 1054 <form id="newMultiCallForm"> 1055 <!-- Template-Auswahl --> 1056 <div class="mb-4"> 1057 <label class="form-label">${t('Outbound-Template wählen (optional)')}</label> 1058 <div class="row"> 1059 <div class="col-md-6 mb-2"> 1060 <button type="button" class="btn btn-outline-primary w-100 text-start template-btn" 1061 onclick="MultiCall.applyTemplate('coldCall')"> 1062 <i class="fas fa-phone me-2"></i>${t('Kaltakquise')} 1063 </button> 1064 </div> 1065 <div class="col-md-6 mb-2"> 1066 <button type="button" class="btn btn-outline-primary w-100 text-start template-btn" 1067 onclick="MultiCall.applyTemplate('appointment')"> 1068 <i class="fas fa-calendar-alt me-2"></i>${t('Terminbestätigung')} 1069 </button> 1070 </div> 1071 <div class="col-md-6 mb-2"> 1072 <button type="button" class="btn btn-outline-primary w-100 text-start template-btn" 1073 onclick="MultiCall.applyTemplate('followUp')"> 1074 <i class="fas fa-redo me-2"></i>${t('Follow-up')} 1075 </button> 1076 </div> 1077 <div class="col-md-6 mb-2"> 1078 <button type="button" class="btn btn-outline-primary w-100 text-start template-btn" 1079 onclick="MultiCall.applyTemplate('reactivation')"> 1080 <i class="fas fa-user-clock me-2"></i>${t('Reaktivierung')} 1081 </button> 1082 </div> 1083 <div class="col-md-6 mb-2"> 1084 <button type="button" class="btn btn-outline-primary w-100 text-start template-btn" 1085 onclick="MultiCall.applyTemplate('survey')"> 1086 <i class="fas fa-poll me-2"></i>${t('Umfrage')} 1087 </button> 1088 </div> 1089 <div class="col-md-6 mb-2"> 1090 <button type="button" class="btn btn-outline-primary w-100 text-start template-btn" 1091 onclick="MultiCall.applyTemplate('setter')"> 1092 <i class="fas fa-handshake me-2"></i>${t('Setter (Terminvereinbarung)')} 1093 </button> 1094 </div> 1095 <div class="col-md-6 mb-2"> 1096 <button type="button" class="btn btn-outline-primary w-100 text-start template-btn" 1097 onclick="MultiCall.applyTemplate('closer')"> 1098 <i class="fas fa-trophy me-2"></i>${t('Closer (Verkaufsabschluss)')} 1099 </button> 1100 </div> 1101 </div> 1102 </div> 1103 1104 <hr class="my-4"> 1105 1106 <div class="mb-3"> 1107 <label for="newMultiCallTask" class="form-label">${t('Aufgabe')} <span class="text-danger">*</span></label> 1108 <textarea class="form-control" id="newMultiCallTask" rows="2" required 1109 placeholder="${t('z.B. Terminbestätigung')}">${''}</textarea> 1110 <small class="form-text text-muted">${t('Geben Sie hier den Zweck des MultiCalls ein')}</small> 1111 </div> 1112 1113 <div class="mb-3"> 1114 <label for="newMultiCallGreeting" class="form-label">${t('BegrüÃung')}</label> 1115 <textarea class="form-control" id="newMultiCallGreeting" rows="2" 1116 placeholder="${t('z.B. Guten Tag {name}')}">${t('Guten Tag {name}')}</textarea> 1117 <small class="form-text text-muted">${t('Verwenden Sie {name} als Variable für den Namen des Kontakts')}</small> 1118 </div> 1119 1120 <div class="mb-3"> 1121 <label for="newMultiCallMessage" class="form-label">${t('Nachricht')}</label> 1122 <textarea class="form-control" id="newMultiCallMessage" rows="3" 1123 placeholder="${t('z.B. Ich möchte Sie an Ihren Termin morgen um 14:00 Uhr erinnern.')}"></textarea> 1124 <small class="form-text text-muted">${t('Geben Sie hier die Hauptnachricht ein')}</small> 1125 </div> 1126 1127 <div class="mb-3"> 1128 <label for="newMultiCallMessage2" class="form-label">${t('Details')}</label> 1129 <textarea class="form-control" id="newMultiCallMessage2" rows="3" 1130 placeholder="${t('z.B. Bitte bringen Sie alle relevanten Unterlagen mit.')}"></textarea> 1131 <small class="form-text text-muted">${t('Fügen Sie hier zusätzliche Details oder Anweisungen hinzu')}</small> 1132 </div> 1133 1134 <div class="mb-3"> 1135 <label for="newMultiCallStatus" class="form-label">${t('Status')}</label> 1136 <select class="form-select" id="newMultiCallStatus"> <!-- form-select statt form-control --> 1137 <option value="aktiv">${t('Aktiv')}</option> 1138 <option value="inaktiv">Inaktiv</option> 1139 </select> 1140 <small class="form-text text-muted">${t('Wählen Sie Aktiv für sofortige Aktivierung')}</small> 1141 </div> 1142 </form> 1143 </div> 1144 <div class="modal-footer"> 1145 <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-i18n="Abbrechen">${t('Abbrechen')}</button> 1146 <button type="button" class="btn btn-primary" onclick="MultiCall.saveNew()" data-i18n="Speichern">${t('Speichern')}</button> 1147 </div> 1148 </div> 1149 </div> 1150 </div> 1151 `; 1152 1153 // Remove existing modal if it exists 1154 const existingModal = document.getElementById('newMultiCallModal'); 1155 if (existingModal) { 1156 const modalInstance = bootstrap.Modal.getInstance(existingModal); 1157 if(modalInstance) modalInstance.hide(); 1158 existingModal.remove(); 1159 } 1160 1161 // Add modal to document 1162 document.body.insertAdjacentHTML('beforeend', modalHtml); 1163 1164 // Show modal 1165 const modalElement = document.getElementById('newMultiCallModal'); 1166 if (modalElement) { 1167 const modal = new bootstrap.Modal(modalElement); 1168 // Event listener hinzufügen, um das Modal aus dem DOM zu entfernen, nachdem es geschlossen wurde 1169 modalElement.addEventListener('hidden.bs.modal', event => { 1170 modalElement.remove(); 1171 }); 1172 modal.show(); 1173 } else { 1174 console.error("Konnte neues Modal-Element nicht finden nach dem Einfügen."); 1175 } 1176 } 1177 1178 static async saveNew() { 1179 try { 1180 const task = document.getElementById('newMultiCallTask').value; 1181 const greeting = document.getElementById('newMultiCallGreeting').value; 1182 const message = document.getElementById('newMultiCallMessage').value; 1183 const message2 = document.getElementById('newMultiCallMessage2').value; 1184 const status = document.getElementById('newMultiCallStatus').value; 1185 1186 if (!task) { 1187 UI.showError('Bitte geben Sie eine Aufgabe ein'); 1188 return; 1189 } 1190 1191 const user = Auth.getCurrentUser(); 1192 if (!user) { 1193 UI.showError('Benutzer ist nicht angemeldet'); 1194 return; 1195 } 1196 1197 // Welle 4: Backend-API statt direkter Firestore-Read 1198 const userData = await usersApi.me(); 1199 if (!userData) { 1200 UI.showError('Benutzerdaten nicht gefunden'); 1201 return; 1202 } 1203 const tenantId = userData.tenant_id || userData.tenantID || userData.tenantId; 1204 1205 const newEntry = { 1206 task, 1207 greeting, 1208 message, 1209 message2, 1210 status, 1211 mulitcall_id: Date.now(), 1212 tenantID: tenantId.toString(), 1213 uid: user.uid, 1214 created_at: serverTimestamp() 1215 }; 1216 1217 await multiCallApi.create(newEntry); // phase8 1218 1219 // Close modal 1220 const modalElement = document.getElementById('newMultiCallModal'); 1221 if (modalElement) { 1222 const modal = bootstrap.Modal.getInstance(modalElement); 1223 if (modal) { 1224 modal.hide(); 1225 } else { 1226 // Fallback, falls Instanz nicht gefunden wird 1227 modalElement.classList.remove('show'); 1228 modalElement.style.display = 'none'; 1229 document.body.classList.remove('modal-open'); 1230 const backdrop = document.querySelector('.modal-backdrop'); 1231 if(backdrop) backdrop.remove(); 1232 } 1233 // Das Entfernen des Modals wird durch den 'hidden.bs.modal' Listener gehandhabt 1234 } 1235 1236 // Show success message and refresh list 1237 UI.showSuccess('MultiCall erfolgreich erstellt'); 1238 await this.loadEntries(); // Lädt die Einträge neu 1239 } catch (error) { 1240 console.error('Error creating new multicall:', error); 1241 UI.showError('Fehler beim Erstellen des MultiCall-Eintrags'); 1242 } 1243 } 1244 1245 // Funktion zum Anwenden eines Templates 1246 static applyTemplate(templateName) { 1247 const template = this.outboundTemplates[templateName]; 1248 if (!template) { 1249 console.error('Template nicht gefunden:', templateName); 1250 return; 1251
1251 } 1252 1253 // Felder mit Template-Daten füllen 1254 document.getElementById('newMultiCallTask').value = template.task; 1255 document.getElementById('newMultiCallGreeting').value = template.greeting; 1256 document.getElementById('newMultiCallMessage').value = template.message; 1257 document.getElementById('newMultiCallMessage2').value = template.message2; 1258 1259 // Visuelles Feedback: Markiere ausgewähltes Template 1260 const buttons = document.querySelectorAll('.template-btn'); 1261 buttons.forEach(btn => btn.classList.remove('btn-primary')); 1262 buttons.forEach(btn => btn.classList.add('btn-outline-primary')); 1263 1264 const clickedButton = event.target.closest('.template-btn'); 1265 if (clickedButton) { 1266 clickedButton.classList.remove('btn-outline-primary'); 1267 clickedButton.classList.add('btn-primary'); 1268 } 1269 1270 // Zeige Success-Toast 1271 if (typeof Toastify === 'function') { 1272 Toastify({ 1273 text: `Template "${template.task}" angewendet`, 1274 duration: 2000, 1275 gravity: "top", 1276 position: "right", 1277 backgroundColor: "#28a745" 1278 }).showToast(); 1279 } 1280 } 1281 1282 static async analyzeScript(entryId, templateName) { 1283 // Close the edit modal first 1284 const editModal = document.getElementById('editMultiCallModal'); 1285 if (editModal) { 1286 const modalInstance = bootstrap.Modal.getInstance(editModal); 1287 if (modalInstance) { 1288 modalInstance.hide(); 1289 } 1290 } 1291 1292 // Get the conversion goal from the form before closing 1293 const conversionGoal = document.getElementById('editMultiCallConversionGoal')?.value || 'appointment'; 1294 const customGoal = document.getElementById('editMultiCallCustomGoal')?.value || ''; 1295 1296 // Store the template name, entry ID and conversion goal for the script analysis page 1297 sessionStorage.setItem('analyzeTemplate', templateName); 1298 sessionStorage.setItem('analyzeEntryId', entryId); 1299 sessionStorage.setItem('analyzeConversionGoal', conversionGoal); 1300 if (conversionGoal === 'custom' && customGoal) { 1301 sessionStorage.setItem('analyzeCustomGoal', customGoal); 1302 } 1303 1304 // Navigate to script analysis page 1305 window.location.hash = '#script-analysis'; 1306 } 1307 1308 // New method for voice-initiated calls 1309 static async createAndStartCall(callData) { 1310 try { 1311 const user = Auth.getCurrentUser(); 1312 if (!user) { 1313 throw new Error('User not authenticated'); 1314 } 1315 1316 const tenantId = await Auth.getCurrentTenantId(); 1317 1318 // Create MultiCall entry 1319 /* phase8: collection+addDoc -> multiCallApi.create */ 1320 const newEntry = { 1321 ...callData, 1322 tenantID: tenantId.toString(), 1323 created_at: new Date().toISOString(), 1324 uid: user.uid, 1325 status: 'pending' 1326 }; 1327 const __created2 = await multiCallApi.create(newEntry); 1328 const docRef = { id: __created2.id }; 1329 1330 // Welle 4: Backend-API statt direkter Firestore-Read 1331 const userData = (await usersApi.me()) || {}; 1332 1333 // Check if voice-specific workflow exists 1334 let workflowId = userData?.default_voice_workflow_id; 1335 1336 if (!workflowId) { 1337 // Fall back to default outbound workflow 1338 workflowId = userData?.default_outbound_workflow_id; 1339 } 1340 1341 if (workflowId) { 1342 // Trigger workflow through backend API 1343 const idToken = await user.getIdToken(); 1344 const response = await fetch('/api/multicall/start', { 1345 method: 'POST', 1346 headers: { 1347 'Authorization': `Bearer ${idToken}`, 1348 'Content-Type': 'application/json' 1349 }, 1350 body: JSON.stringify({ 1351 multicallId: docRef.id, 1352 phoneNumber: callData.phone_number, 1353 workflowId: workflowId, 1354 agentPrompt: `${callData.greeting}\n\n${callData.message}`, 1355 metadata: { 1356 initiated_via: 'voice_command', 1357 contact_id: callData.contact_id 1358 } 1359 }) 1360 }); 1361 1362 if (!response.ok) { 1363 const error = await response.json(); 1364 throw new Error(error.message || 'Failed to start call'); 1365 } 1366 1367 const result = await response.json(); 1368 console.log('Voice-initiated call started:', result); 1369 } else { 1370 console.warn('No workflow configured for voice calls'); 1371 UI.showWarning('Bitte konfigurieren Sie zuerst einen Workflow für ausgehende Anrufe.'); 1372 } 1373 1374 return docRef.id; 1375 } catch (error) { 1376 console.error('Error creating voice-initiated call:', error); 1377 throw error; 1378 } 1379 } 1380} 1381 1382// Add MultiCall to window object for event handlers 1383window.MultiCall = MultiCall;
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.