1// Import benötigter Module 2import { Auth } from './auth.js'; 3import UI from './ui.js?v=20260820-dd1-imperr1-bann1-impauth1'; 4// LAZY: import { Tasks } from "./tasks.js"; 5// LAZY: import { Calendar } from './calendar.js?v=20260826-tdup2-badge1-vserie1-it1'; 6// LAZY: import { CallSummary } from './callSummary.js'; 7import { WhiteLabel } from './whitelabel.js'; 8import { Users } from './users.js?v=20260626-merk1-imperr1-bann1-impauth1-kiteam1-bl1h'; 9import { Employees } from './employees.js?v=20260826-empb1-zsui1-kteam1'; 10import { Permissions } from './permissions.js?v=20260727-audiowording-merk1-impauth1-kteam1'; // Rechte & Zugriffe v2 + deep-link 11import { Contacts } from './contacts.js?v=20260807-noshow2-soap1-p5f1-rrev1-bill1-intake1-alarm1-tokfix1-dat1-mod1b-nam1-merk1--kal1-kont1--kd1-prax1-nsl1-nsl2-impauth1-tdup2-dnd1-kf1-kf3-kt3-it1'; 12import 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'; 13import { MultiCall } from './multiCall.js'; 14import { loadDashboardStats } from '../dashboard.js?v=20260826-resfix2'; 15import { loadAnalyticsPage } from './analytics.js?v=20260710-volabel-merk1-ausw1-ausw2--sku1'; 16import { loadRezeptionPage } from './rezeption.js?v=20260805-wlfix1-merk1-rez1-rez2-rezauto1'; 17// AgentsDetail wird lazy geladen (siehe Verwendung in 'agent-details' case) 18// Global Softphone Manager â auto-bootstraps on first import, 19// initializes a headless AgentSoftphone for every agent the user owns 20// that has softphone enabled. This makes inbound calls work on EVERY page, 21// not just the agent detail page. 22import './globalSoftphone.js?v=20260801-wssfb-merk1--sp1--sp2---sp5---obh1-tst1-vj1-spb1-cw1-uebers1'; 23import PhoneNumbers from './phoneNumbers.js?v=20260728-pndelete-merk1-btntext1-numtext1-impauth1-zp1-zp2-zp3-kt1-zalu1-nsauf1'; 24// LAZY: import { SystemInterfaces } from './systemInterfaces.js'; 25import { CalendarIntegration } from './calendarIntegration.js?v=20260826-caltrenn1-lf1-ok1-badge1'; 26import { CRMIntegration } from './crmIntegration.js'; 27import { SocialMediaIntegration } from './socialMediaIntegration.js'; 28import GooglePeopleIntegration from './googlePeople.js'; 29import { AUFGABE_OPTIONS, DEFAULT_INDUSTRIES } from './constants.js'; 30import { emailReader } from './emailReader.js'; 31import { Subscription } from './subscription.js?v=20260805-trialcard-crv2-merk1-impauth1-kl2-dblabo1-planw1-keinabo1-planw2-planw2b-rurl1-kiabo1-abomob1'; 32import { Profile } from './profile.js?v=20260803-itemnotify-crv2-merk1-prax1-prax2--prax2--prax3-pk1b-avv1-nw1-mk2-impauth1-dblabo1-mail1-rurl1-abomob1-avvfirma1-dm1-dm2-it1'; 33import { Language } from './language.js'; 34import { openLeadStatusModal, createCustomLeadStatus } from '../app.js'; 35import { doc, getDoc } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js'; 36import { db } from '../firebase-init.js'; 37// LAZY: import { CallAnalysis } from './callAnalysis.js?v=20260521-perfopt2-merk1-impauth1'; 38// LAZY: import { ApiIntegration } from './apiIntegration.js'; 39// LAZY: import { TicketSystem } from "./ticketSystem.js"; 40import emailAiAgent from './emailAiAgent.js'; // Import the new module 41 42// praxis-modus-v1 (13.08.2026): true, wenn die Praxis-Ansicht gilt - entweder 43// ueber die mediOne-Domain (physio-edition) oder ueber die Betriebsart des 44// Mandanten (physio-menu, gesetzt in app.js aus /api/users/me). 45function _voPraxisAnsicht() { 46 const h = document.documentElement.classList, b = document.body.classList; 47 return h.contains('physio-edition') || b.contains('physio-edition') 48 || h.contains('physio-menu') || b.contains('physio-menu'); 49} 50 51import { WorkflowUI } from './workflows.js'; // Import the new WorkflowUI module 52// LAZY: import { ScriptAnalysis } from './scriptAnalysis.js'; // Import the ScriptAnalysis module 53// LAZY: import { UserCallStats } from './userCallStats.js'; // Import User Call Stats module 54 55// +++ NEU: Singleton-Instanz für AgentsDetail +++ 56let agentsDetailSingleton = null; 57 58// Kurze Ãbersetzungsfunktion für einfacheren Zugriff 59function t(text) { 60 return Language.translate(text); 61} 62 63export class Pages { 64 static currentPageModule = null; 65 66 static isAdmin() { 67 return Auth.currentUser && Auth.currentUser.uid === 'FgYIPgcxuSRLyH1gw9WI0AZ07LO2'; 68 } 69 70 static async isAdminOrReseller() { 71 try { 72 const isAdmin = await Auth.checkAdminAccess(); 73 const isReseller = await Auth.checkResellerAccess(); 74 return isAdmin || isReseller; 75 } catch (error) { 76 console.error('Error checking admin/reseller access:', error); 77 return false;
78 } 79 } 80 81 static cleanupCurrentPage() { 82 // Clean up ads-reports charts specifically 83 if (window.adsReports && window.adsReports.cleanup) { 84 window.adsReports.cleanup(); 85 } 86 87 // Clean up any other page-specific resources 88 if (this.currentPageModule && this.currentPageModule.cleanup) { 89 this.currentPageModule.cleanup(); 90 } 91 } 92 93 static async loadFromHash(hash) { 94 console.log('loadFromHash called with:', hash); 95 const mainContent = document.getElementById('mainContent'); 96 if (!mainContent) { 97 console.error('Main content container not found'); 98 return; 99 } 100 101 try { 102 // Remove the leading '#' 103 let route = hash.substring(1); 104 // Strip optional leading '/' (so '#/permissions' == '#permissions') 105 if (route.startsWith('/')) route = route.substring(1); 106 // Strip query-string â Deep-Link-Params (z.B. ?user=uid) werden 107 // auf Seitenebene via Permissions.parseHashParams() gelesen. 108 const qIdx = route.indexOf('?'); 109 if (qIdx >= 0) route = route.substring(0, qIdx); 110 111 // Example route: contact-detail/<phoneNumber> 112 if (route.startsWith('contact-detail/')) { 113 const phoneNumberEncoded = route.split('/')[1]; 114 if (phoneNumberEncoded) { 115 const phoneNumber = decodeURIComponent(phoneNumberEncoded); 116 console.log(`Hash route matched: contact-detail, Phone: ${phoneNumber}`); 117 118 // --- MODIFIED: Get container reference and pass it to init --- 119 mainContent.innerHTML = `<div id="contactDetailContainer" class="container-fluid"></div>`; // Leeren Container erstellen 120 const containerElement = mainContent.querySelector('#contactDetailContainer'); // Get reference to the new container 121 if (!containerElement) { 122 console.error('Could not find #contactDetailContainer immediately after creating it.'); 123 UI.showError('Fehler beim Initialisieren der Kontaktdetailseite.'); 124 return; 125 } 126 const contactDetail = new ContactDetail(null, phoneNumber); // Instanz mit Telefonnummer erstellen 127 await contactDetail.init(containerElement); // Pass the container element to init 128 // --- END MODIFIED --- 129 130 this.updateActiveNavigation('contacts'); // Highlight contacts in nav 131 } else { 132 console.warn('Phone number missing in hash route:', hash); 133 // Optionally redirect to a default page or show an error 134 await this.loadPage('dashboard'); 135 } 136 } 137 // Handle SIP Settings route 138 else if (route === 'sip-settings') { 139 console.log('Loading SIP Settings page'); 140 // Import and render SIP settings with cache buster 141 const timestamp = new Date().getTime(); 142 const { sipSettings } = await import(`./sipSettings.js?v=${timestamp}`); 143 sipSettings.renderSettingsPage(); 144 this.updateActiveNavigation('sip-settings'); 145 } 146 // Handle Ring Groups route (Hunt-Group / Ring-All) 147 else if (route === 'ring-groups') { 148 console.log('Loading Ring Groups page'); 149 const timestamp = new Date().getTime(); 150 const { ringGroups } = await import(`./ringGroups.js?v=${timestamp}`); 151 await ringGroups.renderPage(); 152 this.updateActiveNavigation('ring-groups'); 153 } 154 // Handle Phone Setup route (Telefon einrichten â eigene Seite, kein Modal) 155 else if (route === 'phone-setup') { 156 console.log('Loading Phone Setup page'); 157 const timestamp = new Date().getTime(); 158 const { PhoneSetupWizard } = await import(`./phoneSetupWizard.js?v=${timestamp}`); 159 await PhoneSetupWizard.renderPage(); 160 this.updateActiveNavigation('phone-numbers'); 161 } 162 // Handle Zeitplan route (Erreichbarkeit je Rufnummer -- eigene Seite, kein Modal) 163 // zeitplan-v4-seite: Muster wie contact-detail/<phone>. Die pnId steht im 164 // Hash, damit die Seite direkt verlinkbar ist und ein Neuladen sie wieder 165 // aufbaut -- beides ging mit dem Modal nicht. 166 else if (route.startsWith('zeitplan/')) { 167 const pnEncoded = route.split('/')[1]; 168 if (pnEncoded) { 169 const pnId = decodeURIComponent(pnEncoded); 170 console.log('Loading Zeitplan page for', pnId); 171 const timestamp = new Date().getTime(); 172 const { Zeitplan } = await import(`./zeitplanDialog.js?v=${timestamp}`); 173 await Zeitplan.renderPage(pnId); 174 this.updateActiveNavigation('phone-numbers'); 175 } else { 176 console.warn('Zeitplan-Route ohne Rufnummern-ID:', hash); 177 await this.loadPage('phone-numbers'); 178 } 179 } 180 // nummernauftritt-v1: Telefonnummer als eigene Seite (Begruessung und 181 // Rolle je Nummer brauchen zwei mehrzeilige Felder - dafuer war im 182 // Modal kein Platz). Muster wie zeitplan/<id>. 183 else if (route.startsWith('nummer/')) { 184 const pnEncoded = route.split('/')[1]; 185 if (pnEncoded) { 186 const pnId = decodeURIComponent(pnEncoded); 187 console.log('Loading Nummer page for', pnId); 188 const timestamp = new Date().getTime(); 189 const { NummerSeite } = await import(`./nummerSeite.js?v=${timestamp}`); 190 await NummerSeite.renderPage(pnId); 191 this.updateActiveNavigation('phone-numbers'); 192 } else { 193 console.warn('Nummer-Route ohne ID:', hash); 194 await this.loadPage('phone-numbers'); 195 } 196 } 197 // Add more hash routes here if needed 198 // else if (route.startsWith('some-other-page/')) { ... } 199 else { 200 console.log('Hash does not match known route, potentially loading page by name:', route); 201 // Fallback: Try loading page by name if hash matches a known page key 202 // This allows linking directly to pages like #contacts 203 await this.loadPage(route); 204 } 205 } catch (error) { 206 console.error('Error loading page from hash:', hash, error); 207 UI.showError('Fehler beim Navigieren zur Seite: ' + error.message); 208 mainContent.innerHTML = '<div class="alert alert-danger">Fehler beim Laden der Seite.</div>'; 209 } 210 } 211 212 /* kiteam-inhaber-v3b (24.09.2026): Darf dieser Nutzer "Mein KI-Team" 213 oeffnen? WORTGLEICH zur Menue-Regel in app.js (_kiTeamSichtbar): 214 Server-Urteil kiteam_inhaber (services/kiteam_inhaber), sonst 215 Inhaber-Rolle + Direktkunde, sonst Mitarbeiter mit Modulrecht. 216 Fail-closed: ohne Profil kein Zugang. */ 217 static async _kiTeamErlaubt() { 218 try { 219 if (await Auth.checkAdminAccess()) return true; 220 const me = await Auth._fetchMe(); 221 if (!me) return false;
222 if (me.kiteam_inhaber === true) return true; 223 const rolle = String(me.role || '').toLowerCase(); 224 const direkt = !me.managed_by && rolle !== 'reseller' && me.whitelabel !== true; 225 if (['admin', 'user'].includes(rolle) && direkt) return true; 226 return rolle === 'employee' && (me.menu_permissions || {}).ai_team === true; 227 } catch (e) { 228 return false; 229 } 230 } 231 232 static async loadPage(page, params = {}) { 233 try { 234 console.log('Loading page:', page, params && Object.keys(params).length ? params : ''); // Debug log 235 236 // Alias: 'agents-detail' (z.B. aus dem KI-Chat) verhaelt sich wie 'agents' 237 // und uebergibt agentId via URL-Query, damit AgentsDetail.init() sie liest. 238 if (page === 'agents-detail') { 239 if (params && params.agentId) { 240 const newSearch = '?page=agents&id=' + encodeURIComponent(params.agentId); 241 window.history.pushState({}, '', window.location.pathname + newSearch + window.location.hash); 242 } 243 page = 'agents'; 244 } else if (page === 'agents') { 245 /* editagent-v2: direkte Listen-Navigation (Sidebar) raeumt stale id aus der URL, 246 damit AgentsDetail.init() die Liste zeigt statt des zuletzt geoeffneten Agenten */ 247 if (new URLSearchParams(window.location.search).get('id')) { 248 window.history.pushState({}, '', window.location.pathname + '?page=agents' + window.location.hash); 249 } 250 } 251 252 // Clean up previous page resources before loading new page 253 this.cleanupCurrentPage(); 254 255 // Clear any previous error messages 256 UI.clearError(); 257 258 // Get main content container first 259 const mainContent = document.getElementById('mainContent'); 260 if (!mainContent) { 261 throw new Error('Main content container not found'); 262 } 263 264 // Ãberprüfe Admin-Berechtigung für geschützte Seiten 265 // ai-team-v1: vorerst nur Admin sichtbar (Michael 10.08.) 266 // kiteam-inhaber-v3b (24.09.2026): Diese Sperre fragte bis heute 267 // role === 'admin' (Auth.checkAdminAccess) - sie wurde bei der 268 // Freigabe fuer Kontoinhaber (kiteam-inhaber-v2, 14.09.) nicht 269 // mitgezogen. Folge: JEDER Nicht-Admin sah am Rechner "Zugriff 270 // verweigert", obwohl Menue und Server ihn durchliessen. Jetzt 271 // dieselbe Regel wie der Menuepunkt in app.js; der Server 272 // (routes/ai_team.py) bleibt die eigentliche Grenze. 273 if (['ai-team', 'ai-postfach', 'ai-dateien'].includes(page) 274 && !(await Pages._kiTeamErlaubt())) { // ai-team-menue-v1 275 mainContent.innerHTML = ` 276 <div class="container py-4"> 277 <div class="alert alert-danger" role="alert"> 278 <h4 class="alert-heading">Zugriff verweigert</h4> 279 <p>âMein KI-Teamâ steht Ihnen nicht zur Verfügung. Der Kontoinhaber kann den Zugriff unter âRechte & Zugriffeâ freigeben.</p> 280 </div> 281 </div> 282 `; 283 return; 284 } 285 286 // kizugang-v1: Der KI-Zugang gilt fuer die ganze Anwendung. 287 // Serverseitig ist er ebenfalls admin-gegatet; das hier ist nur 288 // die freundliche Antwort statt eines rohen 403. 289 if (page === 'ki-zugang' && !(await Auth.checkAdminAccess())) { 290 mainContent.innerHTML = ` 291 <div class="container py-4"> 292 <div class="alert alert-danger" role="alert"> 293 <h4 class="alert-heading">Zugriff verweigert</h4> 294 <p>Nur Administratoren können den KI-Zugang verwalten.</p> 295 </div> 296 </div> 297 `; 298 return; 299 } 300 301 if (page === 'admin-funnel' && !(await Auth.checkAdminAccess())) { 302 mainContent.innerHTML = ` 303 <div class="container py-4"> 304 <div class="alert alert-danger" role="alert"> 305 <h4 class="alert-heading">Zugriff verweigert</h4> 306 <p>Nur Administratoren können das Funnel-Dashboard sehen.</p> 307 </div> 308 </div> 309 `; 310 return; 311 } 312 313 if (page === 'whitelabel' && !this.isAdmin()) { 314 mainContent.innerHTML = ` 315 <div class="container py-4"> 316 <div class="alert alert-danger" role="alert"> 317 <h4 class="alert-heading">Zugriff verweigert</h4> 318 <p>Sie haben keine Berechtigung, diese Seite aufzurufen. Nur Administratoren können auf diese Seite zugreifen.</p> 319 </div> 320 </div> 321 `; 322 return; 323 } 324 325 // Ãberprüfe Admin- oder Reseller-Berechtigung für Benutzerverwaltung 326 if (page === 'users' && !(await this.isAdminOrReseller())) { 327 mainContent.innerHTML = ` 328 <div class="container py-4"> 329 <div class="alert alert-danger" role="alert"> 330 <h4 class="alert-heading">Zugriff verweigert</h4> 331 <p>Sie haben keine Berechtigung, diese Seite aufzurufen. Nur Administratoren und Reseller können auf die Benutzerverwaltung zugreifen.</p> 332 </div> 333 </div> 334 `; 335 return; 336 } 337 338 // Ãberprüfe Berechtigung für Mitarbeiterverwaltung (Admin, Reseller und Tenant-Owner) 339 if (page === 'employees' || page === 'permissions') { 340 const userRole = await Auth.getUserRole(); 341 if (userRole !== 'admin' && userRole !== 'user' && userRole !== 'reseller') { 342 mainContent.innerHTML = ` 343 <div class="container py-4"> 344 <div class="alert alert-danger" role="alert"> 345 <h4 class="alert-heading">Zugriff verweigert</h4> 346 <p>Sie haben keine Berechtigung, diese Seite aufzurufen. Nur Administratoren, Reseller und Tenant-Owner können auf die Rechteverwaltung zugreifen.</p> 347 </div> 348 </div> 349 `; 350 return; 351 } 352 } 353 354 // Ãberprüfe Menüberechtigungen für andere Seiten 355 // WICHTIG: Bei Impersonation (Reseller/Admin Login bei Kunde) die Berechtigungsprüfung überspringen 356 // Quelle: /api/users/me liest die effektive 3-Tier-Kaskade aus Postgres. 357 // Direkter Firestore-Read wurde 2026-05-18 entfernt, weil Firestore-Projekt 358 // crm-v2-q3gwc8 abgeschaltet ist und FS-Cache veraltete Werte liefert. 359 const isImpersonating = localStorage.getItem('impersonation_token'); 360 if (Auth.currentUser && !isImpersonating) { 361 let menuPermissions = null; 362 try { 363 const token = await Auth.getIdToken(); 364 const meResp = await fetch('/api/users/me', { headers: { 'Authorization': `Bearer ${token}` }}); 365 if (meResp.ok) { 366 const me = await meResp.json(); 367 menuPermissions = me.menu_permissions || {}; 368 } 369 } catch (e) { 370 console.warn('Page-Guard: /api/users/me Fehler â Permission-Check übersprungen:', e); 371 } 372 373 if (menuPermissions) { 374 // Prüfe Berechtigung basierend auf der Seite 375 if ( 376 (page === 'agents' && menuPermissions.inbound === false) || 377 (page === 'inbox' && menuPermissions.inbox === false) || 378 (page === 'system-interfaces' && menuPermissions.system_interfaces === false) || 379 (page === 'multicall' && menuPermissions.outbound === false) ||
380 (page === 'deals' && menuPermissions.deals === false) || 381 (page === 'rezeption' && menuPermissions.rezeption !== true) || 382 (page === 'verordnungen' && menuPermissions.verordnungen !== true) || 383 /* perm-page-rights-v1: eigene Rechte statt Kalender-Recht. 384 Ohne dieses Gate bliebe die Seite ueber die Adresszeile 385 erreichbar, auch wenn der Menuepunkt versteckt ist. */ 386 (page === 'no-show' && menuPermissions.no_show !== true) || 387 (page === 'reviews' && menuPermissions.reviews !== true) || 388 (page === 'anamnese-templates' && menuPermissions.anamnese !== true) || 389 (page === 'tickets' && menuPermissions.tickets !== true) || 390 /* adminperm-v1 */ 391 (page === 'users' && menuPermissions.users !== true) || 392 (page === 'employees' && menuPermissions.employees !== true) || 393 (page === 'permissions' && menuPermissions.permissions !== true) || 394 (page === 'heygen-avatars' && menuPermissions.avatars !== true) || 395 (page === 'video-rooms' && menuPermissions.video_rooms !== true) || 396 (page.startsWith('erp-') && menuPermissions.erp === false) || 397 /* perm-accounting-menu-v1: fail-closed wie die uebrigen Opt-in-Seiten. 398 '=== false' liess die Seite durch, sobald der Schluessel fehlte. */ 399 (page.startsWith('accounting-') && menuPermissions.accounting !== true) 400 ) { 401 mainContent.innerHTML = ` 402 <div class="container py-4"> 403 <div class="alert alert-danger" role="alert"> 404 <h4 class="alert-heading">Zugriff verweigert</h4> 405 <p>Sie haben keine Berechtigung, diese Seite aufzurufen.</p> 406 </div> 407 </div> 408 `; 409 return; 410 } 411 } 412 } 413 414 // Parse page name and query parameters (e.g., 'user-call-stats?userId=xxx') 415 const [pageName, queryString] = page.split('?'); 416 const pageParams = new URLSearchParams(queryString || ''); 417 418 // Update active navigation link 419 this.updateActiveNavigation(pageName); 420 421 // Clear previous content 422 mainContent.innerHTML = ''; 423 424 switch (pageName) { 425 case 'dashboard': 426 return await this.loadDashboard(); 427 case 'analytics': 428 return await this.loadAnalytics(); 429 case 'rezeption': 430 return await this.loadRezeption(); 431 case 'documentation': 432 return await this.loadDocumentation(); 433 case 'calendar-integration': 434 return await CalendarIntegration.configure(); 435 case 'social-media-integration': 436 return await SocialMediaIntegration.configure(); 437 case 'deals': 438 return await this.loadDeals(); 439 case 'email': 440 case 'inbox': 441 console.log('Loading email page'); 442 return await emailReader.init(); 443 case 'booking': 444 console.log('Loading booking page'); 445 const userId = window.location.pathname.split('/').pop(); 446 mainContent.innerHTML = ` 447 <div id="bookingContainer" class="container-fluid"> 448 <div id="calendar"></div> 449 <div id="timeSlots" class="mt-4" style="display: none;"> 450 <h3>Verfügbare Zeiten</h3> 451 <div id="timeSlotsList" class="list-group"></div> 452 </div> 453 <div id="bookingForm" class="mt-4" style="display: none;"> 454 <div class="card"> 455 <div class="card-body"> 456 <h3>Termindetails</h3> 457 <form id="appointmentForm"> 458 <div class="mb-3"> 459 <label for="name" class="form-label">
459Name</label> 460 <input type="text" class="form-control" id="name" required> 461 </div> 462 <div class="mb-3"> 463 <label for="email" class="form-label">E-Mail</label> 464 <input type="email" class="form-control" id="email" required> 465 </div> 466 <div class="mb-3"> 467 <label for="phone" class="form-label">Telefon</label> 468 <input type="tel" class="form-control" id="phone" required> 469 </div> 470 <div class="mb-3"> 471 <label for="notes" class="form-label">Anmerkungen</label> 472 <textarea class="form-control" id="notes" rows="3"></textarea> 473 </div> 474 <button type="submit" class="btn btn-primary">${t('Termin buchen')}</button> 475 </form> 476 </div> 477 </div> 478 </div> 479 </div> 480 `; 481 482 // Initialize the booking functionality 483 const bookingScript = document.createElement('script'); 484 bookingScript.type = 'module'; 485 bookingScript.src = '/static/modules/booking.js'; 486 document.body.appendChild(bookingScript); 487 break; 488 case 'contact-detail': 489 console.log('Loading contact detail page'); // Debug log 490 const contactId = new URLSearchParams(window.location.search).get('id'); 491 mainContent.innerHTML = ` 492 <div id="contactDetailContainer" class="container-fluid"> 493 <div class="text-center mt-5"> 494 <div class="spinner-border" role="status"> 495 <span class="visually-hidden">Loading...</span> 496 </div> 497 </div> 498 </div> 499 `; 500 501 // Ensure container exists before initializing 502 const container = document.getElementById('contactDetailContainer'); 503 if (!container) { 504 console.error('Contact detail container not found after creation'); 505 return; 506 } 507 508 const contactDetail = new ContactDetail(contactId); 509 await contactDetail.init(container); // Pass the container element 510 break; 511 case 'contacts': 512 return await this.loadContacts(); 513 break; 514 case 'calendar': 515 return await this.loadCalendar(); 516 case 'calendar-settings': 517 return await this.loadCalendarSettings(); 518 case 'calendar-new-event': 519 return await this.loadCalendarNewEvent(); 520 case 'calendar-team': 521 return await this.loadCalendarTeam(); 522 case 'patient-notifications': // pn-settings-ui-v1 523 return await this.loadPatientNotifySettings(); 524 case 'ai-team': // ai-team-ui-v1 (nur Admin) 525 return await this.loadAiTeam(); 526 case 'ai-postfach': // ai-team-menue-v1 527 return await this.loadAiTeam({ reiter: 'postfach' }); 528 case 'ai-dateien': // ai-team-menue-v1 529 return await this.loadAiTeam({ reiter: 'dateien' }); 530 case 'no-show': // no-show-ui-v1 531 return await this.loadNoShow(); 532 case 'reviews': // reviews-ui-v1 533 return await this.loadReviews(); 534 case 'anamnese-templates': // anamnese-route-v1 535 return await this.loadAnamneseTemplates(); 536 case 'mytask': 537 case 'tasks': 538 return await this.loadTasks(); 539 case 'tickets': 540 return await this.loadTickets(); 541 case 'verordnungen': 542 return await this.loadVerordnungen(); 543 // Accounting Pages (Buchhaltung - nur Admin) 544 case 'accounting-dashboard': 545 return await this.loadAccountingPage('dashboard'); 546 case 'accounting-bookings': 547 return await this.loadAccountingPage('bookings'); 548 case 'accounting-receipts': 549 try { 550 const accRecMod = await import('./accountingReceipts.js?v=20260807-fix-merk1-belegid1-fbtn1'); 551 accRecMod.loadAccountingReceiptsPage(); 552 } catch (err) { 553 console.error('Error loading Receipts:', err); 554 mainContent.innerHTML = '<div class="alert alert-danger">Fehler: ' + err.message + '</div>'; 555 } 556 break; 557 case 'accounting-reports': 558 try { 559 const accRepMod = await import('./accountingReports.js?v=20260807-tokens-merk1'); 560 accRepMod.loadAccountingReportsPage(); 561 } catch (err) { 562 console.error('Error loading Reports:', err); 563 mainContent.innerHTML = '<div class="alert alert-danger">Fehler: ' + err.message + '</div>'; 564 } 565 break; 566 case 'accounting-tax': 567 return await this.loadAccountingPage('tax'); 568 case 'accounting-datev': 569 return await this.loadAccountingPage('datev'); 570 case 'accounting-settings': 571 return await this.loadAccountingPage('settings'); 572 // Google Ads Pages 573 case "ads-dashboard": 574 case "ads-setup": 575 case "ads-settings": 576 case "ads-campaigns": 577 case "ads-campaigns-new": // New campaign creator with GPT-4 578 case "ads-keywords": 579 case "ads-alerts": 580 case "ads-automation": 581 case "ads-reports": 582 case "ads-bulk":
583 case "tracking-manager": 584 case "tracking-dashboard": 585 return await this.loadAdsPage(page); 586 case 'call-detail': // calldetail-v1 587 return await this.loadCallDetail(); 588 case 'call-summary': 589 return await this.loadCallSummary(); 590 case 'call-analysis': 591 // Add the basic HTML structure for the call analysis page with Tabs 592 mainContent.innerHTML = ` 593<style> 594 /* Header-Styling für Konsistenz */ 595 .callanalysis-header { 596 display: flex; 597 justify-content: space-between; 598 align-items: center; 599 padding: 1.5rem; 600 background: linear-gradient(135deg, #ffffff 0%, #f5f5f5 100%); 601 border-radius: 8px; 602 box-shadow: 0 2px 4px rgba(0,0,0,0.05); 603 margin-bottom: 2rem; 604 } 605 606 .callanalysis-header h1 { 607 margin: 0; 608 font-size: 1.5rem; 609 color: #2e2e2e; 610 } 611 612 .callanalysis-header-actions { 613 display: flex; 614 gap: 0.5rem; 615 align-items: center; 616 } 617</style> 618<div class="container-fluid mt-4"> 619 <div class="callanalysis-header"> 620 <h1>${t('Gesprächsanalyse')}</h1> 621 <div class="callanalysis-header-actions"> 622 <!-- Platzhalter für zukünftige Action-Buttons --> 623 </div> 624 </div> 625 626 <!-- Nav tabs --> 627 <ul class="nav nav-tabs mb-3" id="callAnalysisTabs" role="tablist"> 628 <li class="nav-item" role="presentation"> 629 <button class="nav-link active" id="inbound-tab" data-bs-toggle="tab" data-bs-target="#inbound-tab-pane" type="button" role="tab" aria-controls="inbound-tab-pane" aria-selected="true">${t('Eingehend')}</button> 630 </li> 631 <li class="nav-item" role="presentation"> 632 <button class="nav-link" id="outbound-tab" data-bs-toggle="tab" data-bs-target="#outbound-tab-pane" type="button" role="tab" aria-controls="outbound-tab-pane" aria-selected="false">${t('Ausgehend')}</button> 633 </li> 634 </ul> 635 636 <!-- Tab panes --> 637 <div class="tab-content" id="callAnalysisTabContent"> 638 <!-- Inbound Tab Pane --> 639 <div class="tab-pane fade show active" id="inbound-tab-pane" role="tabpanel" aria-labelledby="inbound-tab" tabindex="0"> 640 <div class="card shadow mb-4"> 641 <div class="card-header py-3 d-flex flex-row align-items-center justify-content-between"> 642 <h6 class="m-0 font-weight-bold text-primary">${t('Analyse Eingehender Anrufe')}</h6> 643 </div> 644 <div class="card-body"> 645 <div id="inboundLoadingIndicator" class="text-center" style="display: none;"> 646 <div class="spinner-border text-primary" role="status"> 647 <span class="visually-hidden">Loading...</span> 648 </div> 649 <p>Lade eingehende Anrufdaten...</p> 650 </div> 651 <div id="inboundCallAnalysis"> 652 <!-- Inbound charts and data will be rendered here --> 653 <p id="noInboundDataMessage" class="text-muted text-center" style="display: none;">Keine Daten für eingehende Anrufe gefunden.</p> 654 <canvas id="inboundSentimentChart" style="display: none;"></canvas> 655 <canvas id="inboundKeywordsChart" style="display: none;"></canvas> 656 <div id="inboundSummaries" class="mt-3" style="max-height: 300px; overflow-y: auto;"></div> 657 <!-- Placeholder for AI Analysis --> 658 <div id="inboundChatGPTAnalysis" class="mt-4"></div> 659 <div id="inboundMetricsCards" class="row mt-4"></div> 660 </div> 661 </div> 662 </div> 663 </div> 664 665 <!-- Outbound Tab Pane --> 666 <div class="tab-pane fade" id="outbound-tab-pane" role="tabpanel" aria-labelledby="outbound-tab" tabindex="0"> 667 <div class="card shadow mb-4"> 668 <div class="card-header py-3 d-flex flex-row align-items-center justify-content-between"> 669 <h6 class="m-0 font-weight-bold text-success">${t('Analyse Ausgehender Anrufe')}</h6> 670 </div> 671 <div class="card-body"> 672 <div id="outboundLoadingIndicator" class="text-center" style="display: none;"> 673 <div class="spinner-border text-success" role="status">
674 <span class="visually-hidden">Loading...</span> 675 </div> 676 <p>Lade ausgehende Anrufdaten...</p> 677 </div> 678 <div id="outboundCallAnalysis"> 679 <!-- Outbound charts and data will be rendered here --> 680 <p id="noOutboundDataMessage" class="text-muted text-center" style="display: none;">Keine Daten für ausgehende Anrufe gefunden.</p> 681 <canvas id="outboundSentimentChart" style="display: none;"></canvas> 682 <canvas id="outboundKeywordsChart" style="display: none;"></canvas> 683 <div id="outboundSummaries" class="mt-3" style="max-height: 300px; overflow-y: auto;"></div> 684 <!-- Placeholder for AI Analysis --> 685 <div id="outboundChatGPTAnalysis" class="mt-4"></div> 686 <div id="outboundMetricsCards" class="row mt-4"></div> 687 </div> 688 </div> 689 </div> 690 </div> 691 </div> 692</div> 693 `; 694 return await this.loadCallAnalysis(); 695 case 'whitelabel': 696 return await this.loadWhitelabel(); 697 case 'mitarbeiterverzeichnis': { 698 const { PersonenVerzeichnis } = await import('./personenVerzeichnis.js?v=20260909-personen4-pvbreit5-pvux2'); 699 await PersonenVerzeichnis.load(); 700 break; 701 } 702 case 'ki-zugang': { 703 const { KiZugang } = await import('./kiZugang.js?v=20260817-kiz3---kiz5---tok1---tok2---tok3---kizan1---kk1---kk2---kizenter1---kizkonto1-kizdirekt1'); 704 await KiZugang.load(); 705 break; 706 } 707 case 'admin-funnel': { 708 const { AdminFunnel } = await import(`./adminFunnel.js?v=${Date.now()}`); 709 await AdminFunnel.load(); 710 break; 711 } 712 case 'users': 713 return await Users.load(); 714 case 'user-call-stats': { 715 const { UserCallStats } = await import("./userCallStats.js"); 716 // loadFromHash strippt den Query-String bevor loadPage aufgerufen wird, 717 // daher fallen pageParams hier oft leer aus. Wir lesen die Params zusaetzlich 718 // aus window.location.hash, wo der Browser den Original-Hash inkl. Query haelt. 719 const _rawHash = window.location.hash || ''; 720 const _qStart = _rawHash.indexOf('?'); 721 const _hashParams = new URLSearchParams(_qStart >= 0 ? _rawHash.substring(_qStart + 1) : ''); 722 const statsUserId = pageParams.get('userId') || _hashParams.get('userId'); 723 const statsUserName = pageParams.get('userName') || _hashParams.get('userName'); 724 await UserCallStats.load(statsUserId, statsUserName); 725 break; 726 } 727 728 729 case 'accounting-bank': 730 try { 731 const accBankMod = await import('./accountingBank.js?v=20260807-acctnames-merk1'); 732 accBankMod.loadAccountingBankPage(); 733 } catch (error) { 734 console.error('Error loading Accounting Bank:', error); 735 mainContent.innerHTML = '<div class="alert alert-danger">Fehler beim Laden der Bank-Integration: ' + error.message + '</div>'; 736 } 737 break; 738 case 'employees': 739 return await Employees.load(); 740 case 'permissions': 741 return await Permissions.load(); 742 case 'create-user': 743 return await this.loadCreateUser(); 744 case 'multicall': 745 await MultiCall.load(); 746 break; 747 case 'script-analysis': { 748 const { ScriptAnalysis } = await import('./scriptAnalysis.js'); 749 await ScriptAnalysis.load(); 750 break; 751 } 752 // sperrliste-v1: Gesperrte Anrufer (Wunsch T-2026-0061) 753 case 'blocked-callers': { 754 const { BlockedCallers } = await import(`./blockedCallers.js?v=${Date.now()}`); 755 mainContent.innerHTML = BlockedCallers.getPage(); 756 await BlockedCallers.initialize(); 757 break; 758 } 759 case 'phone-numbers': 760 mainContent.innerHTML = PhoneNumbers.getPage(); 761 await PhoneNumbers.initialize(); 762 break; 763 // DEPRECATED: Tavus Avatar - ersetzt durch LiveAvatar 764 // case 'tavus-replicas': 765 // const timestamp = new Date().getTime(); 766 // const { TavusReplicas } = await import(`./tavusReplicas.js?v=${timestamp}`); 767 // if (!window.tavusReplicas) { 768 // window.tavusReplicas = new TavusReplicas(); 769 // } else { 770 // window.tavusReplicas.render(); 771 // } 772 // break; 773 774 // liveavatar-removed-20260509 775 case 'heygen-avatars': 776 // Load HeyGen Avatar Management page 777 try { 778 const response = await fetch('/static/heygen-avatars.html'); 779 if (!response.ok) { 780 throw new Error('Failed to load HeyGen Avatars page'); 781 } 782 const html = await response.text(); 783 mainContent.innerHTML = html; 784 785 // Execute scripts in the loaded HTML (handles both src= and inline, module + classic) 786 const scripts = mainContent.querySelectorAll('script'); 787 const loadScript = (script) => new Promise((resolve) => { 788 const newScript = document.createElement('script'); 789 if (script.type) newScript.type = script.type; 790 if (script.src) { 791 newScript.src = script.src; 792 newScript.onload = () => resolve(); 793 newScript.onerror = () => resolve(); 794 } else { 795 newScript.textContent = script.textContent; 796 resolve(); 797 } 798 document.body.appendChild(newScript); 799 if (!script.src) resolve(); 800 }); 801 // Load sequentially so libs (src) finish before inline init scripts run 802 (async () => { 803 for (const script of scripts) { 804 await loadScript(script); 805 } 806 })(); 807 808 // Make the page visible after DOM update - use nextTick 809 setTimeout(() => { 810 const heygemPage = document.getElementById('heygen-avatars-page'); 811 if (heygemPage) { 812 heygemPage.style.display = 'block'; 813 heygemPage.style.visibility = 'visible'; 814 heygemPage.style.opacity = '1'; 815 console.log('â HeyGen page made visible'); 816 } else { 817 console.error('â heygen-avatars-page element not found'); 818 } 819 }, 100); 820 } catch (error) { 821 console.error('Error loading HeyGen Avatars:', error); 822 mainContent.innerHTML = ` 823 <div class="alert alert-danger"> 824 Fehler beim Laden der Avatar-Verwaltung: ${error.message} 825 </div> 826 `; 827 } 828 break; 829 830 case 'avatar-lab': 831 // Avatar Lab - Admin only 2D avatar development 832 await import('./avatarLab.js'); 833 if (window.avatarLab) { 834 await window.avatarLab.init(); 835 } 836 break; 837 case 'video-rooms': { 838 const ts = Date.now(); 839 const mod = await import(`./videoRooms.js?v=${ts}`); 840 await mod.initVideoRoomsPage(); 841 this.updateActiveNavigation('video-rooms'); 842 break; 843 } 844 case 'ring-groups': { 845 const timestamp = new Date().getTime(); 846 const { ringGroups } = await import(`./ringGroups.js?v=${timestamp}`); 847 await ringGroups.renderPage(); 848 this.updateActiveNavigation('ring-groups'); 849 break; 850 } 851 case 'softphone-page': 852 // Full-page softphone view 853 mainContent.innerHTML = ` 854 <div class="page-header d-flex justify-content-between align-items-center mb-4"> 855 <div> 856 <h2><i class="fas fa-headset me-2" style="color:#7a9db5"></i>Softphone</h2> 857 <p class="text-muted mb-0">Anrufe tätigen und empfangen direkt im Browser</p> 858 </div> 859 </div> 860 <div class="row justify-content-center"> 861 <div class="col-md-5 col-lg-4"> 862 <div class="card shadow-sm" style="border-radius:16px;overflow:hidden"> 863 <div class="card-body p-0" id="softphonePageContainer"> 864 <p class="text-muted text-center py-5">Softphone wird geladen...</p> 865 </div> 866 </div> 867 </div> 868 </div> 869 `; 870 // The FAB's panel & agent-select work globally, 871 // this page just provides a dedicated entry point. 872 // Auto-open the FAB panel if not already open 873 try { 874 const { softphoneFab: spFab }
874 = await import('./softphoneFab.js?v=20260714-regsync-merk1-wm1---obh1'); 875 setTimeout(() => { if (spFab && !spFab._open) spFab._openPanel(); }, 300); 876 } catch (e) { console.warn('Softphone page init:', e); } 877 break; 878 case 'pbx-dashboard': 879 // Dynamically import the PBX Dashboard module 880 try { 881 const PBXDashboardModule = await import('./pbxDashboard.js'); 882 const PBXDashboard = PBXDashboardModule.default || PBXDashboardModule.PBXDashboard || PBXDashboardModule; 883 884 if (PBXDashboard && PBXDashboard.render) { 885 mainContent.innerHTML = PBXDashboard.render(); 886 PBXDashboard.init(); 887 } else { 888 console.error('PBXDashboard module not properly loaded'); 889 mainContent.innerHTML = '<div class="alert alert-danger">Fehler beim Laden des PBX Dashboards</div>'; 890 } 891 } catch (error) { 892 console.error('Error loading PBX Dashboard:', error); 893 mainContent.innerHTML = '<div class="alert alert-danger">Fehler beim Laden des PBX Dashboards: ' + error.message + '</div>'; 894 } 895 break; 896 897 // ============================================================================ 898 // ERP MODULE PAGES 899 // ============================================================================ 900 case 'erp-dashboard': 901 case 'erp-invoices': 902 case 'erp-products': 903 case 'erp-suppliers': 904 case 'erp-orders': 905 case 'erp-projects': 906 case 'erp-timesheets': 907 case 'erp-quotes': 908 case 'erp-delivery-notes': 909 case 'erp-recurring-invoices': 910 // erp-ausw-menue-v1: page.replace('erp-','') ergibt 'reports', 911 // navigateTo('reports') ruft loadReportsPage(). Kein Sonderfall noetig. 912 case 'erp-reports': 913 // Load ERP module 914 try { 915 const erpVersion = "20260827_rechspalten2-verl1-kf3"; 916 mainContent.innerHTML = ` 917 <link rel="stylesheet" href="/static/css/erp.css?v=${erpVersion}"> 918 <div id="erp-content" class="erp-container"> 919 <div class="text-center mt-5"> 920 <div class="spinner-border" role="status"> 921 <span class="visually-hidden">Loading...</span> 922 </div> 923 </div> 924 </div> 925 `; 926 927 const erpModule = await import(`./erp.js?v=20260908-projbug1-${erpVersion}`); 928 await erpModule.initERP(); 929 930 // Navigate to specific sub-module based on page 931 const subModule = page.replace('erp-', ''); 932 if (subModule !== 'dashboard') { 933 erpModule.navigateTo(subModule === 'orders' ? 'purchase-orders' : subModule); 934 } 935 } catch (error) { 936 console.error('Error loading ERP module:', error); 937 mainContent.innerHTML = '<div class="alert alert-danger">Fehler beim Laden des ERP-Moduls: ' + error.message + '</div>'; 938 } 939 break; 940 941 case 'agents': 942 mainContent.innerHTML = ` 943 <div id="agentDetailContainer" class="container-fluid"> 944 <div class="text-center mt-5"> 945 <div class="spinner-border" role="status"> 946 <span class="visually-hidden">Loading...</span> 947 </div> 948 </div> 949 </div> 950 `; 951 952 // +++ NEU: Singleton-Logik +++ 953 if (!agentsDetailSingleton) { 954 console.log("Creating new AgentsDetail singleton instance."); 955 // Create an instance WITHOUT passing auth/db 956 const { AgentsDetail } = await import('./agentsDetail.js?v=20260806-migrateoutline1-gpuoff2-gpuoff3-merk1--sp1--sp2--sp3---sp4---tc1---gem1---vpq1---vpq2---gflach1---obh1-anvor1-anvor2-g35t1-iwrt3-iwbt1-iwall1-iwdd1-sonic36-feablauf1-feablauf2-feablauf2b-feablauf2c-feablauf2d-feablauf2e-feablauf2f-feablauf3-piiredakt1-piiumzug1-piikat1-piip3-piihinw1-piiname1-gstt1-smallweg1-gsttaus1-sttboost1-boostph1-boostui1-umlaut1-fish6-tur
956n1-atwien1-atlock1-wienraus1-premat1-atprem1-promptan1b-uml1-agentvoll1-paloesch1-gptlive1-vpoa1-piiwirkt1-uebmob1-uebfix1-ansage1-prov1-gptvoice2-tfueber3-tfname1-hkarte2-gemlive30-bonsai1-bonsairaus1-dm7-klon1-import1-loesch1-elprof1-elprof2-ragz1-btraus1-thraus1-elprof3-kie2-tfneu1-kie3'); agentsDetailSingleton = new AgentsDetail(); window.agentsDetail = agentsDetailSingleton; 957 } else { 958 console.log("Reusing existing AgentsDetail singleton instance."); 959 } 960 // Rufe init auf der Singleton-Instanz auf 961 await agentsDetailSingleton.init(); 962 // +++ ENDE NEU +++ 963 break; 964 case 'crm': 965 await CRMIntegration.configure(); 966 break; 967 case 'system-interfaces': { 968 const { SystemInterfaces } = await import('./systemInterfaces.js?v=20260625-shopware-merk1-dd1-numtext1-impauth1-zp1-zp3-zalu1-bowl1-wamandant1-tokhinw1-umlfix1-waueb1-waemp1'); 969 return await SystemInterfaces.load(); 970 } 971 case 'subscription': 972 console.log('Loading subscription page'); 973 return await Subscription.loadSubscriptionPage(); 974 case 'affiliate': { 975 console.log('Loading affiliate page'); 976 const { Affiliate } = await import('./affiliate.js?v=20260611-assign-merk1'); 977 return await Affiliate.loadAffiliatePage(); 978 } 979 case 'profile': 980 console.log('Loading profile page'); 981 return await Profile.loadUserProfile(); 982 case 'api-integration': { 983 const { ApiIntegration } = await import('./apiIntegration.js'); 984 console.log('Loading API integration page'); 985 return await ApiIntegration.load(); 986 } 987 case 'google-contacts-integration': 988 console.log('Loading Google Contacts integration page'); 989 return await this.loadGoogleContactsIntegration(); 990 case 'email-ai-agent': { 991 // ai-team-emailagent-abloesung-v1: Der alte "E-Mail KI 992 // Agent" hatte 0 Nutzer und kein Backend. Wer den alten 993 // Link noch als Lesezeichen hat, landet mit einer 994 // Erklaerung beim KI-Team, statt vor einer toten Seite. 995 mainContent.innerHTML = 996 '<div class="container-fluid py-4">' 997 + '<div class="card"><div class="card-body">' 998 + '<h5><i class="fas fa-users-gear me-2"></i>Der E-Mail-Assistent ist umgezogen</h5>' 999 + '<p class="text-muted mb-3">Er ist jetzt Teil von <strong>Mein KI-Team</strong>. ' 1000 + 'Dort richten Sie das Postfach einmal unter âZugängeâ ein und weisen es ' 1001 + 'den Mitarbeitern zu, die damit arbeiten sollen.</p>' 1002 + '<a href="#ai-team" class="btn btn-primary">Zu âMein KI-Teamâ</a>' 1003 + '</div></div></div>'; 1004 break; 1005 } 1006 case 'automation-studio': // n8n Automation Studio 1007 console.log('Loading Automation Studio (n8n)'); 1008 if (window.WorkflowAutomation) { 1009 // Module is already loaded 1010 window.WorkflowAutomation.init(); 1011 const automationContainer = window.WorkflowAutomation.render(); 1012 mainContent.innerHTML = ''; 1013 mainContent.appendChild(automationContainer); 1014 } else { 1015 // Module not loaded yet, show error or placeholder 1016 mainContent.innerHTML = ` 1017 <style> 1018 /* Header-Styling für Konsistenz */ 1019 .automation-header { 1020 display: flex; 1021 justify-content: space-between; 1022 align-items: center; 1023 padding: 1.5rem; 1024 background: linear-gradient(135deg, #ffffff 0%, #f5f5f5 100%); 1025 border-radius: 8px; 1026 box-shadow: 0 2px 4px rgba(0,0,0,0.05); 1027 margin-bottom: 2rem; 1028 } 1029 1030 .automation-header h1 { 1031 margin: 0; 1032 font-size: 1.5rem; 1033 color: #2e2e2e; 1034 } 1035 1036 .automation-header-actions { 1037 display: flex; 1038 gap: 0.5rem; 1039 align-items: center; 1040 } 1041 </style> 1042 <div class="container-fluid mt-4"> 1043 <div class="automation-header"> 1044 <h1>${t('Automation Studio')}</h1> 1045 <div class="automation-header-actions"> 1046 <!-- Platzhalter für zukünftige Action-Buttons --> 1047 </div> 1048 </div> 1049 <div class="alert alert-warning"> 1050 <i class="fas fa-exclamation-triangle"></i> ${t('Das Automation Studio Modul konnte nicht geladen werden.')} 1051 <br> 1052 <small>${t('Bitte aktualisieren Sie die Seite oder kontaktieren Sie den Support.')}</small> 1053 </div> 1054 </div> 1055 `; 1056 } 1057 break; 1058 case 'workflows': // Original workflows case 1059 console.log('Loading Workflows page'); 1060 // Keep the existing workflow UI 1061 mainContent.innerHTML = ` 1062 <div class="container-fluid mt-4"> 1063 <div id="workflow-management-section"> 1064 <!-- Workflow UI will be initialized here --> 1065 <div class="text-center mt-5"> 1066 <div class="spinner-border" role="status">
1067 <span class="visually-hidden">Loading...</span> 1068 </div> 1069 <p>Lade Workflow-Modul...</p> 1070 </div> 1071 </div> 1072 </div> 1073 `; 1074 // Initialize the Workflow UI 1075 // Ensure the container exists before initializing 1076 const workflowContainer = document.getElementById('workflow-management-section'); 1077 if (workflowContainer) { 1078 const workflowUI = new WorkflowUI(); 1079 // workflowUI.loadWorkflows(); // You might call initial loading functions here 1080 // workflowContainer.innerHTML = ''; // REMOVED: Clear spinner once JS is ready (or handle loading within WorkflowUI) 1081 // TODO: Add more sophisticated loading/rendering logic within WorkflowUI class 1082 // workflowContainer.innerHTML = '<p>Workflow-Verwaltung (Platzhalter).</p>'; // REMOVED: Simple placeholder text for now 1083 } else { 1084 console.error('Workflow container #workflow-management-section not found after creation'); 1085 } 1086 break; 1087 case 'shop-payment-providers': 1088 try { 1089 mainContent.innerHTML = ` 1090 <div class="container-fluid mt-4"> 1091 <div class="d-flex justify-content-between align-items-center mb-4"> 1092 <h4 class="mb-0">Zahlungsanbieter</h4> 1093 </div> 1094 <div id="shopPpLoading" class="text-center py-5"><div class="spinner-border"></div><p class="text-muted mt-2">Lade Konfigurationâ¦</p></div> 1095 <div id="shopPpContent" style="display:none"></div> 1096 </div>`; 1097 1098 const PP = window.ShopPaymentProviders = {}; 1099 PP.TENANT = ''; 1100 PP._esc = (s) => (s == null ? '' : String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')); 1101 PP._fmtDate = (iso) => { if (!iso) return 'nie'; try { return new Date(iso).toLocaleString('de-DE'); } catch(e) { return iso; } }; 1102 1103 PP.render = async () => { 1104 try { 1105 const token = await Auth.getIdToken(); 1106 const r = await fetch(`/api/shop/admin/payment-providers`, { headers: { 'Authorization': `Bearer ${token}` } }); 1107 if (!r.ok) throw new Error(`HTTP ${r.status}`); 1108 const data = await r.json(); 1109 PP._data = data; 1110 const s = data.stripe || {}; 1111 const inv = data.invoice || { enabled: true }; 1112 const content = document.getElementById('shopPpContent'); 1113 content.innerHTML = ` 1114 <div class="row g-4"> 1115 <div class="col-lg-8"> 1116 <div class="card shadow-sm border-0 mb-4"> 1117 <div class="card-header bg-white border-bottom"> 1118 <div class="d-flex align-items-center justify-content-between"> 1119 <div> 1120 <h5 class="mb-0"><span style="display:inline-block;background:#635BFF;color:#fff;font-weight:700;padding:2px 10px;border-radius:4px;font-size:0.85em;letter-spacing:0.3px;vertical-align:middle">stripe</span></h5> 1121 <small class="text-muted">Kreditkarte, Apple Pay, Google Pay, PayPal, Klarna, SEPA u.v.m.</small> 1122 </div> 1123 <div class="form-check form-switch"> 1124 <input class="form-check-input" type="checkbox" id="stripeEnabled" ${s.enabled ? 'checked' : ''}> 1125 <label class="form-check-label ms-2" for="stripeEnabled">Aktiv</label> 1126 </div> 1127 </div> 1128 </div> 1129 <div class="card-body"> 1130 <div class="mb-3"> 1131 <label class="form-label">Modus</label> 1132 <div class="btn-group w-100" role="group"> 1133 <input type="radio" class="btn-check" name="stripeMode" id="stripeModeTest" value="test" ${(s.mode||'test')==='test' ? 'checked' : ''}> 1134 <label class="btn btn-outline-warning" for="stripeModeTest"><i class="bi bi-bug"></i> Testmodus</label> 1135 <input type="radio" class="btn-check" name="stripeMode" id="stripeModeLive" value="live" ${s.mode==='live' ? 'checked' : ''}> 1136 <label class="btn btn-outline-success" for="stripeModeLive"><i class="bi bi-shield-check"></i> Livemodus</label> 1137 </div> 1138 </div> 1139 1140 <div class="mb-3"> 1141 <label for="stripePk" class="form-label">Veroeffentlichbarer Schluessel <span class="text-muted">(Publishable Key)</span></label> 1142 <input type="text" class="form-control font-monospace" id="stripePk" placeholder="pk_test_⦠oder pk_live_â¦" value="${PP._esc(s.publishable_key||'')}"> 1143 <div class="form-text">Fuer das Frontend. Ungefaehrlich, darf oeffentlich sein.</div> 1144 </div> 1145 1146 <div class="mb-3"> 1147 <label for="stripeSk" class="form-label">Geheimer Schluessel <span class="text-muted">(Secret Key)</span></label> 1148 <input type="password" class="form-control font-monospace" id="stripeSk" placeholder="${s.secret_key_set ? 'Gespeichert: '+PP._esc(s.secret_key_masked)+' - leer lassen zum Beibehalten' : 'sk_test_⦠oder sk_live_â¦'}"> 1149 <div class="form-text">
1149Wird verschluesselt in Firestore abgelegt. Leer lassen zum Beibehalten.</div> 1150 </div> 1151 1152 <div class="mb-3"> 1153 <label for="stripeWh" class="form-label">Webhook-Geheimnis <span class="text-muted">(Signing Secret)</span></label> 1154 <input type="password" class="form-control font-monospace" id="stripeWh" placeholder="${s.webhook_secret_set ? 'Gespeichert: '+PP._esc(s.webhook_secret_masked)+' - leer lassen zum Beibehalten' : 'whsec_â¦'}"> 1155 <div class="form-text">Wird gebraucht um Webhook-Events zu verifizieren.</div> 1156 </div> 1157 1158 <div class="d-flex gap-2 justify-content-end border-top pt-3"> 1159 <button type="button" class="btn btn-outline-secondary" id="shopPpTestBtn"><i class="bi bi-plug"></i> Verbindung pruefen</button> 1160 <button type="button" class="btn btn-primary" id="shopPpSaveBtn"><i class="bi bi-save"></i> Speichern</button> 1161 </div> 1162 <div id="shopPpTestResult" class="mt-3"></div> 1163 </div> 1164 </div> 1165 1166 <div class="card shadow-sm border-0 mb-4"> 1167 <div class="card-header bg-white border-bottom"> 1168 <div class="d-flex align-items-center justify-content-between"> 1169 <div> 1170 <h5 class="mb-0"><i class="bi bi-file-earmark-text" style="color:#6c757d"></i> Kauf auf Rechnung</h5> 1171 <small class="text-muted">Kunde bezahlt nach Erhalt per Ueberweisung. Zahlungsausfall liegt bei dir.</small> 1172 </div> 1173 <div class="form-check form-switch"> 1174 <input class="form-check-input" type="checkbox" id="invoiceEnabled" ${inv.enabled ? 'checked' : ''}> 1175 <label class="form-check-label ms-2" for="invoiceEnabled">Aktiv</label> 1176 </div> 1177 </div> 1178 </div> 1179 <div class="card-body small text-muted"> 1180 <div class="d-flex align-items-start gap-2"> 1181 <i class="bi bi-info-circle text-primary mt-1"></i> 1182 <div> 1183 Wenn aktiv, koennen Kunden ohne Online-Zahlung bestellen. Empfehlung: Bei unbekannter Kundschaft eher deaktivieren und stattdessen Klarna Pay-Later via Stripe anbieten (Ausfallrisiko liegt dann bei Klarna). 1184 <div class="mt-2">Letzte Aenderung: <strong>${PP._fmtDate(inv.updated_at)}</strong></div> 1185 </div> 1186 </div> 1187 <div class="d-flex justify-content-end border-top pt-3 mt-3"> 1188 <button type="button" class="btn btn-primary btn-sm" id="shopPpSaveInvoiceBtn"><i class="bi bi-save"></i> Speichern</button> 1189 </div> 1190 </div> 1191 </div> 1192 1193 <div class="card shadow-sm border-0"> 1194 <div class="card-header bg-white border-bottom"> 1195 <h6 class="mb-0"><i class="bi bi-wallet2"></i> Zahlungsmethoden via Stripe</h6> 1196 </div> 1197 <div class="card-body"> 1198 <p class="text-muted small mb-3">Diese Methoden werden <strong>automatisch</strong> im Checkout angezeigt, sobald du sie im <a href="https://dashboard.stripe.com/settings/payment_methods" target="_blank" rel="noopener">Stripe-Dashboard</a> aktivierst. Keine Code-Aenderung noetig.</p> 1199 <div class="row g-2"> 1200 ${[ 1201 {n:'Karten', d:'Visa, Mastercard, Amex', i:'bi-credit-card'}, 1202 {n:'Apple Pay', d:'iOS/Safari, Domain-Verifikation noetig', i:'bi-apple'}, 1203 {n:'Google Pay', d:'Android, Chrome', i:'bi-google'}, 1204 {n:'PayPal', d:'via Stripe, EU/DE verfuegbar', i:'bi-paypal'}, 1205 {n:'Klarna', d:'Rechnung, Raten, Sofort', i:'bi-bag'}, 1206 {n:'SEPA-Lastschrift', d:'Banklastschrift', i:'bi-bank'}, 1207 {n:'Sofort', d:'Online-Banking DE/AT', i:'bi-lightning'}, 1208 {n:'Link', d:'Stripe 1-Click-Checkout', i:'bi-link-45deg'}, 1209 ].map(m => ` 1210 <div class="col-md-6"> 1211 <div class="border rounded p-2 d-flex align-items-start"> 1212 <i class="bi ${m.i} me-2 mt-1 text-primary"></i> 1213 <div><strong class="d-block">${m.n}</strong><small class="text-muted">${m.d}</small></div> 1214 </div> 1215 </div>`).join('')} 1216 </div> 1217 </div> 1218 </div> 1219 </div> 1220 1221 <div class="col-lg-4"> 1222 <div class="card shadow-sm border-0 mb-4"> 1223 <div class="card-header bg-white border-bottom"><h6 class="mb-0"><i class="bi bi-globe2"></i> Shop-Domain</h6></div> 1224 <div class="card-body"> 1225 <label for="shopPpShopHost" class="form-label small mb-1">Deine Storefront-Domain (ohne <code>https://</code>):</label> 1226 <input type="text" class="form-control form-control-sm font-monospace" id="shopPpShopHost" placeholder="z.B. www.meinshop.de" value="${PP._esc(data.shop_host || '')}"> 1227 <div class="form-text small">Wird fuer die Webhook-URL und Stripe-Return-URLs verwendet. Aenderungen erst nach <em>Speichern</em> wirksam.</div> 1228 </div> 1229 </div> 1230 1231 <div class="card shadow-sm border-0 mb-4"> 1232 <div class="card-header bg-white border-bottom"><h6 class="mb-0"><i class="bi bi-link-45deg"></i> Webhook-URL</h6></div> 1233 <div class="card-body"> 1234 <p class="small text-muted mb-2">Im Stripe-Dashboard unter <strong>Developers → Webhooks</strong> eintragen:</p> 1235 ${data.webhook_url ? ` 1236 <div class="input-group input-group-sm"> 1237 <input type="text" class="form-control font-monospace" id="shopPpWebhookUrl" readonly value="${PP._esc(data.webhook_url)}"> 1238 <button class="btn btn-outline-secondary" id="shopPpWebhookCopy" title="Kopieren"><i class="bi bi-clipboard"></i></button> 1239 </div>` : ` 1240 <div class="alert alert-warning small mb-0 py-2"> 1241 <i class="bi bi-exclamation-triangle"></i> Bitte zuerst oben eine <strong>Shop-Domain</strong> eintragen und speichern. 1242 </div>`} 1243 <div class="mt-2 small"> 1244 <div class="text-muted mb-1">Events die aktiviert sein muessen:</div> 1245 <ul class="small mb-0 ps-3"> 1246 <li><code>payment_intent.succeeded</code></li> 1247 <li><code>payment_intent.payment_failed</code></li> 1248 <li><code>charge.refunded</code></li> 1249 </ul> 1250 </div> 1251 <div class="mt-3 small text-muted"> 1252 Zuletzt erfolgreicher Webhook-Call: <strong>${PP._fmtDate(s.last_webhook_at)}</strong> 1253 </div> 1254 </div> 1255 </div> 1256 1257 <div class="card shadow-sm border-0 mb-4"> 1258 <div class="card-header bg-white border-bottom"><h6 class="mb-0"><i class="bi bi-info-circle"></i> Status</h6></div> 1259 <div class="card-body small"> 1260 <dl class="row mb-0"> 1261 <dt class="col-6 text-muted">Modus</dt><dd class="col-6">${s.mode === 'live' ? '<span class="badge bg-success">Live</span>' : '<span class="badge bg-warning text-dark">Test</span>'}</dd> 1262 <dt class="col-6 text-muted">Aktiv</dt><dd class="col-6">${s.enabled ? '<span class="badge bg-success">Ja</span>' : '<span class="badge bg-secondary">Nein</span>'}</dd> 1263 <dt class="col-6 text-muted">Secret Key</dt><dd class="col-6">${s.secret_key_set ? '<span class="text-success">✓ gesetzt</span>' : '<span class="text-danger">fehlt</span>'}</dd> 1264 <dt class="col-6 text-muted">Webhook Secret</dt><dd class="col-6">${s.webhook_secret_set ? '<span class="text-success">✓ gesetzt</span>' : '<span class="text-danger">fehlt</span>'}</dd> 1265 <dt class="col-6 text-muted">
1265Letzte Aenderung</dt><dd class="col-6">${PP._fmtDate(s.updated_at)}</dd> 1266 </dl> 1267 </div> 1268 </div> 1269 1270 <div class="card shadow-sm border-0 mb-4"> 1271 <div class="card-header bg-white border-bottom"><h6 class="mb-0"><i class="bi bi-apple"></i> Apple Pay</h6></div> 1272 <div class="card-body small"> 1273 <p class="mb-2 text-muted">Apple Pay braucht eine Domain-Verifikation. Stripe stellt im Dashboard unter <strong>Settings → Payment methods → Apple Pay</strong> eine Datei bereit, die unter <code>/.well-known/apple-developer-merchantid-domain-association</code> ausgeliefert werden muss.</p> 1274 <a href="https://dashboard.stripe.com/settings/payment_methods/apple_pay" target="_blank" rel="noopener" class="btn btn-outline-dark btn-sm w-100"><i class="bi bi-box-arrow-up-right"></i> Apple Pay einrichten</a> 1275 </div> 1276 </div> 1277 1278 <div class="card shadow-sm border-0"> 1279 <div class="card-header bg-white border-bottom"><h6 class="mb-0"><i class="bi bi-life-preserver"></i> Hilfe</h6></div> 1280 <div class="card-body small"> 1281 <ul class="mb-0 ps-3"> 1282 <li><a href="https://dashboard.stripe.com/apikeys" target="_blank" rel="noopener">Stripe API-Keys holen</a></li> 1283 <li><a href="https://dashboard.stripe.com/webhooks" target="_blank" rel="noopener">Webhooks verwalten</a></li> 1284 <li><a href="https://dashboard.stripe.com/settings/payment_methods" target="_blank" rel="noopener">Zahlungsmethoden aktivieren</a></li> 1285 </ul> 1286 </div> 1287 </div> 1288 </div> 1289 </div>`; 1290 document.getElementById('shopPpLoading').style.display = 'none'; 1291 content.style.display = 'block'; 1292 1293 const copyBtn = document.getElementById('shopPpWebhookCopy'); 1294 if (copyBtn) { 1295 copyBtn.addEventListener('click', () => { 1296 const el = document.getElementById('shopPpWebhookUrl'); 1297 if (!el) return; 1298 el.select(); 1299 navigator.clipboard.writeText(el.value).then(() => { 1300 if (window.UI && UI.showSuccess) UI.showSuccess('Webhook-URL kopiert'); 1301 }); 1302 }); 1303 } 1304 1305 document.getElementById('shopPpSaveBtn').addEventListener('click', (e) => { e.preventDefault(); PP.save(e); }); 1306 document.getElementById('shopPpTestBtn').addEventListener('click', (e) => { e.preventDefault(); PP.test(); }); 1307 const invBtn = document.getElementById('shopPpSaveInvoiceBtn'); 1308 if (invBtn) invBtn.addEventListener('click', (e) => { e.preventDefault(); PP.save(e); }); 1309 } catch (e) { 1310 document.getElementById('shopPpLoading').innerHTML = `<div class="alert alert-danger">Fehler beim Laden: ${PP._esc(e.message)}</div>`; 1311 } 1312 }; 1313 1314 PP.save = async (ev) => { 1315 const btn = (ev && ev.currentTarget) || document.getElementById('shopPpSaveBtn'); 1316 const originalHtml = btn ? btn.innerHTML : '<i class="bi bi-save"></i> Speichern'; 1317 if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Speichereâ¦'; } 1318 try { 1319 const invoiceEl = document.getElementById('invoiceEnabled'); 1320 const shopHostEl = document.getElementById('shopPpShopHost'); 1321 const shopHostVal = shopHostEl ? shopHostEl.value.trim().replace(/^https?:\/\//i, '').replace(/\/+$/, '') : ''; 1322 const payload = {
1323 stripe: { 1324 enabled: document.getElementById('stripeEnabled').checked, 1325 mode: document.querySelector('input[name="stripeMode"]:checked').value, 1326 publishable_key: document.getElementById('stripePk').value.trim(), 1327 secret_key: document.getElementById('stripeSk').value.trim(), 1328 webhook_secret: document.getElementById('stripeWh').value.trim(), 1329 }, 1330 invoice: { 1331 enabled: invoiceEl ? invoiceEl.checked : true, 1332 }, 1333 shop_host: shopHostVal, 1334 }; 1335 console.log('[PP.save] PUT payload:', JSON.stringify(payload)); 1336 const token = await Auth.getIdToken(); 1337 const r = await fetch('/api/shop/admin/payment-providers', { 1338 method: 'PUT', 1339 headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, 1340 body: JSON.stringify(payload), 1341 }); 1342 const responseText = await r.text(); 1343 console.log('[PP.save] response', r.status, responseText); 1344 if (!r.ok) throw new Error(`HTTP ${r.status}: ${responseText}`); 1345 if (window.UI && UI.showSuccess) UI.showSuccess('Zahlungsanbieter-Konfiguration gespeichert'); 1346 await PP.render(); 1347 } catch (e) { 1348 console.error('[PP.save] error', e); 1349 alert('Fehler beim Speichern: ' + e.message); 1350 if (btn) { btn.disabled = false; btn.innerHTML = originalHtml; } 1351 } 1352 }; 1353 1354 PP.test = async () => { 1355 const resBox = document.getElementById('shopPpTestResult'); 1356 resBox.innerHTML = '<div class="alert alert-info mb-0"><span class="spinner-border spinner-border-sm me-2"></span>Pruefe Verbindung zu Stripeâ¦</div>'; 1357 try { 1358 const token = await Auth.getIdToken(); 1359 const r = await fetch('/api/shop/admin/payment-providers/test', { 1360 method: 'POST', 1361 headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, 1362 body: JSON.stringify({}), 1363 }); 1364 const data = await r.json(); 1365 if (data.ok) { 1366 resBox.innerHTML = `<div class="alert alert-success mb-0"><i class="bi bi-check-circle"></i> Verbindung OK <span class="badge ${data.mode==='live'?'bg-success':'bg-warning text-dark'}">${data.mode}</span><br><small>Account: <code>${PP._esc(data.account_id||'-')}</code>${data.business_name?' - '+PP._esc(data.business_name):''}${data.country?' ('+PP._esc(data.country)+')':''}${data.charges_enabled?' - Charges aktiv':''}</small></div>`; 1367 } else { 1368 resBox.innerHTML = `<div class="alert alert-danger mb-0"><i class="bi bi-x-circle"></i> Fehler: ${PP._esc(data.error||'unknown')}${data.detail?'<br><small class="text-muted">'+PP._esc(data.detail)+'</small>':''}</div>`; 1369 } 1370 } catch (e) { 1371 resBox.innerHTML = `<div class="alert alert-danger mb-0">Fehler: ${PP._esc(e.message)}</div>`; 1372 } 1373 }; 1374 1375 PP.render(); 1376 } catch (error) { 1377 console.error('Error loading payment-providers page:', error); 1378 mainContent.innerHTML = '<div class="alert alert-danger">Fehler: ' + error.message + '</div>'; 1379 } 1380 break; 1381 case 'shop-dashboard': 1382 case 'shop-orders': 1383 case 'shop-categories': 1384 try { 1385 const shopSub = page.replace('shop-', ''); 1386 if (shopSub === 'orders') { 1387 mainContent.innerHTML = ` 1388 <div class="container-fluid mt-4"> 1389 <div class="d-flex justify-content-between align-items-center mb-4"> 1390 <h4 class="mb-0">Shop-Bestellungen</h4> 1391 <div class="d-flex gap-2"> 1392 <button id="shopOrdersReload" class="btn btn-primary"> 1393 <i class="fas fa-sync-alt"></i> Aktualisieren 1394 </button> 1395 </div> 1396 </div> 1397 <div id="shopOrdersBulkBar"></div> 1398 <div id="shopOrdersList"> 1399 <div class="text-center py-5"><div class="spinner-border" role="status"></div><p class="text-muted mt-2">Lade Bestellungenâ¦</p></div> 1400 </div> 1401 </div> 1402 `; 1403 window.ShopOrdersAdmin = window.ShopOrdersAdmin || {}; 1404 const Shop = window.ShopOrdersAdmin; 1405 Shop.TENANT = ''; 1406 Shop._orders = []; 1407 Shop._fmtCents = (c, cur) => { 1408 if (c == null) return '-'; 1409 return new Intl.NumberFormat('de-DE', { style:'currency', currency: cur || 'EUR' }).format(c/100); 1410 }; 1411 Shop._fmtDate = (iso) => { 1412 if (!iso) return '-'; 1413 try { return new Date(iso).toLocaleStr
1413ing('de-DE'); } catch(e){ return iso; } 1414 }; 1415 Shop._statusBadge = (s) => { 1416 const map = { draft: 'secondary', pending: 'warning', confirmed: 'info', shipped: 'primary', delivered: 'success', cancelled: 'danger' }; 1417 const labelMap = { draft: 'Entwurf', pending: 'Offen', confirmed: 'Bestätigt', shipped: 'Versendet', delivered: 'Zugestellt', cancelled: 'Storniert' }; 1418 return `<span class="badge bg-${map[s]||'secondary'}">${labelMap[s]||s||'-'}</span>`; 1419 }; 1420 Shop._statusLabel = (s) => ({ draft: 'Entwurf', pending: 'Offen', confirmed: 'Bestätigt', shipped: 'Versendet', delivered: 'Zugestellt', cancelled: 'Storniert' }[s] || s || '-'); 1421 Shop._paymentBadge = (o) => { 1422 const ps = o.payment_status; 1423 const m = o.payment_method || 'invoice'; 1424 const provider = o.payment_provider ? ' via ' + o.payment_provider : ''; 1425 if (ps === 'paid') return `<span class="badge bg-success" title="Bezahlt${provider}"><i class="fas fa-check-circle"></i> Bezahlt</span>`; 1426 if (ps === 'awaiting_payment') return `<span class="badge bg-warning text-dark" title="Wartet auf Zahlung${provider}"><i class="fas fa-hourglass-half"></i> Wartet</span>`; 1427 if (ps === 'failed') return `<span class="badge bg-danger" title="Zahlung fehlgeschlagen${provider}"><i class="fas fa-times-circle"></i> Fehlgeschlagen</span>`; 1428 if (ps === 'refunded') return `<span class="badge bg-secondary" title="Erstattet${provider}"><i class="fas fa-undo"></i> Erstattet</span>`; 1429 if (m === 'invoice') return `<span class="badge bg-light text-dark border"><i class="fas fa-receipt"></i> Rechnung</span>`; 1430 return `<span class="badge bg-light text-dark border">-</span>`; 1431 }; 1432 Shop._paymentMethodLabel = (o) => { 1433 const m = (o.payment_method || '').toLowerCase(); 1434 if (m === 'stripe') return 'Kreditkarte (Stripe)'; 1435 if (m === 'invoice') return 'Kauf auf Rechnung'; 1436 return m ? (m.charAt(0).toUpperCase() + m.slice(1)) : 'â'; 1437 }; 1438 Shop._esc = (s) => (s == null ? '' : String(s) 1439 .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')); 1440 1441 // ----- Bulk-Selection ------------------------------------------------- 1442 Shop._selected = Shop._selected || new Set(); 1443 Shop._selectionMap = Shop._selectionMap || new Map(); 1444 Shop._wireRowCheckboxes = () => { 1445 document.querySelectorAll('.shop-order-row-cb').forEach(cb => { 1446 cb.addEventListener('change', (ev) => { 1447 const id = ev.target.getAttribute('data-id'); 1448 if (!id) return; 1449 if (ev.target.checked) { 1450 Shop._selected.add(id); 1451 const row = (Shop._orders || []).find(o => o.id === id); 1452 if (row) Shop._selectionMap.set(id, row); 1453 } else { 1454 Shop._selected.delete(id); 1455 Shop._selectionMap.delete(id); 1456 } 1457 Shop._renderBulkBar(); 1458 Shop._syncMasterCb(); 1459 }); 1460 }); 1461 const master = document.getElementById('shopOrdersMasterCb'); 1462 if (master) { 1463 master.addEventListener('change', (ev) => { 1464 const checked = ev.target.checked; 1465 (Shop._orders || []).forEac
1465h(o => { 1466 if (checked) { 1467 Shop._selected.add(o.id); 1468 Shop._selectionMap.set(o.id, o); 1469 } else { 1470 Shop._selected.delete(o.id); 1471 Shop._selectionMap.delete(o.id); 1472 } 1473 }); 1474 document.querySelectorAll('.shop-order-row-cb').forEach(cb => { cb.checked = checked; }); 1475 Shop._renderBulkBar(); 1476 }); 1477 } 1478 // Pre-fill selection-map fuer schon-ausgewaehlte Zeilen (nach Reload) 1479 (Shop._orders || []).forEach(o => { 1480 if (Shop._selected.has(o.id)) Shop._selectionMap.set(o.id, o); 1481 }); 1482 Shop._syncMasterCb(); 1483 }; 1484 Shop._syncMasterCb = () => { 1485 const master = document.getElementById('shopOrdersMasterCb'); 1486 if (!master) return; 1487 const total = (Shop._orders || []).length; 1488 const checked = (Shop._orders || []).filter(o => Shop._selected.has(o.id)).length; 1489 master.checked = total > 0 && checked === total; 1490 master.indeterminate = checked > 0 && checked < total; 1491 }; 1492 Shop._renderBulkBar = () => { 1493 const bar = document.getElementById('shopOrdersBulkBar'); 1494 if (!bar) return; 1495 const n = Shop._selected.size; 1496 if (n === 0) { bar.innerHTML = ''; return; } 1497 bar.innerHTML = ` 1498 <div class="alert alert-info d-flex flex-wrap align-items-center gap-2 mb-3" style="position:sticky;top:0;z-index:1020;"> 1499 <strong class="me-2"><i class="fas fa-check-square me-1"></i>${n} ausgewählt</strong> 1500 <div class="btn-group btn-group-sm"> 1501 <button class="btn btn-outline-success" onclick="window.ShopOrdersAdmin.bulkConvertToInvoice()" title="Aus den Bestellungen Rechnungen erstellen"> 1502 <i class="fas fa-file-invoice-dollar me-1"></i>Rechnungen erstellen 1503 </button> 1504 <div class="btn-group btn-group-sm"> 1505 <button class="btn btn-outline-info" onclick="window.ShopOrdersAdmin.bulkCreateDeliveryNotesPdf()" title="Lieferscheine erstellen und als zusammengefügtes PDF herunterladen"> 1506 <i class="fas fa-truck me-1"></i>Lieferscheine drucken 1507 </button> 1508 <button class="btn btn-outline-info dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" type="button"></button> 1509 <ul class="dropdown-menu"> 1510 <li><a class="dropdown-item" href="#" onclick="event.preventDefault(); window.ShopOrdersAdmin.bulkCreateDeliveryNotes()" title="Nur erstellen, keine PDF">Nur erstellen (kein PDF)</a></li> 1511 </ul> 1512 </div> 1513 <button class="btn btn-outline-success" onclick="window.ShopOrdersAdmin.bulkMarkPaid()" title="Zahlung erhalten markieren"> 1514 <i class="fas fa-euro-sign me-1"></i>Als bezahlt 1515 </button> 1516 <div class="btn-group btn-group-sm"> 1517 <button class="btn btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown" type="button"> 1518 <i class="fas fa-flag me-1"></i>Status setzen 1519 </button> 1520 <ul class="dropdown-menu"> 1521 <li><a class="dropdown-item" href="#" onclick="event.preventDefault(); window.ShopOrdersAdmin.bulkMarkStatus('confirmed')">Bestätigt</a></li> 1522 <li><a class="dropdown-item" href="#" onclick="event.preventDefault(); window.ShopOrdersAdmin.bulkMarkStatus('shipped')">Versendet</a></li> 1523 <li><a class="dropdown-item" href="#" onclick="event.preventDefault(); window.ShopOrdersAdmin.bulkMarkStatus('delivered')">Zugestellt</a></li> 1524 <li><a class="dropdown-item" href="#" onclick="event.preventDefault(); window.ShopOrdersAdmin.bulkMarkStatus('cancelled')">Storniert</a></li> 1525 </ul> 1526 </div> 1527 <button class="btn btn-outline-danger" onclick="window.ShopOrdersAdmin.bulkDelete()" title="Alle löschen"> 1528 <i class="fas fa-tras
1528h me-1"></i>Löschen 1529 </button> 1530 </div> 1531 <button class="btn btn-link btn-sm ms-auto text-decoration-none" onclick="window.ShopOrdersAdmin.bulkClear()"> 1532 <i class="fas fa-times me-1"></i>Auswahl aufheben 1533 </button> 1534 </div>`; 1535 }; 1536 Shop.bulkClear = () => { 1537 Shop._selected.clear(); 1538 Shop._selectionMap.clear(); 1539 document.querySelectorAll('.shop-order-row-cb').forEach(cb => { cb.checked = false; }); 1540 Shop._syncMasterCb(); 1541 Shop._renderBulkBar(); 1542 }; 1543 Shop._bulkSelectedIds = () => Array.from(Shop._selected); 1544 Shop._bulkPostJson = async (action, payload) => { 1545 const ids = Shop._bulkSelectedIds(); 1546 const token = await Auth.getIdToken(); 1547 const r = await fetch('/api/shop/admin/orders/bulk', { 1548 method: 'POST', 1549 headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, 1550 body: JSON.stringify({ action, ids, payload: payload || {} }) 1551 }); 1552 if (!r.ok) { 1553 let msg; 1554 try { msg = (await r.json()).error || 'Fehler'; } catch(e){ msg = `HTTP ${r.status}`; } 1555 throw new Error(msg); 1556 } 1557 return r.json(); 1558 }; 1559 Shop._bulkToast = (kind, summary, extra) => { 1560 const parts = []; 1561 if (summary.ok) parts.push(`${summary.ok} ${kind}`); 1562 if (summary.skipped) parts.push(`${summary.skipped} übersprungen`); 1563 if (summary.failed) parts.push(`${summary.failed} fehlgeschlagen`); 1564 const msg = (extra ? extra + ' · ' : '') + (parts.join(' · ') || 'Keine Aktion'); 1565 if (window.UI && summary.failed) UI.showError ? UI.showError(msg) : alert(msg); 1566 else if (window.UI && UI.showSuccess) UI.showSuccess(msg); 1567 else alert(msg); 1568 }; 1569 Shop.bulkDelete = async () => { 1570 const n = Shop._selected.size; 1571 if (n === 0) return; 1572 if (!confirm(`${n} Bestellung(en) wirklich löschen? Das kann nicht rückgängig gemacht werden.`)) return; 1573 try { 1574 const data = await Shop._bulkPostJson('delete'); 1575 Shop._bulkToast('gelöscht', data.summary); 1576 Shop.bulkClear(); 1577 await Shop.render(); 1578 } catch (e) { alert('Fehler: ' + e.message); } 1579 }; 1580 Shop.bulkConvertToInvoice = async () => { 1581 const n = Shop._selected.size; 1582 if (n === 0) return; 1583 if (!confirm(`Aus ${n} Bestellung(en) Rechnungen erstellen? Bereits konvertierte werden übersprungen.`)) return; 1584 try { 1585 const data = await Shop._bulkPostJson('convert_to_invoice'); 1586 Shop._bulkToast('Rechnungen erstellt', data.summary); 1587 Shop.bulkClear(); 1588 await Shop.render(); 1589 } catch (e) { alert('Fehler: ' + e.message); } 1590 }; 1591 Shop.bulkMarkPaid = async () => { 1592 const n = Shop._selected.size; 1593 if (n === 0) return; 1594 if (!confirm(`${n} Bestellung(en) als bezahlt markieren?`)) return; 1595 try { 1596 const data = await Shop._bulkPostJson('mark_paid'); 1597 Shop._bulkToast('als bezahlt markiert', data.summary); 1598 Shop.bulkClear(); 1599 await Shop.render(); 1600 } catch (e) { alert('Fehler: ' + e.message); } 1601 }; 1602 Shop.bulkMarkStatus = async (status) => { 1603 const n = Shop._selected.size; 1604 if (n === 0) return; 1605 const labelMap = { confirmed: 'Bestätigt', shipped: 'Versendet', delivered: 'Zugestellt', cancelled: 'Storniert' }; 1606 const label = labelMap[status] || status; 1607 if (!confirm(`${n} Bestellung(en) auf Status "${label}" setzen?`)) return; 1608 try { 1609 const data = await Shop._bulkPostJson('mark_status', { status }); 1610 Shop._bulkToast(`auf ${label}`, data.summary); 1611 Shop.bulkClear(); 1612 await Shop.render(); 1613 } catch (e) { alert('Fehler: ' + e.message); } 1614 }; 1615 Shop.bulkCreateDeliveryNotes = async () => { 1616 const n = Shop._selected.size; 1617 if (n === 0) return; 1618 if (!confirm(`Aus ${n} Bestellung(en) Lieferscheine erstellen?`)) return; 1619 try { 1620 const data = await Shop._bulkPostJson('create_delivery_note'); 1621 Shop._bulkToast('Lieferscheine erstellt', data.summary); 1622 Shop.bulkClear(); 1623 } catch (e) { alert('Fehler: ' + e.message); } 1624 }; 1625 Shop.bulkCreateDeliveryNotesPdf = async () => { 1626 const n = Shop._selected.size; 1627 if (n === 0) return; 1628 if (n > 100) { alert('Maximal 100 Bestellungen pro PDF-Bundle'); return; } 1629 if (!confirm(`${n} Lieferschein(e) erstellen und als zusammengefügtes PDF herunterladen?`)) return; 1630 try { 1631 const ids = Shop._bulkSelectedIds(); 1632 const token = await Auth.getIdToken(); 1633 const r = await fetch('/api/shop/admin/orders/bulk/delivery-notes/pdf', { 1634 method: 'POST', 1635 headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, 1636 body: JSON.stringify({ ids }) 1637 }); 1638 if (!r.ok) { 1639 let msg; 1640 try { msg = (await r.json()).error || 'Fehler'; } catch(e){ msg = `HTTP ${r.status}`; } 1641 throw new Error(msg); 1642 } 1643 const blob = await r.blob(); 1644 const url = URL.createObjectURL(blob); 1645 const a = document.createElement('a'); 1646 a.href = url; 1647 a.download = `lieferscheine_${new Date().toISOString().slice(0,10)}.pdf`; 1648 document.body.appendChild(a); 1649 a.click(); 1650 a.remove(); 1651 URL.revokeObjectURL(url); 1652 const errCount = parseInt(r.headers.get('X-Bulk-Errors') || '0', 10); 1653 if (errCount > 0) { 1654 if (window.UI && UI.showError) UI.showError(`PDF erstellt â ${errCount} Bestellung(en) fehlgeschlagen`); 1655 else alert(`PDF erstellt â ${errCount} Bestellung(en) fehlgeschlagen`); 1656 } else { 1657 if (window.UI && UI.showSuccess) UI.showSuccess(`Lieferscheine PDF (${n}) heruntergeladen`); 1658 } 1659 Shop.bulkClear(); 1660 } catch (e) { alert('Fehler beim PDF-Bundle: ' + e.message); } 1661 }; 1662 Shop.createDeliveryNote = async (orderId) => { 1663 const o = (Shop._orders || []).find(x => x.id === orderId) || {}; 1664 const label = o.order_number || orderId; 1665 const hasExisting = !!o.delivery_note_id; 1666 const promptMsg = hasExisting 1667 ? `Lieferschein ${o.delivery_note_number||''} für Bestellung ${label} drucken?` 1668 : `Lieferschein für Bestellung ${label} erstellen und PDF öffnen?`; 1669 if (!confirm(promptMsg)) return; 1670 try { 1671 const token = await Auth.getIdToken(); 1672 const r = await fetch(`/api/shop/admin/orders/${encodeURIComponent(orderId)}/create-delivery-note`, { 1673 method: 'POST', 1674 headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } 1675 }); 1676 if (!r.ok) { 1677 let msg; 1678 try { msg = (await r.json()).error || 'Fehler'; } catch(e){ msg = `HTTP ${r.status}`; } 1679 throw new Error(msg); 1680 } 1681 const data = await r.json(); 1682 const dn = data.delivery_note || {}; 1683 const successMsg = data.already_exists 1684 ? `Lieferschein ${dn.delivery_note_number||''} (bestehend)` 1685 : `Lieferschein ${dn.delivery_note_number||''} erstellt`; 1686 if (window.UI && UI.showSuccess) UI.showSuccess(successMsg); 1687 if (dn.id) { 1688 const pdfUrl = `/api/erp/delivery-notes/${encodeURIComponent(dn.id)}/pdf`; 1689 const tok2 = await Auth.getIdToken(); 1690 const pr = await fetch(pdfUrl, { headers: { 'Authorization': `Bearer ${tok2}` } }); 1691 if (pr.ok) { 1692 const blob = await pr.blob(); 1693 const url = URL.createObjectURL(blob); 1694 window.open(url, '_blank'); 1695 setTimeout(() => URL.revokeObjectURL(url), 60000); 1696 } 1697 } 1698 if (!data.already_exists) await Shop.render(); 1699 } catch (e) { alert('Fehler beim Lieferschein: ' + e.message); } 1700 }; 1701 1702 Shop.render = async () => { 1703 const listEl = document.getElementById('shopOrdersList'); 1704 listEl.innerHTML = '<div class="text-center py-5"><div class="spinner-border" role="status"></div></div>'; 1705 try { 1706 const token = await Auth.getIdToken(); 1707 const resp = await fetch(`/api/shop/admin/orders?limit=200`, { 1708 headers: { 'Authorization': `Bearer ${token}` } 1709 }); 1710 if (!resp.ok) { 1711 const err = await resp.text(); 1712 listEl.innerHTML = `<div class="alert alert-danger">Fehler ${resp.status}: ${err}</div>`; 1713 return; 1714 } 1715 const data = await resp.json(); 1716 Shop._orders = data.orders || []; 1717 if (Shop._orders.length === 0) { 1718 listEl.innerHTML = '<div class="alert alert-info">Noch keine Bestellungen eingegangen.</div>'; 1719 return; 1720 } 1721 const rows = Shop._orders.map(o => { 1722 const addrLine1 = Shop._esc(o.shipping_street || ''); 1723 const addrLine2 = Shop._esc([o.shipping_postal_code, o.shipping_city].filter(
1723Boolean).join(' ')); 1724 const addrHtml = (addrLine1 || addrLine2) 1725 ? `${addrLine1 ? addrLine1+'<br>' : ''}<small class="text-muted">${addrLine2||'-'}</small>` 1726 : '<span class="text-muted">-</span>'; 1727 const trackCell = o.tracking_number 1728 ? (o.tracking_url 1729 ? `<a href="${Shop._esc(o.tracking_url)}" target="_blank" rel="noopener">${Shop._esc(o.tracking_number)}</a>` 1730 : Shop._esc(o.tracking_number)) 1731 : '<span class="text-muted">-</span>'; 1732 return ` 1733 <tr data-id="${Shop._esc(o.id)}"> 1734 <td class="text-center"><input type="checkbox" class="shop-order-row-cb" data-id="${Shop._esc(o.id)}" ${Shop._selected.has(o.id) ? 'checked' : ''}></td> 1735 <td><strong>${Shop._esc(o.order_number||o.id)}</strong><br><small class="text-muted">${Shop._fmtDate(o.created_at)}</small></td> 1736 <td>${(o.customer_company||o.shipping_company) ? `<strong>${Shop._esc(o.customer_company||o.shipping_company)}</strong><br>${Shop._esc(o.customer_name||'')}` : Shop._esc(o.customer_name||'-')}<br><small class="text-muted">${Shop._esc(o.customer_email||'')}</small></td> 1737 <td>${addrHtml}</td> 1738 <td class="text-center">${o.item_count||0}</td> 1739 <td class="text-end"><strong>${Shop._fmtCents(o.total_cents, o.currency)}</strong></td> 1740 <td>${Shop._statusBadge(o.status)}</td> 1741 <td>${Shop._paymentBadge(o)}</td> 1742 <td>${trackCell}</td> 1743 <td class="text-end"> 1744 <div class="btn-group btn-group-sm"> 1745 <button class="btn btn-outline-secondary" onclick="window.ShopOrdersAdmin.view('${Shop._esc(o.id)}')" title="Anzeigen"> 1746 <i class="fas fa-eye"></i> 1747 </button> 1748 <button class="btn btn-outline-primary" onclick="window.ShopOrdersAdmin.edit('${Shop._esc(o.id)}')" title="Bearbeiten"> 1749 <i class="fas fa-edit"></i> 1750 </button> 1751 <button class="btn ${o.invoice_id ? 'btn-success' : 'btn-outline-success'}" onclick="window.ShopOrdersAdmin.convertToInvoice('${Shop._esc(o.id)}')" title="${o.invoice_id ? 'Rechnung öffnen: ' + Shop._esc(o.invoice_number||'') : 'Rechnung erstellen'}"> 1752 <i class="fas fa-file-invoice-dollar"></i> 1753 </button> 1754 <button class="btn ${o.delivery_note_id ? 'btn-info' : 'btn-outline-info'}" onclick="window.ShopOrdersAdmin.createDeliveryNote('${Shop._esc(o.id)}')" title="${o.delivery_note_id ? 'Lieferschein drucken: ' + Shop._esc(o.delivery_note_number||'') : 'Lieferschein erstellen + drucken'}"> 1755 <i class="fas fa-truck"></i> 1756 </button> 1757 <button class="btn btn-outline-danger" onclick="window.ShopOrdersAdmin.del('${Shop._esc(o.id)}')" title="Löschen"> 1758 <i class="fas fa-trash"></i> 1759 </button> 1760 </div> 1761 </td> 1762 </tr>`; 1763 }).join(''); 1764 listEl.innerHTML = ` 1765 <div class="table-responsive"> 1766 <table class="table table-hover" style="table-layout:fixed; width:100%;"> 1767 <colgroup> 1768 <col style="width:3%"><!-- Checkbox --> 1769 <col style="width:9%"><!-- Bestellung --> 1770 <col style="width:13%"><!-- Kunde --> 1771 <col style="width:10%"><!-- Lieferanschrift --> 1772 <col style="width:5%"><!-- Artikel --> 1773 <col style="width:8%"><!-- Summe --> 1774 <col style="width:9%"><!-- Status --> 1775 <col style="width:9%"><!-- Zahlung --> 1776 <col style="width:10%"><!-- Sendung --> 1777 <col style="width:24%"><!-- Aktionen --> 1778 </colgroup> 1779 <thead> 1780 <tr> 1781 <th class="text-center">
1781<input type="checkbox" id="shopOrdersMasterCb" title="Alle auswählen"></th> 1782 <th>Bestellung</th> 1783 <th>Kunde</th> 1784 <th>Lieferanschrift</th> 1785 <th class="text-center">Artikel</th> 1786 <th class="text-end">Summe</th> 1787 <th>Status</th> 1788 <th>Zahlung</th> 1789 <th>Sendung</th> 1790 <th class="text-end">Aktionen</th> 1791 </tr> 1792 </thead> 1793 <tbody>${rows}</tbody> 1794 </table> 1795 </div> 1796 <div class="text-muted small mt-2">${data.count} Bestellungen</div> 1797 `; 1798 Shop._wireRowCheckboxes(); 1799 Shop._renderBulkBar(); 1800 } catch (e) { 1801 listEl.innerHTML = `<div class="alert alert-danger">Fehler beim Laden: ${e.message}</div>`; 1802 } 1803 }; 1804 Shop._fetchOne = async (orderId) => { 1805 const token = await Auth.getIdToken(); 1806 const r = await fetch(`/api/shop/admin/orders/${encodeURIComponent(orderId)}`, { 1807 headers: { 'Authorization': `Bearer ${token}` } 1808 }); 1809 if (!r.ok) throw new Error(`HTTP ${r.status}: ${await r.text()}`); 1810 const j = await r.json(); 1811 return j.order; 1812 }; 1813 Shop._ensureModal = () => { 1814 let m = document.getElementById('shopOrderModal'); 1815 if (m) return m; 1816 m = document.createElement('div'); 1817 m.id = 'shopOrderModal'; 1818 m.className = 'modal fade'; 1819 m.tabIndex = -1; 1820 m.innerHTML = ` 1821 <div class="modal-dialog modal-lg modal-dialog-scrollable"> 1822 <div class="modal-content"> 1823 <div class="modal-header"> 1824 <h5 class="modal-title" id="shopOrderModalTitle">Bestellung</h5> 1825 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="SchlieÃen"></button> 1826 </div> 1827 <div class="modal-body" id="shopOrderModalBody"></div> 1828 <div class="modal-footer" id="shopOrderModalFooter"></div> 1829 </div> 1830 </div>`; 1831 document.body.appendChild(m); 1832 return m; 1833 }; 1834 Shop._openModal = () => { 1835 const el = Shop._ensureModal(); 1836 const inst = bootstrap.Modal.getOrCreateInstance(el); 1837 inst.show(); 1838 return inst; 1839 }; 1840 Shop._closeModal = () => { 1841 const el = document.getElementById('shopOrderModal'); 1842 if (!el) return; 1843 const inst = bootstrap.Modal.getInstance(el); 1844 if (inst) inst.hide(); 1845 }; 1846 Shop._itemsTable = (items) => { 1847 if (!items || !items.length) return '<p class="text-muted">Keine Positionen.</p>'; 1848 const rows = items.map(it => {
1849 const qty = it.quantity || 0; 1850 const unit = it.unit_price_cents || 0; 1851 const line = it.line_total_cents != null ? it.line_total_cents : qty*unit; 1852 const name = Shop._esc(it.name || it.product_id || '-'); 1853 const vl = it.variant_label ? `<br><small class="text-muted">${Shop._esc(it.variant_label)}</small>` : ''; 1854 return `<tr> 1855 <td>${name}${vl}</td> 1856 <td class="text-center">${qty}</td> 1857 <td class="text-end">${Shop._fmtCents(unit)}</td> 1858 <td class="text-end"><strong>${Shop._fmtCents(line)}</strong></td> 1859 </tr>`; 1860 }).join(''); 1861 return ` 1862 <div class="table-responsive"> 1863 <table class="table table-sm mb-0"> 1864 <thead><tr> 1865 <th>Produkt</th> 1866 <th class="text-center" style="width:10%">Menge</th> 1867 <th class="text-end" style="width:18%">Einzelpreis</th> 1868 <th class="text-end" style="width:18%">Zwischensumme</th> 1869 </tr></thead> 1870 <tbody>${rows}</tbody> 1871 </table> 1872 </div>`; 1873 }; 1874 Shop.view = async (orderId) => { 1875 Shop._openModal(); 1876 document.getElementById('shopOrderModalTitle').textContent = 'Bestellung'; 1877 document.getElementById('shopOrderModalBody').innerHTML = '<div class="text-center py-4"><div class="spinner-border" role="status"></div></div>'; 1878 document.getElementById('shopOrderModalFooter').innerHTML = ''; 1879 try { 1880 const o = await Shop._fetchOne(orderId); 1881 document.getElementById('shopOrderModalTitle').textContent = `Bestellung ${o.order_number || o.id}`; 1882 const addrLine = [o.shipping_street, [o.shipping_postal_code, o.shipping_city].filter(Boolean).join(' '), o.shipping_country].filter(Boolean).map(Shop._esc).join('<br>'); 1883 document.getElementById('shopOrderModalBody').innerHTML = ` 1884 <div class="row g-3"> 1885 <div class="col-md-6"> 1886 <h6 class="text-muted small mb-1">Status</h6> 1887 <div>${Shop._statusBadge(o.status)}</div> 1888 </div> 1889 <div class="col-md-6"> 1890 <h6 class="text-muted small mb-1">Eingegangen</h6> 1891 <div>${Shop._fmtDate(o.created_at)}</div> 1892 </div> 1893 <div class="col-md-6"> 1894 <h6 class="text-muted small mb-1">Kunde</h6> 1895 ${(o.customer_company || o.shipping_company) ? `<div><strong>${Shop._esc(o.customer_company || o.shipping_company)}</strong></div><div class="small">${Shop._esc(o.customer_name || '-')}</div>` : `<div><strong>${Shop._esc(o.customer_name || '-')}</strong></div>`} 1896 <div class="text-muted small">${Shop._esc(o.customer_email || '')}</div> 1897 ${o.customer_phone ? `<div class="text-muted small">${Shop._esc(o.customer_phone)}</div>` : ''} 1898 </div> 1899 <div class="col-md-6"> 1900 <h6 class="text-muted small mb-1">Lieferanschrift</h6> 1901 <div>${addrLine || '<span class="text-muted">-</span>'}</div> 1902 </div> 1903 <div class="col-md-6"> 1904 <h6 class="text-muted small mb-1">Zahlung</h6> 1905 <div>
1905${Shop._esc(Shop._paymentMethodLabel(o))} ${Shop._paymentBadge(o)}</div> 1906 ${o.payment_ref ? `<div class="text-muted small mt-1"><code style="font-size:11px">${Shop._esc(o.payment_ref)}</code></div>` : ''} 1907 </div> 1908 <div class="col-md-6"> 1909 <h6 class="text-muted small mb-1">Summe</h6> 1910 <div><strong>${Shop._fmtCents(o.total_cents, o.currency)}</strong></div> 1911 </div> 1912 ${o.tracking_number ? ` 1913 <div class="col-md-6"> 1914 <h6 class="text-muted small mb-1">Sendung</h6> 1915 <div>${Shop._esc(o.tracking_carrier || '')} ${o.tracking_url ? `<a href="${Shop._esc(o.tracking_url)}" target="_blank" rel="noopener">${Shop._esc(o.tracking_number)}</a>` : Shop._esc(o.tracking_number)}</div> 1916 </div>` : ''} 1917 ${o.estimated_delivery ? ` 1918 <div class="col-md-6"> 1919 <h6 class="text-muted small mb-1">Voraussichtliche Lieferung</h6> 1920 <div>${Shop._esc(o.estimated_delivery)}</div> 1921 </div>` : ''} 1922 ${o.note ? ` 1923 <div class="col-12"> 1924 <h6 class="text-muted small mb-1">Kunden-Notiz</h6> 1925 <div class="border rounded p-2 bg-light">${Shop._esc(o.note)}</div> 1926 </div>` : ''} 1927 ${o.internal_note ? ` 1928 <div class="col-12"> 1929 <h6 class="text-muted small mb-1">Interne Notiz</h6> 1930 <div class="border rounded p-2 bg-light">${Shop._esc(o.internal_note)}</div> 1931 </div>` : ''} 1932 <div class="col-12"> 1933 <h6 class="text-muted small mb-1">Positionen</h6> 1934 ${Shop._itemsTable(o.items)} 1935 </div> 1936 </div>`; 1937 document.getElementById('shopOrderModalFooter').innerHTML = ` 1938 <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">SchlieÃen</button> 1939 <button type="button" class="btn btn-primary" onclick="window.ShopOrdersAdmin.edit('${Shop._esc(o.id)}')"> 1940 <i class="fas fa-edit"></i> Bearbeiten 1941 </button>`; 1942 } catch (e) { 1943 document.getElementById('shopOrderModalBody').innerHTML = `<div class="alert alert-danger">${Shop._esc(e.message)}</div>`; 1944 } 1945 }; 1946 Shop.edit = async (orderId) => { 1947 Shop._closeModal(); 1948 const mc = document.getElementById('mainContent'); 1949 if (!mc) return; 1950 mc.innerHTML = '<div class="container-fluid mt-4"><div class="text-center py-5"><div class="spinner-border" role="status"></div><p class="text-muted mt-2">Lade Bestellungâ¦</p></div></div>'; 1951 try { 1952 const o = await Shop._fetchOne(orderId); 1953 const statuses = ['draft','pending','confirmed','shipped','delivered','cancelled']; 1954 const trackUrl = o.tracking_url || (o.tracking_carrier && o.tracking_number && o.tracking_carrier.toLowerCase().includes('dhl') ? `https://www.dhl.de/de/privatkunden/pakete-empfangen/verfolgen.html?piececode=${encodeURIComponent(o.tracking_number)}` : ''); 1955 const hasInvoice = !!o.invoice_id; 1956 const payMethod = o.payment_method ? String(o.payment_method).charAt(0).toUpperCase() + String(o.payment_method).slice(1) : 'â'; 1957 mc.innerHTML = ` 1958 <div class="container-fluid py-4" id="shopOrderEditPage"> 1959 <div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-4"> 1960 <div class="d-flex align-items-center"> 1961 <button class="btn btn-link text-decoration-none text-secondary p-0 me-3" id="shopOrderBackBtn" title="Zurück zur Bestellungsliste"> 1962 <i class="bi bi-arrow-left fs-5"></i> 1963 </button> 1964 <div> 1965 <div class="d-flex align-items-center flex-wrap gap-2"> 1966 <h3 class="mb-0">Bestellung ${Shop._esc(o.order_number || o.id)}</h3> 1967 ${Shop._statusBadge(o.status)} 1968 ${hasInvoice ? `<span class="badge bg-light text-dark border"><i class="fas fa-file-invoice-dollar text-success me-1"></i>${Shop._esc(o.invoice_number||'Rechnung')}</span>` : ''} 1969 </div> 1970 <p class="text-muted small mb-0 mt-1">Eingegangen am ${Shop._fmtDate(o.created_at)}${o.source ? ' · Quelle: ' + Shop._esc(o.source) : ''}</p> 1971 </div> 1972 </div> 1973 <div class="d-flex align-items-center gap-2"> 1974 <label class="form-label mb-0 me-1 small text-muted">Status:</label> 1975 <select class="form-select form-select-sm" id="soeStatusTop" style="width:auto;min-width:150px;"> 1976 ${statuses.map(s => `<option value="${s}" ${o.status===s?'selected':''}>${Shop._statusLabel(s)}</option>`).join('')} 1977 </select> 1978 <button class="btn btn-outline-secondary btn-sm" id="shopOrderCancelBtn">Abbrechen</button> 1979 <button class="btn btn-primary btn-sm" id="shopOrderSaveBtn"><i class="fas fa-save me-1"></i>Speichern</button> 1980 </div> 1981 </div> 1982 <form id="shopOrderEditForm"> 1983 <input type="hidden" id="soeId" value="${Shop._esc(o.id)}"> 1984 <input type="hidden" id="soeStatus" value="${Shop._esc(o.status||'pending')}"> 1985 <div class="row g-3"> 1986 <div class="col-lg-8"> 1987 <div class="card mb-3 shadow-sm"> 1988 <div class="card-header bg-white border-bottom py-2 d-flex justify-content-between align-items-center"> 1989 <strong><i class="fas fa-box me-2 text-primary"></i>Artikel</strong> 1990 <sp
1990an class="text-muted small">${(o.items||[]).length} ${(o.items||[]).length === 1 ? 'Position' : 'Positionen'}</span> 1991 </div> 1992 <div class="card-body p-0"> 1993 ${Shop._itemsTable(o.items)} 1994 </div> 1995 </div> 1996 <div class="card mb-3 shadow-sm"> 1997 <div class="card-header bg-white border-bottom py-2"> 1998 <strong><i class="fas fa-truck me-2 text-primary"></i>Versand & Tracking</strong> 1999 </div> 2000 <div class="card-body"> 2001 <div class="row g-3"> 2002 <div class="col-md-4"> 2003 <label class="form-label small text-muted">Versender</label> 2004 <input type="text" class="form-control" id="soeTrackCarrier" value="${Shop._esc(o.tracking_carrier||'')}" placeholder="DHL, DPD, UPS â¦" list="carrierList"> 2005 <datalist id="carrierList"><option value="DHL"><option value="DPD"><option value="UPS"><option value="Hermes"><option value="GLS"><option value="Post"></datalist> 2006 </div> 2007 <div class="col-md-4"> 2008 <label class="form-label small text-muted">Sendungsnummer</label> 2009 <input type="text" class="form-control" id="soeTrackNumber" value="${Shop._esc(o.tracking_number||'')}"> 2010 </div> 2011 <div class="col-md-4"> 2012 <label class="form-label small text-muted">Vsl. Lieferung</label> 2013 <input type="text" class="form-control" id="soeEta" value="${Shop._esc(o.estimated_delivery||'')}" placeholder="2026-04-20"> 2014 </div> 2015 <div class="col-12"> 2016 <label class="form-label small text-muted">Tracking-URL <span class="text-muted">(automatisch bei DHL)</span></label> 2017 <input type="url" class="form-control" id="soeTrackUrl" value="${Shop._esc(trackUrl)}" placeholder="https://..."> 2018 </div> 2019 </div> 2020 </div> 2021 </div> 2022 <div class="card mb-3 shadow-sm"> 2023 <div class="card-header bg-white border-bottom py-2"> 2024 <strong><i class="fas fa-sticky-note me-2 text-primary"></i>Notizen</strong> 2025 </div> 2026 <div class="card-body"> 2027 <div class="mb-3"> 2028 <label class="form-label small text-muted"><i class="fas fa-comment me-1"></i>Kunden-Notiz <span class="text-muted">(bei Bestellung übermittelt)</span></label> 2029 <textarea class="form-control" id="soeNote" rows="2" placeholder="â">${Shop._esc(o.note||'')}</textarea> 2030 </div> 2031 <div> 2032 <label class="form-label small text-muted"><i class="fas fa-lock me-1"></i>Interne Notiz <span class="text-muted">(nicht sichtbar für Kunden)</span></label> 2033 <textarea class="form-control" id="soeInternalNote" rows="3" placeholder="z.B. Lager
2033stand, Sonderanfragen â¦">${Shop._esc(o.internal_note||'')}</textarea> 2034 </div> 2035 </div> 2036 </div> 2037 </div> 2038 <div class="col-lg-4"> 2039 <div class="card mb-3 shadow-sm"> 2040 <div class="card-header bg-white border-bottom py-2"> 2041 <strong><i class="fas fa-receipt me-2 text-primary"></i>Zusammenfassung</strong> 2042 </div> 2043 <div class="card-body"> 2044 <div class="d-flex justify-content-between small"><span class="text-muted">Zwischensumme</span><span>${Shop._fmtCents(o.subtotal_cents||0)}</span></div> 2045 ${o.shipping_cost_cents ? `<div class="d-flex justify-content-between small mt-1"><span class="text-muted">Versand</span><span>${Shop._fmtCents(o.shipping_cost_cents)}</span></div>` : ''} 2046 ${o.tax_cents ? `<div class="d-flex justify-content-between small mt-1"><span class="text-muted">USt.</span><span>${Shop._fmtCents(o.tax_cents)}</span></div>` : ''} 2047 <hr class="my-2"> 2048 <div class="d-flex justify-content-between"><strong>Gesamt</strong><strong class="fs-5 text-primary">${Shop._fmtCents(o.total_cents||0)}</strong></div> 2049 <hr class="my-2"> 2050 <div class="d-flex justify-content-between small mb-1"><span class="text-muted">Zahlungsart</span><span>${Shop._esc(Shop._paymentMethodLabel(o))}</span></div> 2051 <div class="d-flex justify-content-between small mb-1"><span class="text-muted">Zahlungsstatus</span><span>${Shop._paymentBadge(o)}</span></div> 2052 ${o.payment_ref ? `<div class="d-flex justify-content-between small mb-1"><span class="text-muted">Referenz</span><span><code style="font-size:11px">${Shop._esc(o.payment_ref)}</code></span></div>` : ''} 2053 ${o.paid_at ? `<div class="d-flex justify-content-between small"><span class="text-muted">Bezahlt am</span><span>${Shop._fmtDate(o.paid_at)}</span></div>` : ''} 2054 ${o.payment_status !== 'paid' ? `<button type="button" class="btn btn-outline-success btn-sm w-100 mt-2" id="shopOrderMarkPaidBtn"><i class="fas fa-check-circle me-1"></i>Als bezahlt markieren</button>` : ''} 2055 </div> 2056 </div> 2057 <div class="card mb-3 shadow-sm"> 2058 <div class="card-header bg-white border-bottom py-2 d-flex justify-content-between align-items-center"> 2059 <strong><i class="fas fa-user me-2 text-primary"></i>Kunde</strong> 2060 </div> 2061 <div class="card-body"> 2062 <div class="mb-2"> 2063 <label class="form-label small text-muted">Firma <span class="text-muted">(optional)</span></label> 2064 <input type="text" class="form-control form-control-sm" id="soeCustomerCompany" value="${Shop._esc(o.customer_company||o.shipping_company||'')}" placeholder="z.B. Muster GmbH"> 2065 </div> 2066 <div class="mb-2"> 2067 <label class="form-label small text-muted">Name</label> 2068 <input type="text" class="form-control form-control-sm" id="soeCustomerName" value="${Shop._esc(o.customer_name||'')}"> 2069 </div> 2070 <div class="mb-2"> 2071 <label class="form-label small text-muted">E-Mail</label> 2072 <input type="email" class="form-control form-control-sm" id="soeCustomerEmail" value="${Shop._esc(o.customer_email||'')}"> 2073 </div> 2074 <div> 2075 <label class="form-label small text-muted">Telefon</label> 2076 <input type="text" class="form-control form-control-sm" id="soeCustomerPhone" value="${Shop._esc(o.customer_phone||'')}"> 2077 </div> 2078 </div> 2079 </div> 2080 <div class="card mb-3 shadow-sm"> 2081 <div class="card-header bg-white border-bottom py-2"> 2082 <strong><i class="fas fa-map-marker-alt me-2 text-primary"></i>Lieferanschrift</strong> 2083 </div> 2084 <div class="card-body"> 2085 <div class="mb-2"> 2086 <label class="form-label small text-muted">StraÃe & Hausnr.</label> 2087 <input type="text" class="form-control form-control-sm" id="soeShipStreet" value="${Shop._esc(o.shipping_street||'')}"> 2088 </div> 2089 <div class="row g-2 mb-2"> 2090 <div class="col-4"> 2091 <label class="form-label small text-muted">PLZ</label> 2092 <input type="text" class="form-control form-control-sm" id="soeShipPlz" value="${Shop._esc(o.shipping_postal_code||'')}"> 2093 </div> 2094 <div class="col-8"> 2095 <label class="form-label small text-muted">Ort</label> 2096 <input type="text" class="form-control form-control-sm" id="soeShipCity" value="${Shop._esc(o.shipping_city||'')}"> 2097 </div> 2098 </div> 2099 <div> 2100 <label class="form-label small text-muted">Land</label> 2101 <select class="form-select form-select-sm" id="soeShipCountry"> 2102 <option value="DE" ${(o.shipping_country||'DE')==='DE'?'selected':''}>Deutschland</option> 2103 <option value="AT" ${o.shipping_country==='AT'?'selected':''}>Ãsterreich</option> 2104 <option value="CH" ${o.shipping_country==='CH'?'selected':''}>Schweiz</option> 2105 </select> 2106 </div> 2107 </div> 2108 </div> 2109 <div class="card mb-3 shadow-sm"> 2110 <div class="card-header bg-white border-bottom py-2"> 2111 <strong><i class="fas fa-bolt me-2 text-primary"></i>Aktionen</strong> 2112 </div> 2113 <div class="card-body d-grid gap-2"> 2114 <button type="button" class="btn ${hasInvoice ? 'btn-success' : 'btn-outline-success'} btn-sm" id="shopOrderInvoiceBtn" title="${hasInvoice ? 'Zur Rechnung ' + Shop._esc(o.invoice_number||'') : 'Aus Bestellung Rechnung erstellen'}"> 2115 <i class="fas fa-file-invoice-dollar me-1"></i> 2116 ${hasInvoice ? 'Rechnung öffnen (' + Shop._esc(o.invoice_number||'') + ')' : 'Rechnung erstellen'} 2117 </button> 2118 <button type="button" class="btn btn-outline-dark btn-sm" id="shopOrderDeliveryBtn" ${hasInvoice ? '' : 'disabled'} title="${hasInvoice ? 'Lieferschein als PDF öffnen' : 'Lieferschein benötigt erstellte Rechnung'}"> 2119 <i class="fas fa-truck me-1"></i>Lieferschein drucken 2120 </button> 2121 <button type="button" class="btn btn-outline-danger btn-sm" id="shopOrderDeleteBtn"> 2122 <i class="fas fa-tras
2122h me-1"></i>Bestellung löschen 2123 </button> 2124 </div> 2125 </div> 2126 </div> 2127 </div> 2128 </form> 2129 </div>`; 2130 const backToList = () => Pages.loadPage('shop-orders'); 2131 const syncStatus = () => { document.getElementById('soeStatus').value = document.getElementById('soeStatusTop').value; }; 2132 document.getElementById('shopOrderBackBtn').addEventListener('click', backToList); 2133 document.getElementById('shopOrderCancelBtn').addEventListener('click', backToList); 2134 document.getElementById('shopOrderStatusTop') && document.getElementById('shopOrderStatusTop').addEventListener('change', syncStatus); 2135 document.getElementById('soeStatusTop').addEventListener('change', syncStatus); 2136 document.getElementById('shopOrderSaveBtn').addEventListener('click', () => { syncStatus(); Shop._save(o.id); }); 2137 document.getElementById('shopOrderInvoiceBtn').addEventListener('click', () => { 2138 if (hasInvoice) { Pages.loadPage('erp-invoices'); return; } 2139 Shop.convertToInvoice(o.id); 2140 }); 2141 const delivBtn = document.getElementById('shopOrderDeliveryBtn'); 2142 if (delivBtn && hasInvoice) { 2143 delivBtn.addEventListener('click', async () => { 2144 try { 2145 const token = await Auth.getIdToken(); 2146 const r = await fetch(`/api/erp/invoices/${encodeURIComponent(o.invoice_id)}/delivery-note.pdf`, { headers: { 'Authorization': `Bearer ${token}` } }); 2147 if (!r.ok) throw new Error(`HTTP ${r.status}`); 2148 const blob = await r.blob(); 2149 const url = URL.createObjectURL(blob); 2150 window.open(url, '_blank'); 2151 setTimeout(() => URL.revokeObjectURL(url), 10000); 2152 } catch (err) { alert('Lieferschein konnte nicht geladen werden: ' + err.message); } 2153 }); 2154 } 2155 const markPaidBtn = document.getElementById('shopOrderMarkPaidBtn'); 2156 if (markPaidBtn) { 2157 markPaidBtn.addEventListener('click', async () => { 2158 if (!confirm('Bestellung als bezahlt markieren? Diese Aktion sollte nur verwendet werden wenn die Zahlung auÃerhalb des Systems eingegangen ist (z.B. Ãberweisung).')) return; 2159 markPaidBtn.disabled = true; 2160 markPaidBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>â¦'; 2161 try { 2162 const token = await Auth.getIdToken(); 2163 const r = await fetch(`/api/shop/admin/orders/${encodeURIComponent(o.id)}`, { 2164 method: 'PUT', 2165 headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, 2166 body: JSON.stringify({ payment_status: 'paid' }) 2167 }); 2168 if (!r.ok) throw new Error(`HTTP ${r.status}: ${await r.text()}`); 2169 if (window.UI && UI.showSuccess) UI.showSuccess('Bestellung als bezahlt markiert'); 2170 Shop.edit(o.id); // Seite neu rendern 2171 } catch (err) { 2172 alert('Fehler: ' + err.message); 2173 markPaidBtn.disabled = false;
2174 markPaidBtn.innerHTML = '<i class="fas fa-check-circle me-1"></i>Als bezahlt markieren'; 2175 } 2176 }); 2177 } 2178 document.getElementById('shopOrderDeleteBtn').addEventListener('click', async () => { 2179 const label = o.order_number || o.id; 2180 if (!confirm(`Bestellung ${label} wirklich löschen? Das kann nicht rückgängig gemacht werden.`)) return; 2181 try { 2182 const token = await Auth.getIdToken(); 2183 const r = await fetch(`/api/shop/admin/orders/${encodeURIComponent(o.id)}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); 2184 if (!r.ok) throw new Error(`HTTP ${r.status}: ${await r.text()}`); 2185 if (window.UI && UI.showSuccess) UI.showSuccess('Bestellung gelöscht'); 2186 Pages.loadPage('shop-orders'); 2187 } catch (err) { alert('Fehler beim Löschen: ' + err.message); } 2188 }); 2189 } catch (e) { 2190 mc.innerHTML = `<div class="container-fluid mt-4"><div class="alert alert-danger">Fehler beim Laden der Bestellung: ${Shop._esc(e.message)}</div><button class="btn btn-outline-secondary" onclick="Pages.loadPage('shop-orders')"><i class="bi bi-arrow-left"></i> Zurück zur Liste</button></div>`; 2191 } 2192 }; 2193 Shop._save = async (orderId) => { 2194 const btns = ['shopOrderSaveBtn','shopOrderSaveBtnBottom'].map(id => document.getElementById(id)).filter(Boolean); 2195 btns.forEach(b => { b.disabled = true; b.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Speichertâ¦'; }); 2196 try { 2197 const payload = { 2198 status: document.getElementById('soeStatus').value, 2199 customer_company: document.getElementById('soeCustomerCompany').value, 2200 customer_name: document.getElementById('soeCustomerName').value, 2201 customer_email: document.getElementById('soeCustomerEmail').value, 2202 customer_phone: document.getElementById('soeCustomerPhone').value, 2203 shipping_street: document.getElementById('soeShipStreet').value, 2204 shipping_postal_code: document.getElementById('soeShipPlz').value, 2205 shipping_city: document.getElementById('soeShipCity').value, 2206 shipping_country: document.getElementById('soeShipCountry').value, 2207 tracking_carrier: document.getElementById('soeTrackCarrier').value, 2208 tracking_number: document.getElementById('soeTrackNumber').value, 2209 tracking_url: document.getElementById('soeTrackUrl').value, 2210 estimated_delivery: document.getElementById('soeEta').value, 2211 note: document.getElementById('soeNote').value, 2212 internal_note: document.getElementById('soeInternalNote').value, 2213 }; 2214 const token = await Auth.getIdToken(); 2215 const r = await fetch(`/api/shop/admin/orders/${encodeURIComponent(orderId)}`, { 2216 method: 'PUT', 2217 headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, 2218 body: JSON.stringify(payload) 2219 }); 2220 if (!r.ok) throw new Error(`HTTP ${r.status}: ${await r.text()}`); 2221 if (window.UI && UI.showSuccess) UI.showSuccess('Bestellung gespeichert'); 2222 Pages.loadPage('shop-orders'); 2223 } catch (e) { 2224 alert('Fehler beim Speichern: ' + e.message); 2225 btns.forEach(b => { b.disabled = false; b.innerHTML = '<i class="fas fa-save"></i> Speichern'; }); 2226 } 2227 };
2228 Shop.convertToInvoice = async (orderId) => { 2229 const o = (Shop._orders || []).find(x => x.id === orderId) || {}; 2230 // Already converted â jump to invoice list 2231 if (o.invoice_id) { 2232 if (confirm(`Für ${o.order_number || orderId} existiert bereits Rechnung ${o.invoice_number||''}.\n\nZu Rechnungen wechseln?`)) { 2233 Pages.loadPage('erp-invoices'); 2234 } 2235 return; 2236 } 2237 const label = o.order_number || orderId; 2238 if (!confirm(`Aus Bestellung ${label} eine Rechnung erstellen?`)) return; 2239 try { 2240 const token = await Auth.getIdToken(); 2241 const r = await fetch(`/api/shop/admin/orders/${encodeURIComponent(orderId)}/convert-to-invoice`, { 2242 method: 'POST', 2243 headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } 2244 }); 2245 if (!r.ok) throw new Error(`HTTP ${r.status}: ${await r.text()}`); 2246 const data = await r.json(); 2247 const msg = data.already_converted 2248 ? `Rechnung bereits vorhanden: ${data.invoice_number}` 2249 : `Rechnung ${data.invoice_number} erstellt`; 2250 if (window.UI && UI.showSuccess) UI.showSuccess(msg); 2251 else alert(msg); 2252 await Shop.render(); 2253 if (confirm(`${msg}\n\nJetzt zur Rechnungsübersicht wechseln?`)) { 2254 Pages.loadPage('erp-invoices'); 2255 } 2256 } catch (e) { 2257 alert('Fehler beim Erstellen der Rechnung: ' + e.message); 2258 } 2259 }; 2260 2261 Shop.del = async (orderId) => { 2262 const o = (Shop._orders || []).find(x => x.id === orderId) || {}; 2263 const label = o.order_number || orderId; 2264 if (!confirm(`Bestellung ${label} wirklich löschen? Das kann nicht rückgängig gemacht werden.`)) return; 2265 try { 2266 const token = await Auth.getIdToken(); 2267 const r = await fetch(`/api/shop/admin/orders/${encodeURIComponent(orderId)}`, { 2268 method: 'DELETE', 2269 headers: { 'Authorization': `Bearer ${token}` } 2270 }); 2271 if (!r.ok) throw new Error(`HTTP ${r.status}: ${await r.text()}`); 2272 await Shop.render(); 2273 if (window.UI && UI.showSuccess) UI.showSuccess('Bestellung gelöscht'); 2274 } catch (e) { 2275 alert('Fehler beim Löschen: ' + e.message); 2276 } 2277 }; 2278 document.getElementById('shopOrdersReload').addEventListener('click', () => Shop.render()); 2279 Shop.render(); 2280 } else if (shopSub === 'dashboard') { 2281 mainContent.innerHTML = ` 2282 <div class="container-fluid mt-4"> 2283 <div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2"> 2284 <h4 class="mb-0">
2284Shop-Ãbersicht</h4> 2285 <div class="d-flex gap-2 align-items-center"> 2286 <div class="btn-group" role="group" id="shopDashRange"> 2287 <button type="button" class="btn btn-outline-secondary btn-sm" data-range="7">7 Tage</button> 2288 <button type="button" class="btn btn-primary btn-sm" data-range="30">30 Tage</button> 2289 <button type="button" class="btn btn-outline-secondary btn-sm" data-range="90">90 Tage</button> 2290 </div> 2291 <button id="shopDashReload" class="btn btn-primary" title="Aktualisieren"> 2292 <i class="fas fa-sync-alt"></i> 2293 </button> 2294 </div> 2295 </div> 2296 <div id="shopDashBody"> 2297 <div class="text-center py-5"><div class="spinner-border" role="status"></div><p class="text-muted mt-2">Lade Kennzahlenâ¦</p></div> 2298 </div> 2299 </div> 2300 `; 2301 2302 window.ShopDashAdmin = window.ShopDashAdmin || {}; 2303 const SD = window.ShopDashAdmin; 2304 SD.TENANT = ''; 2305 SD.range = SD.range || 30; 2306 SD._charts = SD._charts || {}; 2307 2308 SD._fmtCents = (c, cur) => { 2309 if (c == null) return 'â'; 2310 return new Intl.NumberFormat('de-DE', { style:'currency', currency: cur || 'EUR' }).format((c||0)/100); 2311 }; 2312 SD._fmtDate = (iso) => { 2313 if (!iso) return 'â'; 2314 try { 2315 return new Date(iso).toLocaleString('de-DE', { day:'2-digit', month:'2-digit', year:'numeric', hour:'2-digit', minute:'2-digit' }); 2316 } catch(e) { return iso; } 2317 }; 2318 SD._fmtDay = (iso) => { 2319 try { 2320 const d = new Date(iso + 'T00:00:00Z'); 2321 return d.toLocaleDateString('de-DE', { day:'2-digit', month:'2-digit' }); 2322 } catch(e) { return iso; } 2323 }; 2324 SD._statusBadge = (s) => { 2325 const map = { 2326 'paid': 'bg-success', 2327 'confirmed': 'bg-success', 2328 'delivered': 'bg-success', 2329 'shipped': 'bg-info', 2330 'processing': 'bg-primary', 2331 'pending': 'bg-warning text-dark', 2332 'awaiting_payment': 'bg-warning text-dark', 2333 'cancelled': 'bg-secondary', 2334 'canceled': 'bg-secondary', 2335 'refunded': 'bg-secondary', 2336 'failed': 'bg-danger', 2337 }; 2338 const cls = map[(s||'').toLowerCase()] || 'bg-secondary'; 2339 return `<span class="badge ${cls}">${s || 'â'}</span>`; 2340 }; 2341 SD._pct = (v) => { 2342 if (v == null) return '<span class="text-muted">â</span>'; 2343 const cls = v >= 0 ? 'text-success' : 'text-danger'; 2344 const arrow = v >= 0 ? 'â²' : 'â¼'; 2345 return `<span class="${cls} small fw-semibold">${arrow} ${Math.abs(v).toFixed(1)}%</span>`;
2346 }; 2347 2348 SD._renderKpis = (d) => { 2349 const k = d.kpis || {}; 2350 const range = d.range_days || 30; 2351 return ` 2352 <div class="row g-3 mb-4"> 2353 <div class="col-md-6 col-lg-3"> 2354 <div class="card border-0 shadow-sm h-100"> 2355 <div class="card-body"> 2356 <div class="d-flex align-items-center text-muted small mb-2"> 2357 <i class="bi bi-bag-check me-2"></i>Bestellungen heute 2358 </div> 2359 <div class="display-6 fw-bold mb-1">${k.orders_today ?? 0}</div> 2360 <div class="text-muted small">Umsatz heute: <span class="fw-semibold text-dark">${SD._fmtCents(k.revenue_today_cents)}</span></div> 2361 </div> 2362 </div> 2363 </div> 2364 <div class="col-md-6 col-lg-3"> 2365 <div class="card border-0 shadow-sm h-100"> 2366 <div class="card-body"> 2367 <div class="d-flex align-items-center text-muted small mb-2"> 2368 <i class="bi bi-hourglass-split me-2"></i>Wartet auf Zahlung 2369 </div> 2370 <div class="display-6 fw-bold mb-1">${k.pending_count ?? 0}</div> 2371 <div class="text-muted small">Offen: <span class="fw-semibold text-dark">${SD._fmtCents(k.pending_cents)}</span></div> 2372 </div> 2373 </div> 2374 </div> 2375 <div class="col-md-6 col-lg-3"> 2376 <div class="card border-0 shadow-sm h-100"> 2377 <div class="card-body"> 2378 <div class="d-flex align-items-center text-muted small mb-2"> 2379 <i class="bi bi-cash-coin me-2"></i>Umsatz letzte ${range} Tage 2380 </div> 2381 <div class="display-6 fw-bold mb-1">${SD._fmtCents(k.revenue_range_cents)}</div> 2382 <div class="small">${SD._pct(k.revenue_delta_pct)} <span class="text-muted ms-1">vs. Vorperiode</span></div> 2383 </div> 2384 </div> 2385 </div> 2386 <div class="col-md-6 col-lg-3"> 2387 <div class="card border-0 shadow-sm h-100"> 2388 <div class="card-body"> 2389 <div class="d-flex align-items-center text-muted small mb-2"> 2390 <i class="bi bi-graph-up-arrow me-2"></i>Bestellungen / Ã-Wert 2391 </div> 2392 <div class="display-6 fw-bold mb-1">${k.orders_range ?? 0}</div> 2393 <div class="text-muted small">Ã-Bestellwert: <span class="fw-semibold text-dark">${SD._fmtCents(k.avg_order_cents)}</span> · ${SD._pct(k.orders_delta_pct)}</div> 2394 </div> 2395 </div> 2396 </div> 2397 </div> 2398 `; 2399 }; 2400 2401 SD._renderBody = (d) => { 2402 const recent = d.recent_orders || []; 2403 const top = d.top_products || []; 2404 const hasRecent = recent.length > 0; 2405 const hasTop = top.length > 0; 2406 2407 return ` 2408 ${SD._renderKpis(d)} 2409 <div class="row g-3 mb-4"> 2410 <div class="col-lg-8"> 2411 <div class="card border-0 shadow-sm h-100"> 2412 <div class="card-header bg-white border-0 pt-3 pb-0"> 2413 <h6 class="mb-0 fw-bold"><i class="bi bi-bar-chart me-2 text-primary"></i>Umsatz-Verlauf (${d.range_days} Tage)</h6> 2414 </div> 2415 <div class="card-body">
2416 <canvas id="shopDashRevenueChart" height="100"></canvas> 2417 </div> 2418 </div> 2419 </div> 2420 <div class="col-lg-4"> 2421 <div class="card border-0 shadow-sm h-100"> 2422 <div class="card-header bg-white border-0 pt-3 pb-0"> 2423 <h6 class="mb-0 fw-bold"><i class="bi bi-pie-chart me-2 text-primary"></i>Status-Verteilung</h6> 2424 </div> 2425 <div class="card-body d-flex align-items-center justify-content-center"> 2426 <canvas id="shopDashStatusChart" height="220"></canvas> 2427 </div> 2428 </div> 2429 </div> 2430 </div> 2431 <div class="row g-3"> 2432 <div class="col-lg-7"> 2433 <div class="card border-0 shadow-sm h-100"> 2434 <div class="card-header bg-white border-0 pt-3 pb-2 d-flex justify-content-between align-items-center"> 2435 <h6 class="mb-0 fw-bold"><i class="bi bi-clock-history me-2 text-primary"></i>Letzte Bestellungen</h6> 2436 <a href="#" onclick="Pages.loadPage('shop-orders');return false;" class="small text-decoration-none">Alle anzeigen â</a> 2437 </div> 2438 <div class="card-body p-0"> 2439 ${!hasRecent ? '<p class="text-muted text-center py-4 mb-0">Keine Bestellungen im Zeitraum.</p>' : ` 2440 <div class="table-responsive"> 2441 <table class="table table-sm mb-0 align-middle"> 2442 <thead class="small text-muted"> 2443 <tr> 2444 <th class="ps-3">Nr.</th> 2445 <th>Kunde</th> 2446 <th>Zeit</th> 2447 <th>Status</th> 2448 <th class="text-end pe-3">Betrag</th> 2449 </tr> 2450 </thead> 2451 <tbody> 2452 ${recent.map(o => ` 2453 <tr style="cursor:pointer" onclick="Pages.loadPage('shop-orders')"> 2454 <td class="ps-3 small fw-semibold">${o.order_number || o.id.slice(0,8)}</td> 2455 <td class="small">${(o.customer_name || o.customer_email || 'â')}</td> 2456 <td class="small text-muted">${SD._fmtDate(o.created_at)}</td> 2457 <td>${SD._statusBadge(o.payment_status || o.status)}</td> 2458 <td class="text-end pe-3 fw-semibold">${SD._fmtCents(o.total_cents, o.currency)}</td> 2459 </tr> 2460 `).join('')} 2461 </tbody> 2462 </table> 2463 </div> 2464 `} 2465 </div> 2466 </div> 2467 </div> 2468 <div class="col-lg-5"> 2469 <div class="card border-0 shadow-sm h-100"> 2470 <div class="card-header bg-white border-0 pt-3 pb-2"> 2471 <h6 class="mb-0 fw-bold"><i class="bi bi-trophy me-2 text-primary"></i>Top-Produkte (${d.range_days} Tage)</h6> 2472 </div> 2473 <div class="card-body p-0"> 2474 ${!hasTop ? '<p class="text-muted text-center py-4 mb-0">Noch keine Verkäufe im Zeitraum.</p>' : ` 2475 <div class="table-responsive"> 2476 <table class="table table-sm mb-0 align-middle"> 2477 <thead class="small text-muted"> 2478 <tr> 2479 <th class="ps-3">Produkt</th> 2480 <th class="text-end">Stück</th> 2481 <th class="text-end pe-3">Umsatz</th> 2482 </tr> 2483 </thead> 2484 <tbody> 2485 ${top.map(p => ` 2486 <tr> 2487 <td class="ps-3 small"> 2488 <div class="fw-semibold">${p.name || p.sku || 'Unbekannt'}</div> 2489 ${p.sku && p.sku !== p.name ? `<div class="text-muted" style="font-size:0.75rem">${p.sku}</div>` : ''} 2490 </td> 2491 <td class="text-end fw-semibold">${p.quantity}</td> 2492 <td class="text-end pe-3">${SD._fmtCents(p.revenue_cents)}</td> 2493 </tr> 2494 `).join('')} 2495 </tbody> 2496 </table> 2497 </div> 2498 `} 2499 </div> 2500 </div> 2501 </div> 2502 </div> 2503 `; 2504 }; 2505 2506 SD._destroyCharts = () => { 2507 Object.values(SD._charts).forEac
2507h(c => { try { c && c.destroy(); } catch(e){} }); 2508 SD._charts = {}; 2509 }; 2510 2511 SD._drawCharts = async (d) => { 2512 SD._destroyCharts(); 2513 await window.loadLib('chart'); 2514 2515 const series = d.revenue_series || []; 2516 const revCanvas = document.getElementById('shopDashRevenueChart'); 2517 if (revCanvas) { 2518 SD._charts.revenue = new Chart(revCanvas.getContext('2d'), { 2519 type: 'bar', 2520 data: { 2521 labels: series.map(s => SD._fmtDay(s.date)), 2522 datasets: [{ 2523 label: 'Umsatz (â¬)', 2524 data: series.map(s => (s.revenue_cents||0)/100), 2525 backgroundColor: '#0d6efd', 2526 borderRadius: 4, 2527 }] 2528 }, 2529 options: { 2530 responsive: true, 2531 maintainAspectRatio: false, 2532 plugins: { 2533 legend: { display: false }, 2534 tooltip: { 2535 callbacks: { 2536 label: (ctx) => { 2537 const s = series[ctx.dataIndex] || {}; 2538 return [ 2539 `Umsatz: ${new Intl.NumberFormat('de-DE',{style:'currency',currency:'EUR'}).format(ctx.parsed.y)}`, 2540 `Bestellungen: ${s.orders || 0}`, 2541 ]; 2542 } 2543 } 2544 } 2545 }, 2546 scales: { 2547 x: { grid: { display: false }, ticks: { font: { size: 10 } } }, 2548 y: { 2549 beginAtZero: true, 2550 ticks: { callback: (v) => new Intl.NumberFormat('de-DE',{notation:'compact',style:'currency',currency:'EUR'}).format(v) } 2551 } 2552 } 2553 } 2554 }); 2555 } 2556 2557 const status = d.status_breakdown || {}; 2558 const statusEntries = Object.entries(status).sort((a,b) => b[1] - a[1]); 2559 const statusCanvas = document.getElementById('shopDashStatusChart'); 2560 if (statusCanvas && statusEntries.length > 0) { 2561 const colorMap = { 2562 'paid': '#198754', 'confirmed': '#198754', 'delivered': '#198754', 2563 'shipped': '#0dcaf0', 'processing': '#0d6efd', 2564 'pending': '#ffc107', 'awaiting_payment': '#ffc107', 2565 'cancelled': '#6c757d', 'canceled': '#6c757d', 'refunded': '#adb5bd', 2566 'failed': '#dc3545', 2567 }; 2568 SD._charts.status = new Chart(statusCanvas.getContext('2d'), { 2569 type: 'doughnut', 2570 data: { 2571 labels: statusEntries.map(([k]) => k), 2572 datasets: [{ 2573 data: statusEntries.map(([,v]) => v), 2574 backgroundColor: statusEntries.map(([k]) => colorMap[k.toLowerCase()] || '#6c757d'), 2575 borderWidth: 2, 2576 borderColor: '#fff', 2577 }] 2578 }, 2579 options: { 2580 responsive: true, 2581 maintainAspectRatio: false, 2582 plugins: { 2583 legend: { position: 'bottom', labels: { font: { size: 11 }, boxWidth: 12 } } 2584 }, 2585 cutout: '60%' 2586 } 2587 }); 2588 } else if (statusCanvas) { 2589 statusCanvas.parentElement.innerHTML = '<p class="text-muted text-center mb-0 py-4">Keine Daten.</p>'; 2590 } 2591 }; 2592 2593 SD.render = async () => { 2594 const body = document.getElementById('shopDashB
2594ody'); 2595 if (!body) return; 2596 body.innerHTML = '<div class="text-center py-5"><div class="spinner-border" role="status"></div><p class="text-muted mt-2">Lade Kennzahlenâ¦</p></div>'; 2597 try { 2598 const token = await Auth.getIdToken(); 2599 const r = await fetch(`/api/shop/admin/dashboard?range=${SD.range}`, { 2600 headers: { 'Authorization': `Bearer ${token}` } 2601 }); 2602 if (!r.ok) throw new Error(`HTTP ${r.status}: ${await r.text()}`); 2603 const data = await r.json(); 2604 body.innerHTML = SD._renderBody(data); 2605 SD._drawCharts(data); 2606 } catch (e) { 2607 body.innerHTML = `<div class="alert alert-danger">Fehler beim Laden: ${e.message}</div>`; 2608 console.error('Shop dashboard error:', e); 2609 } 2610 }; 2611 2612 document.querySelectorAll('#shopDashRange button').forEach(btn => { 2613 btn.addEventListener('click', () => { 2614 SD.range = parseInt(btn.dataset.range, 10); 2615 document.querySelectorAll('#shopDashRange button').forEach(b => { 2616 b.classList.toggle('btn-primary', b === btn); 2617 b.classList.toggle('btn-outline-primary', b !== btn); 2618 }); 2619 SD.render(); 2620 }); 2621 }); 2622 document.querySelectorAll('#shopDashRange button').forEach(b => { 2623 const active = parseInt(b.dataset.range, 10) === SD.range; 2624 b.classList.toggle('btn-primary', active); 2625 b.classList.toggle('btn-outline-primary', !active); 2626 }); 2627 document.getElementById('shopDashReload').addEventListener('click', () => SD.render()); 2628 SD.render(); 2629 } else { 2630 const shopLabels = { 2631 'categories': { title: 'Shop-Kategorien', icon: 'bi-tags', desc: 'Kategorien für die Gliederung der Shop-Produkte.' } 2632 }; 2633 const info = shopLabels[shopSub] || { title: 'Shop', icon: 'bi-shop', desc: '' }; 2634 mainContent.innerHTML = ` 2635 <div class="container-fluid mt-4"> 2636 <div class="d-flex align-items-center mb-4"> 2637 <i class="bi ${info.icon}" style="font-size: 2rem; margin-right: 0.75rem; color: #0d6efd;"></i> 2638 <div> 2639 <h2 class="mb-0">${info.title}</h2> 2640 <p class="text-muted mb-0">${info.desc}</p> 2641 </div> 2642 </div> 2643 <div class="alert alert-info"> 2644 Produkte werden über <a href="#" onclick="Pages.loadPage('erp-products');return false;">ERP → Produkte</a> gepflegt. 2645 </div> 2646 </div> 2647 `; 2648 } 2649 } catch (error) { 2650 console.error('Error loading Shop page:', error); 2651 mainContent.innerHTML = '<div class="alert alert-danger">Fehler beim Laden der Shop-Seite: ' + error.message + '</div>'; 2652 } 2653 break; 2654 /* GASTRO_PHASE_A_MISSION_CONTROL */ 2655 case 'restaurant-mission': { 2656 try { 2657 const ts = new Date().getTime(); 2658 const mod = await import(`/static/modules/restaurantMissionControl.js?v=${ts}`); 2659 const Klass = mod.default || window.RestaurantMissionControl; 2660 if (!Klass) throw new Error('MissionControl module not found'); 2661 const inst = new Klass(); 2662 await inst.render(mainContent); 2663 } catch (err) { 2664 console.error('Mission Control load failed:', err); 2665 mainContent.innerHTML = '<div class="container py-4"><div class="alert alert-danger">Fehler beim Laden von Mission Control: ' + (err.message || err) + '</div></div>'; 2666 } 2667 return; 2668 } 2669 /* GASTRO_PHASE2_CASES (legacy, still routable internally) */ 2670 case 'restaurant-reservations': 2671 case 'restaurant-floor-plan': 2672 case 'restaurant-walkins': { 2673 try { 2674 const view = pageName === 'restaurant-floor-plan' ? 'floorplan' : 2675 pageName === 'restaurant-walkins' ? 'walkins' : 'reservations'; 2676 const ts = new Date().getTime(); 2677 const mod = await import(`/static/modules/restaurantReservations.js?v=${ts}`); 2678 const Klass = mod.RestaurantReservations || mod.default; 2679 if (!Klass) throw new Error('RestaurantReservations module not found'); 2680 const inst = new Klass(view);
2681 await inst.init(); 2682 } catch (err) { 2683 console.error('Gastro page load failed:', err); 2684 mainContent.innerHTML = '<div class="container py-4"><div class="alert alert-danger">Fehler beim Laden der Gastro-Seite: ' + (err.message || err) + '</div></div>'; 2685 } 2686 return; 2687 } 2688 default: 2689 console.error('Unknown page:', page); 2690 mainContent.innerHTML = '<div class="alert alert-danger">Seite nicht gefunden</div>'; 2691 } 2692 } catch (error) { 2693 console.error('Error loading page:', error); 2694 UI.showError('Fehler beim Laden der Seite: ' + error.message); 2695 } 2696 } 2697 2698 static updateActiveNavigation(page) { 2699 // Alle aktiven Links deaktivieren 2700 document.querySelectorAll('.nav-link.active').forEach(link => { 2701 link.classList.remove('active'); 2702 }); 2703 2704 // Den neuen aktiven Link setzen 2705 const navLink = document.querySelector(`.nav-link[data-page="${page}"]`); 2706 if (navLink) { 2707 navLink.classList.add('active'); 2708 } 2709 2710 // Ãbersetze die Menüpunkte nach einer Seitenänderung 2711 try { 2712 Language.translateMenuItems(); 2713 } catch (error) { 2714 console.error('Fehler beim Ãbersetzen der Menüpunkte:', error); 2715 } 2716 2717 // Show main app container if not already visible 2718 const mainApp = document.getElementById('mainApp'); 2719 const loginScreen = document.querySelector('#loginScreen, #loginScreen2'); 2720 2721 if (mainApp && loginScreen) { 2722 mainApp.classList.remove('d-none'); 2723 loginScreen.classList.add('d-none'); 2724 } 2725 } 2726 2727 static async loadRezeption() { 2728 try { 2729 const mainContent = document.getElementById('mainContent'); 2730 if (!mainContent) { console.error('Main content container not found'); return; } 2731 mainContent.innerHTML = `<div id="rezeptionHost" class="container-fluid mt-2"></div>`; 2732 return await loadRezeptionPage(); 2733 } catch (error) { 2734 console.error('Error loading rezeption:', error); 2735 } 2736 } 2737 2738 static async loadAnalytics(days) { 2739 try { 2740 const mainContent = document.getElementById('mainContent'); 2741 if (!mainContent) { 2742 console.error('Main content container not found'); 2743 return; 2744 } 2745 mainContent.innerHTML = `<div id="analyticsHost" class="container-fluid mt-2"></div>`; 2746 await loadAnalyticsPage(days); 2747 } catch (error) { 2748 console.error('Error loading analytics:', error); 2749 UI.showError('Fehler beim Laden der Auswertung'); 2750 } 2751 } 2752 2753 static async loadDashboard() { 2754 try { 2755 const mainContent = document.getElementById('mainContent'); 2756 if (!mainContent) { 2757 console.error('Main content container not found'); 2758 return; 2759 } 2760 2761 // Create dashboard container 2762 mainContent.innerHTML = ` 2763 <div id="dashboardStats"> 2764 <div class="text-center mt-5"> 2765 <div class="spinner-border" role="status"> 2766 <span class="visually-hidden">Loading...</span> 2767 </div> 2768 </div> 2769 </div> 2770 `; 2771 2772 // Load dashboard data 2773 await loadDashboardStats(); 2774 } catch (error) { 2775 console.error('Error loading dashboard:', error); 2776 UI.showError('Fehler beim Laden des Dashboards'); 2777 } 2778 } 2779 2780 static async loadDocumentation() { 2781 // Get main content container 2782 const mainContent = document.getElementById('mainContent'); 2783 if (!mainContent) { 2784 console.error('Main content container not found'); 2785 return; 2786 } 2787 2788 // Clear previous content 2789 mainContent.innerHTML = ` 2790 <div id="documentationSection" class="content-section"> 2791 <div class="container-fluid"> 2792 <div id="documentation-container"> 2793 <!-- Content will be dynamically added by Dokumentation.js --> 2794 </div> 2795 </div> 2796 </div> 2797 `; 2798 2799 // Call the showDocumentation function 2800 if (typeof window.showDocumentation === 'function') { 2801 window.showDocumentation(); 2802 } else { 2803 console.error('showDocumentation function not found'); 2804 } 2805 } 2806 2807 static async loadContacts() { 2808 try { 2809 const mainContent = document.getElementById('mainContent'); 2810 if (!mainContent) return; 2811 2812 // Mobile-Erkennung 2813 const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); 2814 2815 if (isMobile) { 2816 // EINFACHE Mobile-Version - nur Container 2817 mainContent.innerHTML = ` 2818 <div class="contacts-overview"> 2819 <div style="padding: 15px; background: #f8f9fa; margin-bottom: 10px;"> 2820 <h1 style="font-size: 20px; margin: 0;">${_voPraxisAnsicht() ? 'Patienten' : 'Kontakte'}</h1> 2821 </div> 2822 <div id="contactsContainer"> 2823 <div style="text-align: center; padding: 20px;"> 2824 <div>Lade Kontakte...</div> 2825 </div> 2826 </div> 2827 </div> 2828 `; 2829 2830 // Initialisiere Mobile-Kontakte 2831 if (window.contactsMobile && window.contactsMobile.init) { 2832 await window.contactsMobile.init(); 2833 } 2834 return; 2835 } 2836 2837 // Desktop-Version (unverändert) 2838 mainContent.innerHTML = ` 2839 <style> 2840 .contacts-header { 2841 display: flex; 2842 justify-content: space-between; 2843 align-items: center; 2844 margin-bottom: 2rem; 2845 padding: 1.5rem; 2846 background: linear-gradient(135deg, #ffffff 0%, #f5f5f5 100%); 2847 border-radius: 8px; 2848 box-shadow: 0 2px 4px rgba(0,0,0,0.05); 2849 } 2850 .contacts-header h1 { 2851 color: #2e2e2e; 2852 font-size: 1.5rem; 2853 margin: 0; 2854 } 2855 .contacts-actions { 2856 display: flex; 2857 gap: 10px; 2858 align-items: center; 2859 } 2860 </style> 2861 <div class="contacts-overview"> 2862 <div class="contacts-header"> 2863 <h1>${_voPraxisAnsicht() ? 'Patienten' : t('Kontakte')}</h1> 2864 <div class="contacts-actions"> 2865 <!-- Actions will be added here if needed --> 2866 </div> 2867 </div> 2868 2869 <div class="card mb-4"> 2870 <div class="search-filters"> 2871 <!-- Search Bar --> 2872 <div class="search-bar mb-3 d-flex"> 2873 <div class="input-group">
2874 <span class="input-group-text"> 2875 <i class="fas fa-search"></i> 2876 </span> 2877 <input type="text" 2878 class="form-control" 2879 id="searchInput" 2880 placeholder="${t('Suche nach Name, Firma, Telefon...')}" 2881 > 2882 <button class="btn btn-outline-secondary" id="clearSearchBtn">Ã</button> 2883 </div> 2884 </div> 2885 2886 <!-- Filters Row --> 2887 <div class="filters-row d-flex flex-column flex-md-row justify-content-between align-items-md-center"> 2888 <div class="filters d-flex flex-column flex-md-row gap-2 flex-grow-1 mb-3 mb-md-0"> 2889 <select class="form-select" id="leadStatusFilter"> 2890 <option value="">${t('Lead-Status: Alle')}</option> 2891 <option value="Angerufen">Angerufen</option> 2892 <option value="Anrufen">Anrufen</option> 2893 <option value="In Bearbeitung">In Bearbeitung</option> 2894 <option value="Kein Bedarf">Kein Bedarf</option> 2895 <option value="Kunde">Kunde</option> 2896 <option value="Neu">Neu</option> 2897 <option value="Nicht Qualifiziert">Nicht Qualifiziert</option> 2898 <option value="Qualifiziert">Qualifiziert</option> 2899 <option value="Wiedervorlage">Wiedervorlage</option> 2900 <option value="Abschluss">Abschluss</option> 2901 </select> 2902 <button class="btn btn-outline-primary" id="addLeadStatusBtn" title="Neuen Lead-Status erstellen"> 2903 <i class="fas fa-plus"></i> 2904 </button> 2905 2906 <select class="form-select" id="brancheFilter"> 2907 <option value="">${t('Branche: Alle')}</option> 2908 ${DEFAULT_INDUSTRIES.map(industry => 2909 `<option value="${industry}">${t(industry)}</option>` 2910 ).join('')} 2911 </select> 2912 2913 <select class="form-select" id="aufgabeFilter"> 2914 <option value="">${t('Aufgabe: Alle')}</option> 2915 ${AUFGABE_OPTIONS.map(task => 2916 `<option value="${task}">${t(task)}</option>` 2917 ).join('')} 2918 </select> 2919 2920 <!-- Datumsbereich Filter für letzte Ãnderung --> 2921 <div class="d-flex gap-1 align-items-center"> 2922 <input type="date" 2923 class="form-control" 2924 id="dateFromFilter" 2925 style="width: 150px;" 2926 title="Geändert von"> 2927 <span class="text-muted small">${t('bis')}</span> 2928 <input type="date" 2929 class="form-control" 2930 id="dateToFilter" 2931 style="width: 150px;" 2932 title="Geändert bis"> 2933 <button class="btn btn-outline-secondary btn-sm" 2934 id="clearDateFilterBtn" 2935 title="Datumsfilter zurücksetzen"> 2936 <i class="fas fa-times"></i> 2937 </button> 2938 </div> 2939 </div> 2940 2941 <div class="d-flex align-items-center gap-3">
2942 <span class="text-muted"> 2943 <i class="fas fa-filter"></i> 2944 <span id="contactCount"></span> 2945 </span> 2946 </div> 2947 </div> 2948 </div> 2949 </div> 2950 <div id="contactsContainer" class="mt-3"> 2951 <!-- Contacts will be loaded here --> 2952 </div> 2953 </div> 2954 `; 2955 // Event Listener für den Lead-Status Button 2956 document.getElementById('addLeadStatusBtn')?.addEventListener('click', openLeadStatusModal); 2957 2958 // Event Listener für den Erstellen-Button im Modal 2959 document.getElementById('createLeadStatusBtn')?.addEventListener('click', createCustomLeadStatus); 2960 2961 // Initialisiere die Kontakte 2962 const { contacts } = await import('./contacts.js?v=20260807-noshow2-soap1-p5f1-rrev1-bill1-intake1-alarm1-tokfix1-dat1-mod1b-nam1-merk1--kal1-kont1--kd1-prax1-nsl1-nsl2-impauth1-tdup2-dnd1-kf1-kf3-kt3-it1'); 2963 const user = await Auth.getCurrentUser(); 2964 if (!user) { 2965 throw new Error('Kein Benutzer angemeldet'); 2966 } 2967 2968 // Store the contacts manager instance globally for access from other components 2969 window.contactsInstance = contacts; 2970 2971 // Load and render contacts 2972 await contacts.init(); 2973 2974 } catch (error) { 2975 console.error('Error loading contacts:', error); 2976 UI.showError('Fehler beim Laden der Kontakte: ' + error.message); 2977 } 2978 } 2979 2980 static async loadCalendar() { 2981 const { Calendar } = await import('./calendar.js?v=20260826-tdup2-badge1-dnd1-kalrw2-vcall1-vserie1-it1'); 2982 return await Calendar.show(); 2983 } 2984 2985 static async loadCalendarSettings() { 2986 const { Calendar } = await import('./calendar.js?v=20260826-tdup2-badge1-dnd1-kalrw2-vcall1-vserie1-it1'); 2987 return await Calendar.showSettingsPage(); 2988 } 2989 2990 static async loadCalendarNewEvent() { 2991 const { Calendar } = await import('./calendar.js?v=20260826-tdup2-badge1-dnd1-kalrw2-vcall1-vserie1-it1'); 2992 return await Calendar.showNewEventPage(); 2993 } 2994 2995 // pn-settings-ui-v1: Einstellungen des Benachrichtigungs-Motors 2996 static async loadAnamneseTemplates() { // anamnese-route-v1 2997 const { AnamneseTemplates } = await import('./anamneseTemplates.js?v=20260807-anam2-anamfix1-intake1-intake1c-iv2-alarm1-nk1-nk2-tab1-merk1--fb1--fb2'); 2998 return await AnamneseTemplates.show(); 2999 } 3000 3001 // reviews-ui-v1: Zufriedenheit, Beschwerden, oeffentliche Bewertungen. 3002 static async loadReviews() { 3003 const { Reviews } = await import('./reviews.js?v=20260807-rev1-tokfix1-merk1'); 3004 return await Reviews.show(); 3005 } 3006 3007 // no-show-ui-v1: Ausfaelle erfassen, Risiko-Vorschau, Auswertung, Regeln. 3008 static async loadAiTeam(ziel) { 3009 const { AiTeam } = await import('./aiTeam.js?v=20260810-aiteam1b-takt1-ux1-ux2-dupfix1-tel1-org1-akttab1-akttab2-aus1-wz1-zug1-del1-bug1-chat1-chat2-board2-board2b-board3-board3b-board3c-board3d-akte1-akte1b-takt1-pf1-pf1b-stand1-mail1-mail2--abl1--abl2--abl3-einst1-einst2-pdf1-gpuoff1-mlh1-ftx1-ftx2--ext1--ext2-ct1-ct2-merk1-mailanh1-anlw1-anlw2-anlw3-anlw4-anlw5-bugfix1-bugfix2-loesefix1-app1-appurl1-spur1-spur2--paket1--paket2--paket3-auftrag1-auftrag2-wort1-wort2-wort3-mikro1-mikro2-mikro3-sendknop-sendknop2-convdel1-convdel2-appdaten1-rollen2-untern1-einstieg1b-routinen1-vier1-modellweg1-projekt1---gesicht1-gfx1-fadenverl1-chatanh1---nutz1---tok1---tok3---stuf1---stuf2-mit1---gesh1---gesh2-pffrisch1-rz1-ob1-adsui1-adso1-adso2-abbr1-fort1-ged1-untern2-adom1-dauto1-lohn1-supp1-mobil1-depot1-lim1-btntext1-hg1-hg1-neu1-mk2-pff2-kont1-ablknopf1-chatz1-dokdesk1-vertgrp1-vsliste1-piiui1-fw1-zug1-buero1-korb1-lz1-pfanl1-pfleer1-orga1-chef1-orgb1-kiteamob1-kiteamfix1-probez1-mail1-s4v1-karte1-verf1-bugfix1-zirk1-kiperm1-pford1-lauf1-wrang1-strat1-schlraus1-video1-kiinh1-kiinh3-chatlang1bc-sammeldel1-sammeldel2-absort1-fgtext1-fgtext2-abscroll1-mehrlauf1-spin1-dlspin1-fehlg1'); 3010 return await AiTeam.show(ziel); 3011 } 3012 3013 static async loadNoShow() {
3014 const { NoShow } = await import('./noShow.js?v=20260807-p34fix-tokfix1-merk1-nstab1'); 3015 return await NoShow.show(); 3016 } 3017 3018 static async loadPatientNotifySettings() { 3019 const { PatientNotifySettings } = await import('./patientNotifySettings.js?v=20260807-cwout1-tokfix1-merk1'); 3020 return await PatientNotifySettings.show(); 3021 } 3022 3023 static async loadCalendarTeam() { 3024 const { Calendar } = await import('./calendar.js?v=20260826-tdup2-badge1-dnd1-kalrw2-vcall1-vserie1-it1'); 3025 return await Calendar.showTeamPage(); 3026 } 3027 3028 static async loadTasks() { 3029 const { Tasks } = await import('./tasks.js?v=20260728-kanbandrop-merk1-aufg1-aufgmob1-aufg2-impauth1-herrenlos1'); 3030 return await Tasks.load(); 3031 } 3032 3033 static async loadTickets() { 3034 const { TicketSystem } = await import('./ticketSystem.js?v=20260728-singleinst-merk1--tick1--tick2-fwtick1-tickprio1-tickui1-kf3-tsuche1b'); 3035 const mainContent = document.getElementById('mainContent'); 3036 if (!mainContent) return; 3037 3038 mainContent.innerHTML = ` 3039 <div class="tickets-container"> 3040 <div class="ticket-nav"> 3041 <a href="#" class="ticket-nav-item active" data-view="dashboard"> 3042 <i class="fas fa-tachometer-alt"></i> ${t('Dashboard')} 3043 </a> 3044 <a href="#" class="ticket-nav-item" data-view="tickets"> 3045 <i class="fas fa-list"></i> ${t('Alle Tickets')} 3046 </a> 3047 <a href="#" class="ticket-nav-item" data-view="new"> 3048 <i class="fas fa-plus"></i> ${t('Neues Ticket')} 3049 </a> 3050 <a href="#" class="ticket-nav-item" data-view="analytics"> 3051 <i class="fas fa-chart-bar"></i> ${t('Analytics')} 3052 </a> 3053 </div> 3054 <div id="ticket-content"> 3055 <!-- Ticket content will be loaded here --> 3056 </div> 3057 </div> 3058 `; 3059 3060 // ticket-deeplink-v1: Sprung aus der digitalen Rezeption ("Details"). 3061 // Die Absicht steht schon vor dem Seitenaufbau fest - sonst wirft der 3062 // Aufbau der Ticket-Seite die Detailansicht sofort aufs Dashboard zurueck. 3063 const _deepLink = window.__ticketDeepLink || null; 3064 3065 // deeplink-dupnav-v1: app.js laeuft in mehreren Modul-Instanzen (Tag mit 3066 // Stempel + '../app.js'-Importe ohne Stempel), dadurch feuert pro Klick 3067 // mehr als ein Navigations-Handler und diese Funktion laeuft doppelt. 3068 // Der erste Lauf verbraucht die Sprungabsicht, der zweite saehe sie nicht 3069 // mehr und wuerde die frisch geoeffnete Detailansicht wieder aufs 3070 // Dashboard zuruecksetzen. pages.js gibt es nur EINMAL - deshalb wird hier 3071 // gemerkt, dass gerade ein Sprung lief, und ein unmittelbar folgender 3072 // Zweitlauf laesst die Ansicht in Ruhe. 3073 if (_deepLink) window.__ticketDeepLinkAt = Date.now(); 3074 const _recentDeepLink = !!window.__ticketDeepLinkAt && 3075 (Date.now() - window.__ticketDeepLinkAt) < 2500; 3076 3077 // Initialize ticket system if not already initialized 3078 // ticket-deeplink-v2: Erst- und Folgebesuch brauchen unterschiedliche 3079 // Behandlung. Bei einer FRISCHEN Instanz steht die Anmeldung noch nicht, 3080 // ein sofortiger Abruf lief in einen 401 und landete wieder im Dashboard. 3081 // Deshalb dort nur vormerken - init() oeffnet den Vorgang, sobald es kann. 3082 const _freshTicketSystem = !window.ticketSystem; 3083 if (_freshTicketSystem) { 3084 window.ticketSystem = new TicketSystem(); 3085 } else if (!_deepLink && !_recentDeepLink) { 3086 // ticket-viewguard-v1: switchView statt loadDashboard - setzt zusaetzlich 3087 // die aktive Ansicht zurueck (Render-Guards in ticketSystem.js). 3088 window.ticketSystem.switchView('dashboard'); 3089 } 3090 3091 if (_deepLink) { 3092 window.__ticketDeepLink = null; 3093 const _ts = window.ticketSystem; 3094 if (_ts && typeof _ts.loadTicketDetail === 'function') { 3095 _ts.currentView = 'detail'; // gegen den verzoegerten Dashboard-Render aus init() 3096 if (_freshTicketSystem) { 3097 _ts.__pendingDeepLink = _deepLink; 3098 // Auffangnetz, falls init() den Vorgang nicht abholt. 3099 setTimeout(() => { 3100 if (_ts.__pendingDeepLink === _deepLink) { 3101 _ts.__pendingDeepLink = null; 3102 _ts.currentView = 'detail'; 3103 _ts.loadTicketDetail(_deepLink); 3104 } 3105 }, 6000); 3106 } else { 3107 _ts.loadTicketDetail(_deepLink); 3108 } 3109 } 3110 } 3111 } 3112 3113 static async loadVerordnungen() { 3114 const mainContent = document.getElementById('mainContent'); 3115 if (!mainContent) return; 3116 3117 mainContent.innerHTML = '<div id="verordnungen-root"><div class="text-center p-5"><i class="fas fa-spinner fa-spin fa-2x"></i></div></div>'; 3118 3119 try { 3120 const timestamp = new Date().getTime(); 3121 const module = await import(`/static/modules/verordnungen.js?v=${timestamp}`); 3122 if (module.Verordnungen) { 3123 const v = new module.Verordnungen();
3124 await v.init(); 3125 } 3126 } catch (err) { 3127 console.error('Error loading Verordnungen module:', err); 3128 mainContent.innerHTML = '<div class="alert alert-danger m-3">Fehler beim Laden des Verordnungs-Moduls: ' + err.message + '</div>'; 3129 } 3130 } 3131 3132 3133 3134 // accounting-page-gate-v1: Zugriff auf eine Buchhaltungsseite. 3135 // Rolle admin darf immer; sonst entscheidet die Gruppe "accounting" im 3136 // Rechte-Schema. Bei jedem Fehler bleibt es geschlossen (fail-closed). 3137 static async accountingAccess(subPage) { 3138 const MAP = { 3139 dashboard: 'overview', settings: 'overview', bookings: 'bookings', 3140 receipts: 'receipts', reports: 'reports', bank: 'bank', 3141 tax: 'taxes', datev: 'datev', 3142 }; 3143 try { 3144 if (await Auth.checkAdminAccess()) return true; 3145 } catch (e) { /* keine Rolle -> Rechte-Schema entscheidet */ } 3146 try { 3147 if (!Pages._accPerm) { 3148 const token = await Auth.getIdToken(); 3149 Pages._accPerm = fetch('/api/permissions/me', { 3150 credentials: 'same-origin', 3151 headers: { Accept: 'application/json', Authorization: 'Bearer ' + token }, 3152 }).then(r => (r.ok ? r.json() : null)); 3153 } 3154 const res = await Pages._accPerm; 3155 const grp = res && res.effective && res.effective.groups 3156 && res.effective.groups.accounting; 3157 if (!grp || !grp.enabled || !grp.items) return false; 3158 return !!grp.items[MAP[subPage] || 'overview']; 3159 } catch (e) { 3160 return false; 3161 } 3162 } 3163 3164 static async loadAccountingPage(subPage) { 3165 const mainContent = document.getElementById("mainContent"); 3166 if (!mainContent) return; 3167 3168 // accounting-page-gate-v1: Die Buchhaltung haengt nicht mehr an der 3169 // Rolle, sondern am regulaeren Rechte-Schema (Gruppe "accounting"). 3170 // Der fruehere Rollen-Check hier hat die mandantenweise Freischaltung 3171 // ausgehebelt - der Menuepunkt war sichtbar, die Seite verweigerte. 3172 if (!(await Pages.accountingAccess(subPage))) { 3173 mainContent.innerHTML = `<div class="alert alert-warning mt-4"> 3174 <i class="fas fa-lock me-2"></i>Für die Buchhaltung fehlt Ihnen die Berechtigung. 3175 Ein Administrator kann sie unter âRechte & Zugriffeâ freischalten. 3176 </div>`; 3177 return; 3178 } 3179 3180 try { 3181 switch (subPage) { 3182 case 'dashboard': 3183 if (typeof AccountingDashboard !== 'undefined') { 3184 await AccountingDashboard.loadPageContent(mainContent); 3185 } else { 3186 mainContent.innerHTML = '<div class="text-center py-5"><div class="spinner-border"></div><p class="mt-2">Buchhaltungs-Dashboard wird geladen...</p></div>'; 3187 const script = document.createElement('script'); 3188 script.src = '/static/modules/accountingDashboard.js?v=' + Date.now(); 3189 script.onload = () => AccountingDashboard.loadPageContent(mainContent); 3190 document.head.appendChild(script); 3191 } 3192 break; 3193 case 'bookings': 3194 if (typeof AccountingBookings !== 'undefined') { 3195 await AccountingBookings.loadPageContent(mainContent); 3196 } else { 3197 mainContent.innerHTML = '<div class="text-center py-5"><div class="spinner-border"></div><p class="mt-2">Buchungen werden geladen...</p></div>'; 3198 const script = document.createElement('script'); 3199 script.src = '/static/modules/accountingBookings.js?v=' + Date.now(); 3200 script.onload = () => AccountingBookings.loadPageContent(mainContent); 3201 document.head.appendChild(script); 3202 } 3203 break; 3204 case 'reports': 3205 try { 3206 const repMod = await import('./accountingReports.js?v=20260807-tokens-merk1'); 3207 repMod.loadAccountingReportsPage(); 3208 } catch (err) { 3209 console.error('Error loading Reports:', err); 3210 mainContent.innerHTML = '<div class="alert alert-danger">Fehler: ' + err.message + '</div>'; 3211 } 3212 break; 3213 case 'tax': 3214 // accounting-tax-ui-v1: lud vorher accountingReports.js - 3215 // drei Menuepunkte, eine Seite, kein eigener Inhalt. 3216 try { 3217 const taxMod = await import('./accountingTax.js?v=20260807-tokens-merk1'); 3218 taxMod.loadAccountingTaxPage(); 3219 } catch (err) { 3220 console.error('Error loading Tax:', err); 3221 mainContent.innerHTML = '<div class="alert alert-danger">Fehler: ' + err.message + '</div>'; 3222 } 3223 break; 3224 case 'datev': 3225 // accounting-datev-ui-v1: eigene Seite statt der Auswertungen 3226 try { 3227 const datevMod = await import('./accountingDatev.js?v=20260807-tokens-merk1'); 3228 await datevMod.loadAccountingDatevPage(); 3229 } catch (err) { 3230 console.error('Error loading DATEV:', err); 3231 mainContent.innerHTML = '<div class="alert alert-danger">Fehler: ' + err.message + '</div>'; 3232 } 3233 break; 3234 case 'receipts': 3235 try { 3236 const recMod = await import('./accountingReceipts.js?v=20260807-fix-merk1-belegid1-fbtn1'); 3237 recMod.loadAccountingReceiptsPage(); 3238 } catch (err) { 3239 console.error('Error loading Receipts:', err); 3240 mainContent.innerHTML = '<div class="alert alert-danger">Fehler: ' + err.message + '</div>'; 3241 } 3242 break; 3243 case 'settings':
3244 // accounting-settings-ui-v1: war ein Platzhalter, obwohl das 3245 // Backend vollstaendig da war. Ohne diese Seite liessen sich 3246 // Steuernummer und DATEV-Nummern gar nicht eintragen. 3247 try { 3248 const setMod = await import('./accountingSettings.js?v=20260807-tokens-merk1'); 3249 await setMod.loadAccountingSettingsPage(); 3250 } catch (err) { 3251 console.error('Error loading Settings:', err); 3252 mainContent.innerHTML = '<div class="alert alert-danger">Fehler: ' + err.message + '</div>'; 3253 } 3254 break; 3255 default: 3256 mainContent.innerHTML = '<div class="alert alert-warning mt-4">Seite nicht gefunden.</div>'; 3257 } 3258 } catch (error) { 3259 console.error('Error loading accounting page:', error); 3260 mainContent.innerHTML = `<div class="alert alert-danger mt-4">Fehler beim Laden: ${error.message}</div>`; 3261 } 3262 } 3263 3264 static async loadAdsPage(page) { 3265 const mainContent = document.getElementById("mainContent"); 3266 if (!mainContent) return; 3267 3268 try { 3269 // Try to load as JS module first with cache buster 3270 const timestamp = new Date().getTime();
3271 const modulePath = `/static/modules/${page}.js?v=${timestamp}`; 3272 3273 try { 3274 const module = await import(modulePath); 3275 3276 // Call the module's init function based on page name 3277 if (module.default) { 3278 await module.default(); 3279 } else if (page === 'ads-campaigns' && module.initAdsCampaigns) { 3280 await module.initAdsCampaigns(); 3281 } else if (page === 'ads-campaigns-new' && module.initCampaignCreatorV2) { 3282 await module.initCampaignCreatorV2(); 3283 } else if (page === 'ads-keywords' && module.initAdsKeywords) { 3284 await module.initAdsKeywords(); 3285 } else if (page === 'ads-alerts' && module.initAdsAlerts) { 3286 await module.initAdsAlerts(); 3287 } else if (page === 'ads-automation' && module.initAdsAutomation) { 3288 await module.initAdsAutomation(); 3289 } else if (page === 'ads-reports' && module.initAdsReports) { 3290 await module.initAdsReports(); 3291 // Store the module reference for cleanup 3292 this.currentPageModule = window.adsReports; 3293 } else if (page === 'ads-bulk' && module.initAdsBulk) { 3294 await module.initAdsBulk(); 3295 } else if (page === 'ads-dashboard' && module.initAdsDashboard) { 3296 await module.initAdsDashboard(); 3297 } else if (page === 'ads-setup' && module.initAdsSetup) { 3298 await module.initAdsSetup(); 3299 } else if (page === 'tracking-manager' && module.initTrackingManager) { 3300 await module.initTrackingManager(); 3301 } else if (page === 'tracking-dashboard' && module.initTrackingDashboard) { 3302 await module.initTrackingDashboard(); 3303 } else if (page === 'ticketSystem') { 3304 // TicketSystem is a class, instantiate it 3305 if (module.TicketSystem) { 3306 window.ticketSystem = new module.TicketSystem(); 3307 } else if (module.initTicketSystem) { 3308 await module.initTicketSystem(); 3309 } 3310 } 3311 3312 console.log(`Loaded Google Ads JS module: ${page}`); 3313 return; 3314 } catch (moduleError) { 3315 // If JS module doesn't exist, try HTML fallback 3316 console.log(`JS module not found for ${page}, trying HTML...`); 3317 } 3318 3319 // Fallback to HTML loading 3320 const response = await fetch(`/static/modules/${page}.html`); 3321 if (!response.ok) { 3322 throw new Error(`Failed to load ${page}`); 3323 } 3324 const html = await response.text(); 3325 mainContent.innerHTML = html; 3326 3327 // Execute scripts in the loaded HTML 3328 const scripts = mainContent.querySelectorAll("script"); 3329 scripts.forEach(script => { 3330 try { 3331 const newScript = document.createElement("script"); 3332 // Check if it's a module script 3333 if (script.type === "module") { 3334 newScript.type = "module"; 3335 } 3336 newScript.textContent = script.textContent; 3337 document.body.appendChild(newScript); 3338 // Don't remove module scripts immediately 3339 if (script.type !== "module") { 3340 setTimeout(() => document.body.removeChild(newScript), 100); 3341 } 3342 } catch (e) { 3343 console.error("Error executing script:", e); 3344 } 3345 }); 3346 3347 console.log(`Loaded Google Ads HTML page: ${page}`); 3348 } catch (error) { 3349 console.error(`Error loading ${page}:`, error); 3350 mainContent.innerHTML = `<div class="alert alert-danger">Fehler beim Laden der Seite ${page}</div>`; 3351 } 3352 } 3353 static async loadCallDetail() { // calldetail-v1 3354 const id = new URLSearchParams(window.location.search).get('id'); 3355 const { CallDetail } = await import('./callDetail.js?v=20260805-calldetail5-merk1-sperr1-ri1'); 3356 return await CallDetail.load(id); 3357 } 3358 3359 static async loadCallSummary() { 3360 const { CallSummary } = await import('./callSummary.js?v=20260805-calldetail5-soap1-p5f1-rrev1-bill1-intake1-alarm1-tokfix1-dat1-mod1b-nam1-merk1--kal1-kont1--kd1-chat1-chat2-voice1-prax1-nsl1-nsl2-impauth1-tdup2-testf1-waueb1-testsicht1-kteam1-kt3'); 3361 await CallSummary.load(); 3362 } 3363 3364 static async loadCallAnalysis() { 3365 const { CallAnalysis } = await import('./callAnalysis.js'); 3366 // Wrap the load call in a setTimeout to ensure DOM is ready 3367 setTimeout(async () => { 3368 try { 3369 await CallAnalysis.load(); 3370 } catch (error) { 3371 console.error("Error executing CallAnalysis.load() after timeout:", error); 3372 // Optionally display an error to the user in the UI 3373 const mainContent = document.getElementById('mainContent'); 3374 if (mainContent) { 3375 mainContent.innerHTML = `<div class="alert alert-danger">Fehler beim Initialisieren der Gesprächsanalyse: ${error.message}</div>`; 3376 } 3377 } 3378 }, 0); // Delay of 0ms is often sufficient 3379 } 3380 3381 static async loadWhitelabel() { 3382 console.log('Loading whitelabel page...'); 3383 try { 3384 const isAdmin = await Auth.checkAdminAccess(); 3385 console.log('Admin access check result:', isAdmin); 3386 3387 if (!isAdmin) {
3388 console.log('Access denied: User is not an admin'); 3389 return this.showNoAccessError(); 3390 } 3391 3392 console.log('Access granted: Loading whitelabel content'); 3393 return await WhiteLabel.load(); 3394 } catch (error) { 3395 console.error('Error in loadWhitelabel:', error); 3396 return this.showNoAccessError(); 3397 } 3398 } 3399 3400 static showNoAccessError() { 3401 const mainContent = document.getElementById('mainContent'); 3402 if (!mainContent) { 3403 console.error('Main content container not found'); 3404 return; 3405 } 3406 3407 mainContent.innerHTML = ` 3408 <div class="container-fluid"> 3409 <div class="alert alert-danger" role="alert"> 3410 <i class="fas fa-exclamation-triangle me-2"></i> 3411 Sie haben keine Berechtigung für diese Seite. 3412 </div> 3413 </div> 3414 `; 3415 } 3416 3417 static async loadCreateUser() { 3418 const content = document.getElementById('content'); 3419 content.innerHTML = ` 3420 <div class="container-fluid"> 3421 <h1>Benutzer anlegen</h1> 3422 3423 <div class="card"> 3424 <div class="card-body"> 3425 <form id="createUserForm"> 3426 <div class="row g-3"> 3427 <div class="col-md-6"> 3428 <label for="createUserEmail" class="form-label">E-Mail</label> 3429 <input type="email" class="form-control" id="createUserEmail" required> 3430 </div> 3431 <div class="col-md-6"> 3432 <label for="createUserPassword" class="form-label">Passwort</label> 3433 <input type="password" class="form-control" id="createUserPassword" required> 3434 </div> 3435 <div class="col-md-6"> 3436 <label for="createUserRole" class="form-label">Rolle</label> 3437 <select class="form-select" id="createUserRole" required> 3438 <option value="user">Benutzer</option> 3439 <option value="admin">Administrator</option> 3440 </select> 3441 </div> 3442 </div> 3443 <button type="submit" class="btn btn-primary mt-3"> 3444 <i class="fas fa-plus"></i> Benutzer erstellen 3445 </button> 3446 </form> 3447 </div> 3448 </div> 3449 </div> 3450 `; 3451 3452 // Add form submit handler 3453 const createUserForm = document.getElementById('createUserForm'); 3454 if (createUserForm) { 3455 createUserForm.addEventListener('submit', async (e) => { 3456 e.preventDefault(); 3457 const email = document.getElementById('createUserEmail').value; 3458 const password = document.getElementById('createUserPassword').value; 3459 const role = document.getElementById('createUserRole').value; 3460 3461 if (email && password && role) { 3462 await Users.create(email, password, role); 3463 createUserForm.reset(); 3464 } 3465 }); 3466 } 3467 } 3468 3469 static async loadMulticall() { 3470 const mainContent = document.getElementById('mainContent'); 3471 mainContent.innerHTML = ` 3472 <div class="container-fluid"> 3473 <h1 class="mb-4">Multicall</h1> 3474 <div class="row"> 3475 <div class="col-12"> 3476 <!-- Multicall content will be added here --> 3477 <div class="alert alert-info"> 3478 Multicall-Funktion wird geladen... 3479 </div> 3480 </div> 3481 </div> 3482 </div> 3483 `; 3484 // You can add more multicall-specific functionality here 3485 } 3486 3487 static async loadDeals() { 3488 const dealsManager = (await import("./deals.js?v=phase8-merk1-deal1-deal2-deal3-drag1-drag2-impauth1-kf3")).dealsManager; 3489 await dealsManager.init(); 3490 } 3491 3492 static async loadGoogleContactsIntegration() { 3493 const mainContent = document.getElementById('mainContent'); 3494 if (!mainContent) { 3495 throw new Error('Main content container not found'); 3496 } 3497 3498 try { 3499 // Import required modules 3500 // showToast is not available, use UI methods instead 3501 const { Auth } = await import('./auth.js'); 3502 3503 // Lade die HTML-Seite 3504 const response = await fetch('/static/pages/google-contacts-integration.html'); 3505 if (!response.ok) { 3506 throw new Error('Failed to load Google Contacts integration page'); 3507 } 3508 3509 const html = await response.text(); 3510 3511 // Parse HTML to extract script content 3512 const parser = new DOMParser(); 3513 const doc = parser.parseFromString(html, 'text/html'); 3514 3515 // Extract script content before removing 3516 const scripts = doc.querySelectorAll('script[type="module"]'); 3517 const scriptContents = [];
3518 scripts.forEach(script => { 3519 if (script.textContent && script.textContent.includes('translatePage')) { 3520 scriptContents.push(script.textContent); 3521 } 3522 script.remove(); 3523 }); 3524 3525 // Set the HTML content 3526 mainContent.innerHTML = doc.body.innerHTML; 3527 3528 // Execute the translation script 3529 if (scriptContents.length > 0) { 3530 console.log('[Pages] Executing translation script from HTML'); 3531 // Create and execute a new script module 3532 const moduleScript = document.createElement('script'); 3533 moduleScript.type = 'module'; 3534 moduleScript.textContent = scriptContents[0]; 3535 document.body.appendChild(moduleScript); 3536 // Remove after execution 3537 setTimeout(() => moduleScript.remove(), 1000); 3538 } 3539 3540 // Initialize Google People Integration using the imported module 3541 // Remove local duplicate - use imported module instead 3542 /*const GooglePeopleIntegration = { 3543 CLIENT_ID: '666681974357-5aifspupr1roo2dqi1bu9kovl41ouifg.apps.googleusercontent.com', 3544 SCOPES: 'https://www.googleapis.com/auth/contacts https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email', 3545 REDIRECT_URI: `${window.location.origin}/api/auth/google-people/callback`, 3546 connectionStatus: null, 3547 3548 async init() { 3549 console.log('Initialisiere Google People Integration'); 3550 await this.checkConnectionStatus(); 3551 this.setupEventListeners(); 3552 }, 3553 3554 async checkConnectionStatus() { 3555 try { 3556 const response = await fetch('/api/auth/google-people/status', { 3557 headers: { 3558 'Authorization': `Bearer ${await Auth.getIdToken()}` 3559 } 3560 }); 3561 3562 if (response.ok) { 3563 this.connectionStatus = await response.json(); 3564 this.updateUI(); 3565 } 3566 } catch (error) { 3567 console.error('Fehler beim Prüfen des Verbindungsstatus:', error); 3568 this.updateUI(); 3569 } 3570 }, 3571 3572 updateUI() { 3573 const container = document.getElementById('google-people-status'); 3574 if (!container) return; 3575 3576 if (this.connectionStatus?.connected) { 3577 container.innerHTML = ` 3578 <div class="integration-card connected"> 3579 <div class="integration-header"> 3580 <img src="https://www.gstatic.com/images/branding/product/2x/contacts_48dp.png" 3581 alt="Google Contacts" class="integration-logo"> 3582 <div class="integration-info"> 3583 <h3>Google Contacts</h3> 3584 <p class="status">Verbunden als ${this.connectionStatus.email}</p> 3585 </div> 3586 </div> 3587 <div class="integration-details"> 3588 <p><strong>Letzte Synchronisation:</strong> 3589 ${this.connectionStatus.last_sync ? 3590 new Date(this.connectionStatus.last_sync).toLocaleString('de-DE') : 3591 'Noch nicht synchronisiert'} 3592 </p> 3593 <p><strong>Sync-Richtung:</strong>
3594 ${this.getSyncDirectionLabel(this.connectionStatus.sync_direction)} 3595 </p> 3596 </div> 3597 <div class="integration-actions"> 3598 <button onclick="window.GooglePeopleIntegration.startSync()" class="btn btn-secondary"> 3599 <i class="fas fa-sync"></i> Jetzt synchronisieren 3600 </button> 3601 <button onclick="window.GooglePeopleIntegration.showSettings()" class="btn btn-secondary"> 3602 <i class="fas fa-cog"></i> Einstellungen 3603 </button> 3604 <button onclick="window.GooglePeopleIntegration.disconnect()" class="btn btn-danger"> 3605 <i class="fas fa-unlink"></i> Verbindung trennen 3606 </button> 3607 </div> 3608 </div> 3609 `; 3610 } else { 3611 container.innerHTML = ` 3612 <div class="integration-card disconnected"> 3613 <div class="integration-header"> 3614 <img src="https://www.gstatic.com/images/branding/product/2x/contacts_48dp.png" 3615 alt="Google Contacts" class="integration-logo"> 3616 <div class="integration-info"> 3617 <h3>Google Contacts</h3> 3618 <p class="status">Nicht verbunden</p> 3619 </div> 3620 </div> 3621 <div class="integration-description"> 3622 <p>Verbinden Sie Ihre Google Kontakte für automatische Synchronisation:</p> 3623 <ul> 3624 <li>Automatischer Import Ihrer Google Kontakte</li> 3625 <li>Bidirektionale Synchronisation</li> 3626 <li>Feldmapping zwischen voiceOne und Google</li> 3627 </ul> 3628 </div> 3629 <div class="integration-actions"> 3630 <button onclick="window.GooglePeopleIntegration.connect()" class="btn btn-primary"> 3631 <i class="fab fa-google"></i> Mit Google Contacts verbinden 3632 </button> 3633 </div> 3634 </div> 3635 `; 3636 } 3637 }, 3638 3639 async connect() { 3640 console.log('GooglePeopleIntegration.connect() called from pages.js'); 3641 try { 3642 // Generiere state mit Service-Indikator 3643 const stateData = { 3644 service: 'people', 3645 nonce: Math.random().toString(36).substring(7) 3646 }; 3647 const state = btoa(JSON.stringify(stateData)); 3648 sessionStorage.setItem('google_people_oauth_state', state); 3649 console.log('Saving OAuth state:', { 3650 stateData: stateData, 3651 encodedState: state, 3652 stateLength: state.length, 3653 verifyStorage: sessionStorage.getItem('google_people_oauth_state') === state 3654 }); 3655 3656 const params = new URLSearchParams({ 3657 client_id: this.CLIENT_ID, 3658 redirect_uri: this.REDIRECT_URI, 3659 response_type: 'code', 3660 scope: this.SCOPES, 3661 access_type: 'offline', 3662 prompt: 'consent', 3663 state: state 3664 }); 3665 3666 const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${params}`; 3667 3668 console.log('Starte Google People OAuth Flow...'); 3669 console.log('Leite um zu:', authUrl); 3670 3671 // OAuth-Flow durch Weiterleitung starten 3672 window.location.href = authUrl; 3673 3674 } catch (error) { 3675 console.error('Fehler beim Verbinden mit Google People:', error); 3676 UI.showError('Fehler beim Verbinden mit Google Contacts'); 3677 } 3678 }, 3679 3680 async exchangeCodeForToken(code) { 3681 try { 3682 UI.showInfo('Verbinde mit Google Contacts...'); 3683 3684 const response = await fetch('/api/auth/google-people/token', { 3685 method: 'POST', 3686 headers: { 3687 'Content-Type': 'application/json', 3688 'Authorization': `Bearer ${await Auth.getIdToken()}` 3689 }, 3690 body: JSON.stringify({ 3691 code: code, 3692 redirect_uri: this.REDIRECT_URI, 3693 client_id: this.CLIENT_ID 3694 }) 3695 }); 3696 3697 if (response.ok) { 3698 const data = await response.json(); 3699 UI.showSuccess('Erfolgreich mit Google Contacts verbunden'); 3700 await this.checkConnectionStatus(); 3701 this.startSync(); 3702 } else { 3703 const error = await response.json(); 3704 console.error('Token exchange failed:', error); 3705 UI.showError('Fehler beim Verbinden mit Google Contacts'); 3706 } 3707 } catch (error) { 3708 console.error('Fehler beim Token-Austausch:', error); 3709 UI.showError('Fehler beim Verbinden mit Google Contacts'); 3710 } 3711 }, 3712 3713 async disconnect() { 3714 if (!confirm(t('Möchten Sie die Verbindung zu Google Contacts wirklich trennen?'))) { 3715 return; 3716 } 3717 3718 try { 3719 const response = await fetch('/api/auth/google-people/disconnect', { 3720 method: 'POST', 3721 headers: { 3722 'Authorization': `Bearer ${await Auth.getIdToken()}` 3723 } 3724 }); 3725 3726 if (response.ok) { 3727 UI.showSuccess('Verbindung zu Google Contacts getrennt'); 3728 await this.checkConnectionStatus(); 3729 } else { 3730 UI.showError('Fehler beim Trennen der Verbindung'); 3731 } 3732 } catch (error) { 3733 console.error('Fehler beim Trennen:', error); 3734 UI.showError('Fehler beim Trennen der Verbindung'); 3735 } 3736 }, 3737 3738 async startSync() { 3739 try { 3740 UI.showInfo('Synchronisiere Kontakte...'); 3741 3742 const response = await fetch('/api/contacts/sync/google', { 3743 method: 'POST', 3744 headers: { 3745 'Authorization': `Bearer ${await Auth.getIdToken()}` 3746 } 3747 }); 3748 3749 if (response.ok) { 3750 const data = await response.json(); 3751 UI.showInfo('Synchronisation gestartet. Dies kann einige Minuten dauern.'); 3752 3753 setTimeout(() => { 3754 this.checkConnectionStatus(); 3755 }, 5000); 3756 } else { 3757 const error = await response.json(); 3758 UI.showError(error.error || 'Fehler beim Starten der Synchronisation'); 3759 } 3760 3761 } catch (error) { 3762 console.error('Fehler beim Starten der Synchronisation:', error); 3763 UI.showError('Fehler beim Starten der Synchronisation'); 3764 } 3765 }, 3766 3767 async showSettings() { 3768 const modal = document.createElement('div'); 3769 modal.className = 'modal fade show'; 3770 modal.style.display = 'block'; 3771 modal.innerHTML = ` 3772 <div class="modal-dialog"> 3773 <div class="modal-content"> 3774 <div class="modal-header"> 3775 <h5 class="modal-title">Google Contacts Einstellungen</h5> 3776 <button type="button" class="btn-close" onclick="this.closest('.modal').remove()"></button> 3777 </div> 3778 <div class="modal-body"> 3779 <form id="google-people-settings-form"> 3780 <div class="mb-3"> 3781 <label class="form-label">Synchronisationsrichtung</label> 3782 <select class="form-select" id="sync-direction"> 3783 <option value="bidirectional" ${this.connectionStatus?.sync_direction === 'bidirectional' ? 'selected' : ''}> 3784 Bidirektional (Empfohlen) 3785 </option> 3786 <option value="from_google" ${this.connectionStatus?.sync_direction === 'from_google' ? 'selected' : ''}> 3787 Nur von Google zu voiceOne 3788 </option> 3789 <option value="to_google" ${this.connectionStatus?.sync_direction === 'to_google' ? 'selected' : ''}> 3790 Nur von voiceOne zu Google 3791 </option> 3792 </select> 3793 </div> 3794 <div class="mb-3"> 3795 <div class="form-check"> 3796 <input class="form-check-input" type="checkbox" id="sync-enabled" 3797 ${this.connectionStatus?.sync_enabled !== false ? 'checked' : ''}> 3798 <label class="form-check-label" for="sync-enabled"> 3799 Automatische Synchronisation aktiviert 3800 </label> 3801 </div> 3802 </div> 3803 <div class="alert alert-info"> 3804 <i class="fas fa-info-circle"></i> 3805 Die automatische Synchronisation erfolgt alle 30 Minuten. 3806 </div> 3807 </form> 3808 </div> 3809 <div class="modal-footer"> 3810 <button type="button" class="btn btn-secondary" onclick="this.closest('.modal').remove()"> 3811 Abbrechen 3812 </button> 3813 <button type="button" class="btn btn-primary" onclick="window.GooglePeopleIntegration.saveSettings()"> 3814 Speichern 3815 </button> 3816 </div> 3817 </div> 3818 </div> 3819 `; 3820 document.body.appendChild(modal); 3821 3822 // Add backdrop 3823 const backdrop = document.createElement('div'); 3824 backdrop.className = 'modal-backdrop fade show'; 3825 document.body.appendChild(backdrop); 3826 3827 // Remove backdrop when modal is closed 3828 modal.querySelector('.btn-close').addEventListener('click', () => { 3829 backdrop.remove(); 3830 }); 3831 modal.querySelector('.btn-secondary').addEventListener('click', () => { 3832 backdrop.remove(); 3833 }); 3834 }, 3835 3836 async saveSettings() { 3837 const syncDirection = document.getElementById('sync-direction').value; 3838 const syncEnabled = document.getElementById('sync-enabled').checked; 3839 3840 try { 3841 const response = await fetch('/api/auth/google-people/settings', { 3842 method: 'POST', 3843 headers: { 3844 'Content-Type': 'application/json', 3845 'Authorization': `Bearer ${await Auth.getIdToken()}` 3846 }, 3847 body: JSON.stringify({ 3848 sync_direction: syncDirection, 3849 sync_enabled: syncEnabled 3850 }) 3851 }); 3852 3853 if (response.ok) { 3854 UI.showSuccess('Einstellungen gespeichert'); 3855 // Remove modal and backdrop 3856 document.querySelector('.modal').remove(); 3857 document.querySelector('.modal-backdrop').remove(); 3858 // Refresh connection status 3859 await this.checkConnectionStatus(); 3860 } else { 3861 const error = await response.json(); 3862 UI.showError(error.error || 'Fehler beim Speichern der Einstellungen'); 3863 } 3864 } catch (error) { 3865 console.error('Fehler beim Speichern der Einstellungen:', error); 3866 UI.showError('Fehler beim Speichern der Einstellungen'); 3867 } 3868 }, 3869 3870 getSyncDirectionLabel(direction) { 3871 const labels = { 3872 'bidirectional': 'Bidirektional', 3873 'from_google': 'Von Google zu voiceOne', 3874 'to_google': 'Von voiceOne zu Google' 3875 }; 3876 return labels[direction] || direction; 3877 }, 3878 3879 setupEventListeners() { 3880 // Check if we're returning from OAuth callback
3881 const urlParams = new URLSearchParams(window.location.search); 3882 const code = urlParams.get('code'); 3883 const state = urlParams.get('state'); 3884 const error = urlParams.get('error'); 3885 3886 console.log('OAuth callback check:', { 3887 hasCode: !!code, 3888 hasState: !!state, 3889 hasError: !!error, 3890 rawURL: window.location.search, 3891 decodedState: state 3892 }); 3893 3894 if (code || error) { 3895 // We're returning from OAuth, handle the redirect 3896 this.handleRedirect(code, state, error); 3897 } 3898 }, 3899 3900 async handleRedirect(code, state, error) { 3901 try { 3902 if (error) { 3903 console.error('Google OAuth Error:', error); 3904 UI.showError(`Fehler bei der Verbindung mit Google: ${error}`); 3905 return; 3906 } 3907 3908 if (!code) { 3909 console.error('Kein Autorisierungscode erhalten'); 3910 UI.showError('Fehler bei der Google-Verbindung: Kein Code erhalten'); 3911 return; 3912 } 3913 3914 // Validate state 3915 const savedState = sessionStorage.getItem('google_people_oauth_state'); 3916 console.log('State validation:', { 3917 savedState: savedState, 3918 receivedState: state, 3919 statesMatch: savedState === state, 3920 savedStateLength: savedState ? savedState.length : 0, 3921 receivedStateLength: state ? state.length : 0 3922 }); 3923 3924 // If no saved state, check if the received state is valid for Google People 3925 if (!savedState && state) { 3926 try { 3927 const decodedState = JSON.parse(atob(state)); 3928 if (decodedState.service === 'people') { 3929 console.log('No saved state, but received valid Google People state - proceeding'); 3930 // Continue with the flow 3931 } else { 3932 console.error('Invalid state service:', decodedState.service); 3933 UI.showError('Ungültiger OAuth State'); 3934 return; 3935 } 3936 } catch (e) { 3937 console.error('Failed to decode state:', e); 3938 UI.showError('Sicherheitsfehler bei der Google-Verbindung'); 3939 return; 3940 } 3941 } else if (savedState !== state) { 3942 console.error('OAuth state mismatch - saved:', savedState, 'received:', state); 3943 UI.showError('Sicherheitsfehler bei der Google-Verbindung'); 3944 return; 3945 } 3946 3947 // Clear state from session 3948 sessionStorage.removeItem('google_people_oauth_state'); 3949 3950 // Remove OAuth params from URL immediately to prevent code reuse 3951 const newUrl = window.location.pathname + window.location.hash; 3952 window.history.replaceState({}, document.title, newUrl); 3953 3954 // Exchange code for token 3955 await this.exchangeCodeForToken(code); 3956 3957 } catch (error) { 3958 console.error('Fehler beim Verarbeiten der OAuth-Weiterleitung:', error); 3959 UI.showError('Fehler bei der Google-Verbindung'); 3960 } 3961 } 3962 };*/ 3963 3964 // Debug logging 3965 console.log('[DEBUG] Google Contacts Integration Page Loading'); 3966 console.log('[DEBUG] GooglePeopleIntegration available?', typeof GooglePeopleIntegration); 3967 console.log('[DEBUG] GooglePeopleIntegration object:', GooglePeopleIntegration); 3968 3969 // Use the imported GooglePeopleIntegration module 3970 // Already available globally through googlePeople.js 3971 3972 try { 3973 // Initialize the imported module 3974 console.log('[DEBUG] Calling GooglePeopleIntegration.init()'); 3975 await GooglePeopleIntegration.init(); 3976 console.log('[DEBUG] GooglePeopleIntegration.init() completed'); 3977 } catch (initError) { 3978 console.error('[DEBUG] Error initializing GooglePeopleIntegration:', initError); 3979 console.error('[DEBUG] Stack trace:', initError.stack); 3980 } 3981 3982 } catch (error) { 3983 console.error('Error loading Google Contacts integration:', error); 3984 mainContent.innerHTML = ` 3985 <div class="container py-4"> 3986 <div class="alert alert-danger"> 3987 Fehler beim Laden der Google Contacts Integration: ${error.message} 3988 </div> 3989 <button class="btn btn-secondary" onclick="Pages.loadPage('system-interfaces')"> 3990 <i class="fas fa-arrow-left"></i> Zurück 3991 </button> 3992 </div> 3993 `; 3994 } 3995 } 3996} 3997 3998// Make Pages available globally 3999window.Pages = Pages;
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.