https://energyexperts.ai/wp-content/plugins/energyExperts/assets/js/ee-widget-system.js?ver=2.25.616
1/** 2 * EnergyExperts Widget System 3 * @fileoverview Modern, Apple-style widget dashboard for project management 4 * @version 2.0.0 5 * @author EnergyExperts Team 6 * 7 * @description 8 * A modular widget system supporting: 9 * - Grid layout: 1/3, 2/3, 3/3 width à 1/4, 2/4, 3/4, 4/4 height 10 * - Preview mode with key metrics 11 * - Fullscreen expansion for details 12 * - Extensible widget classes 13 * 14 * @example 15 * // Initialize the widget system 16 * const manager = initWidgetSystem('my-container'); 17 * 18 * // Create a widget 19 * manager.create({ 20 * id: 'my-widget', 21 * type: 'metric', 22 * title: 'Revenue', 23 * data: { value: '1.2M', unit: 'â¬' } 24 * }); 25 */ 26 27 28'use strict'; 29 30// ============================================================================= 31// CONSTANTS & CONFIGURATION 32// ============================================================================= 33 34/** 35 * Widget size configurations 36 * @constant {Object} 37 */ 38const WIDGET_SIZES = { 39 WIDTH: { 40 SMALL: '1/3', 41 MEDIUM: '2/3', 42 LARGE: '3/3' 43 }, 44 HEIGHT: { 45 QUARTER: '1/4', 46 HALF: '2/4', 47 THREE_QUARTER: '3/4', 48 FULL: '4/4' 49 } 50}; 51 52/** 53 * CSS class mappings for widget sizes 54 * @constant {Object} 55 */ 56const SIZE_CLASSES = { 57 width: { 58 '1/3': 'ee-w-1', 59 '2/3': 'ee-w-2', 60 '3/3': 'ee-w-3' 61 }, 62 height: { 63 '1/4': 'ee-h-1', 64 '2/4': 'ee-h-2', 65 '3/4': 'ee-h-3', 66 '4/4': 'ee-h-4' 67 } 68}; 69 70/** 71 * SVG Icons used in widgets 72 * @constant {Object} 73 */ 74const WIDGET_ICONS = { 75 expand: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 76 <path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/> 77 </svg>`, 78 close: `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 79 <path d="M18 6L6 18M6 6l12 12"/> 80 </svg>` 81}; 82 83// ============================================================================= 84// UTILITY FUNCTIONS 85// ============================================================================= 86 87/** 88 * Safe getElementById with EEDom fallback 89 * @param {string} id - Element ID (without #) 90 * @param {boolean} [useCache=true] - Whether to use cache 91 * @returns {Element|null} 92 */ 93function safeGetById(id, useCache = true) { 94 if (typeof EEDom !== 'undefined' && EEDom.getById) { 95 return EEDom.getById(id, useCache); 96 } 97 return document.getElementById(id); 98} 99 100/** 101 * Safe querySelector with EEDom fallback 102 * @param {string} selector - CSS selector 103 * @returns {Element|null} 104 */ 105function safeQuery(selector) { 106 if (typeof EEDom !== 'undefined' && EEDom.query) { 107 return EEDom.query(selector); 108 } 109 return document.querySelector(selector); 110} 111 112/** 113 * Format currency values 114 * @param {number} value - The value to format 115 * @param {string} [locale='de-DE'] - Locale for formatting 116 * @param {string} [currency='EUR'] - Currency code 117 * @returns {string} Formatted currency string 118 */ 119function formatCurrency(value, locale = 'de-DE', currency = 'EUR') { 120 if (typeof value !== 'number' || isNaN(value)) return 'â'; 121 return new Intl.NumberFormat(locale, { 122 style: 'currency', 123 currency: currency, 124 minimumFractionDigits: 0, 125 maximumFractionDigits: 0 126 }).format(value); 127} 128 129/** 130 * Create DOM element with attributes 131 * @param {string} tag - HTML tag name 132 * @param {Object} [attrs={}] - Element attributes 133 * @param {string|HTMLElement} [content=''] - Inner content 134 * @returns {HTMLElement} 135 */ 136function createElement(tag, attrs = {}, content = '') { 137 // Use EEDom.create if available, otherwise fallback to native 138 if (typeof EEDom !== 'undefined' && EEDom.create) { 139 const el = EEDom.create(tag, attrs, typeof content === 'string' ? content : ''); 140 if (content instanceof HTMLElement) { 141 el.appendChild(content); 142 } 143 return el; 144 } 145 146 const el = document.createElement(tag); 147 Object.entries(attrs).forEach(([key, value]) => { 148 if (key === 'className') { 149 el.className = value; 150 } else if (key === 'dataset') { 151 Object.entries(value).forEach(([dataKey, dataValue]) => { 152 el.dataset[dataKey] = dataValue; 153 }); 154 } else { 155 el.setAttribute(key, value); 156 } 157 }); 158 if (typeof content === 'string') { 159 el.innerHTML = content; 160 } else if (content instanceof HTMLElement) { 161 el.appendChild(content); 162 } 163 return el; 164} 165 166// ============================================================================= 167// WIDGET MANAGER CLASS 168// ============================================================================= 169 170/** 171 * Central controller for all widgets 172 * @class WidgetManager 173 */ 174class WidgetManager { 175 /** 176 * Create a WidgetManager instance 177 * @param {string} containerId - ID of the container element 178 */ 179 constructor(containerId) { 180 /** @type {string} */ 181 this.containerId = containerId; 182 183 /** @type {HTMLElement|null} */ 184 this.container = null; 185 186 /** @type {Map<string, BaseWidget>} */ 187 this.widgets = new Map(); 188 189 /** @type {Map<string, typeof BaseWidget>} */ 190 this.widgetTypes = new Map(); 191 192 /** @type {HTMLElement|null} */ 193 this.fullscreenOverlay = null; 194 195 /** @type {BaseWidget|null} */ 196 this.activeFullscreenWidget = null; 197 198 /** @type {boolean} */ 199 this.isInitialized = false;
200 201 /** @type {boolean} */ 202 this.isSaving = false; 203 204 this._registerBuiltInTypes(); 205 206 // Register in global registry so cross-dashboard helpers (Phase 4 207 // constraint-badge refresh) can iterate over all active managers. 208 // Stale refs are OK â _updateConstraintBadge is a no-op when 209 // widget.element is detached. 210 if (!window.__eeWidgetManagers) window.__eeWidgetManagers = new Set(); 211 window.__eeWidgetManagers.add(this); 212 } 213 214 // ------------------------------------------------------------------------- 215 // Public Methods 216 // ------------------------------------------------------------------------- 217 218 /** 219 * Initialize the widget system 220 * @returns {WidgetManager} Returns this for chaining 221 */ 222 init() { 223 if (this.isInitialized) return this; 224 225 // Use useCache: false because the container may have just been dynamically created 226 // Fallback to native getElementById if EEDom is not available 227 this.container = (typeof EEDom !== 'undefined' && safeGetById) 228 ? safeGetById(this.containerId, false) 229 : document.getElementById(this.containerId); 230 if (!this.container) { 231 console.error(`[WidgetManager] Container #${this.containerId} not found`); 232 return this; 233 } 234 235 this.container.classList.add('ee-widget-grid'); 236 this._createFullscreenOverlay(); 237 this._setupKeyboardHandlers(); 238 239 this.isInitialized = true; 240 return this; 241 } 242 243 /** 244 * Register a custom widget type 245 * @param {string} typeName - Name of the widget type 246 * @param {typeof BaseWidget} WidgetClass - Widget class constructor 247 * @returns {WidgetManager} Returns this for chaining 248 */ 249 registerType(typeName, WidgetClass) { 250 this.widgetTypes.set(typeName, WidgetClass); 251 return this; 252 } 253 254 /** 255 * Create and add a widget 256 * @param {Object} config - Widget configuration 257 * @param {string} config.id - Unique widget ID 258 * @param {string} config.type - Widget type name 259 * @param {string} [config.title] - Widget title 260 * @param {string} [config.icon] - Widget icon HTML 261 * @param {string} [config.width='1/3'] - Widget width 262 * @param {string} [config.height='1/4'] - Widget height 263 * @param {boolean} [config.expandable=true] - Can be expanded 264 * @param {Object} [config.data={}] - Widget data 265 * @param {number} [config.order=0] - Display order 266 * @returns {BaseWidget} The created widget 267 */ 268 create(config) { 269 const { 270 id, 271 type, 272 title, 273 icon, 274 width = WIDGET_SIZES.WIDTH.SMALL, 275 height = WIDGET_SIZES.HEIGHT.QUARTER, 276 expandable = true, 277 data = {}, 278 order = 0 279 } = config; 280 281 // B2B-Feature-Gating: Tasks, Notes und Documents Widgets ausblenden 282 const _b2bHiddenTypes = ['tasks-preview', 'tasks-button', 'notes-preview', 'notes-button', 'category-documents']; 283 if (typeof window !== 'undefined' && window.isB2BOnly && _b2bHiddenTypes.includes(type)) { 284 return null; 285 } 286 287 const WidgetClass = this.widgetTypes.get(type) || BaseWidget; 288 289 const widget = new WidgetClass({ 290 id, 291 type, 292 title, 293 icon, 294 width, 295 height, 296 expandable, 297 data, 298 order, 299 manager: this 300 }); 301 302 this.widgets.set(id, widget); 303 widget.render(this.container); 304 305 return widget; 306 } 307 308 /** 309 * Get a widget by ID 310 * @param {string} widgetId - Widget ID 311 * @returns {BaseWidget|undefined} 312 */ 313 get(widgetId) { 314 return this.widgets.get(widgetId); 315 } 316 317 /** 318 * Update a widget's data 319 * @param {string} widgetId - Widget ID 320 * @param {Object} data - New data to merge 321 * @returns {WidgetManager} Returns this for chaining 322 */ 323 update(widgetId, data) { 324 const widget = this.widgets.get(widgetId); 325 if (widget) { 326 widget.updateData(data); 327 } 328 return this; 329 } 330 331 /** 332 * Remove a widget 333 * @param {string} widgetId - Widget ID 334 * @returns {WidgetManager} Returns this for chaining 335 */ 336 remove(widgetId) { 337 const widget = this.widgets.get(widgetId); 338 if (widget) { 339 widget.destroy(); 340 this.widgets.delete(widgetId); 341 } 342 return this; 343 } 344 345 /** 346 * Expand widget to fullscreen 347 * @param {string} widgetId - Widget ID 348 */ 349 expandWidget(widgetId) { 350 const widget = this.widgets.get(widgetId); 351 if (!widget || !widget.expandable) return; 352 353 this.activeFullscreenWidget = widget; 354 355 // Re-bind close button to current manager when expanding 356 this._bindOverlayEventListeners(this.fullscreenOverlay); 357 358 const content = this.fullscreenOverlay.querySelector('.ee-fullscreen-content'); 359 const title = this.fullscreenOverlay.querySelector('.ee-fullscreen-title'); 360 const iconEl = this.fullscreenOverlay.querySelector('.ee-fullscreen-icon'); 361 362 title.textContent = widget.title; 363 364 if (widget.icon) { 365 iconEl.innerHTML = widget.icon; 366 iconEl.style.display = 'flex'; 367 } else { 368 iconEl.style.display = 'none'; 369 } 370 371 // Phase 4: constraint-badge in fullscreen-header (same behaviour as preview) 372 var fsBadgeHost = this.fullscreenOverlay.querySelector('[data-fullscreen-constraint-badge]'); 373 if (!fsBadgeHost) { 374 fsBadgeHost = document.createElement('div'); 375 fsBadgeHost.setAttribute('data-fullscreen-constraint-badge', ''); 376 fsBadgeHost.className = 'ee-widget-constraint-badge-host ee-widget-constraint-badge-host--fullscreen'; 377 title.insertAdjacentElement('afterend', fsBadgeHost); 378 } 379 if (widget.showConstraintBadge && 380 typeof window.EE_getAppliedAlphaForProject === 'function' && 381 typeof window.EE_renderConstraintBadge === 'function') { 382 var _a = window.EE_getAppliedAlphaForProject(); 383 if (typeof _a === 'number' && _a > 0) { 384 window.EE_renderConstraintBadge(fsBadgeHost, 'applied', _a); 385 } else { 386 fsBadgeHost.innerHTML = ''; 387 } 388 } else { 389 fsBadgeHost.innerHTML = ''; 390 } 391 392 // Show loading state 393 content.innerHTML = ''; 394 content.classList.add('loading'); 395 content.classList.remove('loaded'); 396 397 this.fullscreenOverlay.classList.add('active'); 398 document.body.classList.add('ee-widget-fullscreen-open'); 399 400 // Render content after short delay to show loading animation 401 setTimeout(() => { 402 const fullscreenContent = widget.renderFullscreen(); 403 404 if (typeof fullscreenContent === 'string') { 405 content.innerHTML = fullscreenContent; 406 } else if (fullscreenContent instanceof HTMLElement) { 407 content.appendChild(fullscreenContent); 408 } 409 410 widget.initFullscreen(content); 411 412 // Mark as loaded 413 content.classList.remove('loading'); 414 content.classList.add('loaded'); 415 416 window.dispatchEvent(new Event('resize')); 417 widget.onFullscreenOpen(); 418 }, 100); 419 } 420 421 /** 422 * Close fullscreen view 423 */ 424 closeFullscreen() { 425 if (!this.activeFullscreenWidget) return; 426 if (this.isSaving) return; 427 428 this.fullscreenOverlay.classList.remove('active'); 429 document.body.classList.remove('ee-widget-fullscreen-open'); 430 431 this.activeFullscreenWidget.onFullscreenClose(); 432 this.activeFullscreenWidget = null; 433 } 434 435 /** 436 * Set global saving state - blocks closing while saving 437 * @param {boolean} saving - Whether currently saving 438 */ 439 setSaving(saving) { 440 this.isSaving = saving; 441 442 if (!this.fullscreenOverlay) return; 443 444 const closeBtn = this.fullscreenOverlay.querySelector('.ee-fullscreen-close'); 445 446 if (saving) { 447 this._showGlobalSavingOverlay(true); 448 if (closeBtn) { 449 closeBtn.disabled = true; 450 closeBtn.style.opacity = '0.3'; 451 closeBtn.style.pointerEvents = 'none'; 452 } 453 } else { 454 this._showGlobalSavingOverlay(false); 455 if (closeBtn) { 456 closeBtn.disabled = false;
457 closeBtn.style.opacity = ''; 458 closeBtn.style.pointerEvents = ''; 459 } 460 } 461 } 462 463 /** 464 * Show all widgets 465 * @returns {WidgetManager} 466 */ 467 showAll() { 468 this.widgets.forEach(widget => widget.show()); 469 return this; 470 } 471 472 /** 473 * Hide all widgets 474 * @returns {WidgetManager} 475 */ 476 hideAll() { 477 this.widgets.forEach(widget => widget.hide()); 478 return this; 479 } 480 481 /** 482 * Filter widgets by type 483 * @param {string} type - Widget type to show 484 * @returns {WidgetManager} 485 */ 486 filterByType(type) { 487 this.widgets.forEach(widget => { 488 widget.type === type ? widget.show() : widget.hide(); 489 }); 490 return this; 491 } 492 493 /** 494 * Clear all widgets 495 * @returns {WidgetManager} 496 */ 497 clear() { 498 this.widgets.forEach(widget => widget.destroy()); 499 this.widgets.clear(); 500 return this; 501 } 502 503 // ------------------------------------------------------------------------- 504 // Private Methods 505 // ------------------------------------------------------------------------- 506 507 /** 508 * Register built-in widget types 509 * @private 510 */ 511 _registerBuiltInTypes() { 512 this.registerType('base', BaseWidget); 513 this.registerType('metric', MetricWidget); 514 this.registerType('chart', ChartWidget); 515 this.registerType('grid-connection', GridConnectionWidget); 516 this.registerType('finance', FinanceWidget); 517 this.registerType('permit', PermitWidget); 518 this.registerType('construction', ConstructionWidget); 519 this.registerType('operation', OperationWidget); 520 this.registerType('dashboard', DashboardWidget); 521 this.registerType('notes', NotesWidget); 522 } 523 524 /** 525 * Create fullscreen overlay element 526 * @private 527 */ 528 _createFullscreenOverlay() { 529 const existingOverlay = safeGetById('ee-widget-fullscreen'); 530 if (existingOverlay) { 531 this.fullscreenOverlay = existingOverlay; 532 // Re-bind event listeners to current manager instance 533 this._bindOverlayEventListeners(existingOverlay); 534 return; 535 } 536 537 const overlay = createElement('div', { 538 id: 'ee-widget-fullscreen', 539 className: 'ee-fullscreen-overlay' 540 }, ` 541 <div class="ee-fullscreen-container"> 542 <div class="ee-fullscreen-header"> 543 <div class="ee-fullscreen-icon"></div> 544 <h2 class="ee-fullscreen-title"></h2> 545 <button class="ee-fullscreen-close" aria-label="SchlieÃen"> 546 ${WIDGET_ICONS.close} 547 </button> 548 </div> 549 <div class="ee-fullscreen-content"></div> 550 </div> 551 `); 552 553 document.body.appendChild(overlay); 554 this.fullscreenOverlay = overlay; 555 556 // Bind event listeners 557 this._bindOverlayEventListeners(overlay); 558 } 559 560 /** 561 * Bind event listeners to overlay - can be re-bound to update manager reference 562 * @private 563 * @param {HTMLElement} overlay - The overlay element 564 */ 565 _bindOverlayEventListeners(overlay) { 566 const closeBtn = overlay.querySelector('.ee-fullscreen-close'); 567 568 // Remove old listeners by cloning the button 569 const newCloseBtn = closeBtn.cloneNode(true); 570 closeBtn.parentNode.replaceChild(newCloseBtn, closeBtn); 571 572 // Add new listener bound to current manager instance 573 newCloseBtn.addEventListener('click', (e) => { 574 e.preventDefault(); 575 e.stopPropagation(); 576 this.closeFullscreen(); 577 }); 578 579 // Also handle overlay background click 580 // Use a data attribute to track if already bound 581 if (!overlay.dataset.overlayBound) { 582 overlay.addEventListener('click', (e) => { 583 if (e.target === overlay) { 584 // Find the active manager and close 585 if (this.activeFullscreenWidget) { 586 this.closeFullscreen(); 587 } 588 } 589 }); 590 overlay.dataset.overlayBound = 'true'; 591 } 592 } 593 594 /** 595 * Setup keyboard event handlers 596 * @private 597 */ 598 _setupKeyboardHandlers() { 599 document.addEventListener('keydown', (e) => { 600 if (e.key === 'Escape' && this.activeFullscreenWidget) { 601 this.closeFullscreen(); 602 } 603 }); 604 } 605 606 /** 607 * Show/hide global saving overlay 608 * @private 609 * @param {boolean} show - Whether to show the overlay 610 */ 611 _showGlobalSavingOverlay(show) {
612 let overlay = this.fullscreenOverlay?.querySelector('.ee-global-saving-overlay'); 613 614 if (show) { 615 if (!overlay) { 616 overlay = createElement('div', { 617 className: 'ee-global-saving-overlay' 618 }, ` 619 <div class="ee-saving-content"> 620 <span class="ee-saving-spinner"></span> 621 <span>Speichern...</span> 622 </div> 623 `); 624 const content = this.fullscreenOverlay?.querySelector('.ee-fullscreen-content'); 625 if (content) { 626 content.appendChild(overlay); 627 } 628 } 629 overlay.style.display = 'flex'; 630 } else if (overlay) { 631 overlay.style.display = 'none'; 632 } 633 } 634} 635 636// ============================================================================= 637// BASE WIDGET CLASS 638// ============================================================================= 639 640/** 641 * Base class for all widgets - extend this to create custom widgets 642 * @class BaseWidget 643 */ 644class BaseWidget { 645 /** 646 * Create a BaseWidget instance 647 * @param {Object} config - Widget configuration 648 */ 649 constructor(config) { 650 /** @type {string} */ 651 this.id = config.id; 652 653 /** @type {string} */ 654 this.type = config.type; 655 656 /** @type {string} */ 657 this.title = config.title || ''; 658 659 /** @type {string} */ 660 this.icon = config.icon || ''; 661 662 /** @type {string} */ 663 this.width = config.width; 664 665 /** @type {string} */ 666 this.height = config.height; 667 668 /** @type {boolean} */ 669 this.expandable = config.expandable; 670 671 /** @type {Object} */ 672 this.data = config.data || {}; 673 674 /** @type {number} */ 675 this.order = config.order || 0; 676 677 /** @type {WidgetManager} */ 678 this.manager = config.manager; 679 680 /** @type {HTMLElement|null} */ 681 this.element = null; 682 683 /** @type {boolean} */ 684 this.isVisible = true; 685 686 /** @type {boolean} */ 687 this.isLoading = false; 688 } 689 690 // ------------------------------------------------------------------------- 691 // Public Methods 692 // ------------------------------------------------------------------------- 693 694 /** 695 * Render the widget to a container 696 * @param {HTMLElement} container - Container element 697 * @returns {BaseWidget} 698 */ 699 render(container) { 700 this.element = createElement('div', { 701 id: `widget-${this.id}`, 702 className: this._getWidgetClasses() 703 }); 704 this.element.style.order = this.order; 705 this.element.setAttribute('data-widget-id', this.id); 706 707 this.element.innerHTML = ` 708 <div class="ee-widget-inner"> 709 <div class="ee-widget-header"> 710 ${this.icon ? `<div class="ee-widget-icon">${this.icon}</div>` : ''} 711 <h3 class="ee-widget-title">${this.title}</h3> 712 <div class="ee-widget-constraint-badge-host" data-widget-constraint-badge></div> 713 ${this.expandable ? ` 714 <button class="ee-widget-expand" aria-label="Erweitern"> 715 ${WIDGET_ICONS.expand} 716 </button> 717 ` : ''} 718 </div> 719 <div class="ee-widget-body"> 720 <div class="ee-widget-preview"> 721 ${this.renderPreview()} 722 </div> 723 </div> 724 <div class="ee-widget-loader"> 725 <div class="ee-widget-spinner"></div> 726 </div> 727 </div> 728 `; 729 730 if (this.expandable) { 731 const expandBtn = this.element.querySelector('.ee-widget-expand'); 732 expandBtn?.addEventListener('click', (e) => { 733 e.stopPropagation(); 734 e.preventDefault(); 735 this.manager.expandWidget(this.id); 736 }); 737 } 738 739 container.appendChild(this.element); 740 this.initPreview(); 741 this._updateConstraintBadge(); 742 743 return this; 744 } 745 746 /**
747 * Render / refresh the constraint-badge in this widget's header (preview card). 748 * Visible only for widgets that opt-in via `this.showConstraintBadge = true` 749 * AND where a tracker tilt-constraint is currently applied to the project. 750 * 751 * Uses the global helper window.EE_getAppliedAlphaForProject() to resolve 752 * the current applied alpha_max across all tracker-systems in the variation. 753 * Rendered via window.EE_renderConstraintBadge (Phase 3 global helper). 754 */ 755 _updateConstraintBadge() { 756 if (!this.element) return; 757 if (!this.showConstraintBadge) return; 758 var host = this.element.querySelector('[data-widget-constraint-badge]'); 759 if (!host) return; 760 if (typeof window.EE_getAppliedAlphaForProject !== 'function') { host.innerHTML = ''; return; } 761 if (typeof window.EE_renderConstraintBadge !== 'function') { host.innerHTML = ''; return; } 762 var alpha = window.EE_getAppliedAlphaForProject(); 763 if (typeof alpha === 'number' && alpha > 0) { 764 window.EE_renderConstraintBadge(host, 'applied', alpha); 765 } else { 766 host.innerHTML = ''; 767 } 768 } 769 770 /** 771 * Render preview content - override in subclasses 772 * @returns {string} HTML content 773 */ 774 renderPreview() { 775 return `<div class="ee-widget-placeholder">Widget Content</div>`; 776 } 777 778 /** 779 * Initialize preview after render - override in subclasses 780 */ 781 initPreview() { 782 // Override in subclasses 783 } 784 785 /** 786 * Render fullscreen content - override in subclasses 787 * @returns {string|HTMLElement} HTML content or element 788 */ 789 renderFullscreen() { 790 return `<div class="ee-fullscreen-placeholder">Fullscreen Content</div>`; 791 } 792 793 /** 794 * Initialize fullscreen after render - override in subclasses 795 * @param {HTMLElement} container - Fullscreen container 796 */ 797 initFullscreen(container) { 798 // Override in subclasses 799 } 800 801 /** 802 * Called when fullscreen opens - override in subclasses 803 */ 804 onFullscreenOpen() { 805 // Override in subclasses 806 } 807 808 /** 809 * Called when fullscreen closes - override in subclasses 810 */ 811 onFullscreenClose() { 812 // Override in subclasses 813 } 814 815 /** 816 * Update widget data and refresh 817 * @param {Object} newData - Data to merge 818 */ 819 updateData(newData) { 820 this.data = { ...this.data, ...newData }; 821 this.refresh(); 822 } 823 824 /** 825 * Refresh the widget preview 826 * Optimized to prevent flickering by checking if content actually changed 827 */ 828 refresh() { 829 if (!this.element) return; 830 831 const preview = this.element.querySelector('.ee-widget-preview'); 832 if (!preview) return; 833 834 const newContent = this.renderPreview(); 835 836 // Only update if content actually changed to prevent flickering 837 if (preview.innerHTML.trim() === newContent.trim()) { 838 return; 839 } 840 841 // Use opacity transition to smooth the update 842 preview.style.opacity = '0'; 843 844 requestAnimationFrame(() => { 845 preview.innerHTML = newContent; 846 this.initPreview(); 847 848 requestAnimationFrame(() => { 849 preview.style.opacity = '1'; 850 }); 851 }); 852 } 853 854 /** 855 * Show loading state 856 */ 857 showLoading() { 858 this.isLoading = true; 859 this.element?.classList.add('loading'); 860 } 861 862 /** 863 * Hide loading state 864 */ 865 hideLoading() { 866 this.isLoading = false; 867 this.element?.classList.remove('loading'); 868 } 869 870 /** 871 * Show widget 872 */ 873 show() { 874 this.isVisible = true; 875 this.element?.classList.remove('hidden'); 876 } 877 878 /** 879 * Hide widget 880 */ 881 hide() { 882 this.isVisible = false; 883 this.element?.classList.add('hidden'); 884 } 885 886 /** 887 * Destroy widget and remove from DOM 888 */ 889 destroy() { 890 this.element?.remove(); 891 this.element = null; 892 } 893 894 // ------------------------------------------------------------------------- 895 // Protected Methods (for subclasses) 896 // ------------------------------------------------------------------------- 897 898 /** 899 * Setup tab switching in fullscreen 900 * @protected 901 * @param {HTMLElement} container - Container with tabs 902 * @param {string} [tabSelector='.ee-tab'] - Tab button selector 903 * @param {string} [contentSelector='.ee-tab-content'] - Content selector 904 */ 905 _setupTabs(container, tabSelector = '.ee-tab', contentSelector = '.ee-tab-content') { 906 const tabs = container.querySelectorAll(tabSelector); 907 const contents = container.querySelectorAll(contentSelector); 908
909 tabs.forEach(tab => { 910 tab.addEventListener('click', () => { 911 tabs.forEach(t => t.classList.remove('active')); 912 contents.forEach(c => c.classList.remove('active')); 913 914 tab.classList.add('active'); 915 const tabId = tab.dataset.tab; 916 const targetContent = container.querySelector(`[id$="-tab-${tabId}"]`); 917 targetContent?.classList.add('active'); 918 }); 919 }); 920 } 921 922 // ------------------------------------------------------------------------- 923 // Private Methods 924 // ------------------------------------------------------------------------- 925 926 /** 927 * Get CSS classes for widget 928 * @private 929 * @returns {string} 930 */ 931 _getWidgetClasses() { 932 const classes = ['ee-widget']; 933 934 const widthClass = SIZE_CLASSES.width[this.width]; 935 const heightClass = SIZE_CLASSES.height[this.height]; 936 937 if (widthClass) classes.push(widthClass); 938 if (heightClass) classes.push(heightClass); 939 if (this.expandable) classes.push('expandable'); 940 941 return classes.join(' '); 942 } 943} 944 945// ============================================================================= 946// SPECIALIZED WIDGET CLASSES 947// ============================================================================= 948 949/** 950 * Widget for displaying key metrics with trends 951 * @class MetricWidget 952 * @extends BaseWidget 953 */ 954class MetricWidget extends BaseWidget { 955 renderPreview() { 956 const { value, unit, label, change, trend } = this.data; 957 const trendIcon = trend === 'up' ? 'â' : trend === 'down' ? 'â' : 'â'; 958 const trendClass = trend === 'up' ? 'positive' : trend === 'down' ? 'negative' : 'neutral'; 959 960 return ` 961 <div class="ee-metric-widget"> 962 <div class="ee-metric-label">${label || ''}</div> 963 <div class="ee-metric-value"> 964 ${value || 'â'} 965 ${unit ? `<span class="ee-metric-unit">${unit}</span>` : ''} 966 </div> 967 ${change ? ` 968 <div class="ee-metric-change ${trendClass}"> 969 ${trendIcon} ${change} 970 </div> 971 ` : ''} 972 </div> 973 `; 974 } 975} 976 977/** 978 * Widget for displaying charts and graphs 979 * @class ChartWidget 980 * @extends BaseWidget 981 */ 982class ChartWidget extends BaseWidget { 983 renderPreview() { 984 return ` 985 <div class="ee-chart-preview"> 986 <div class="ee-chart-placeholder" id="chart-preview-${this.id}"> 987 ${this.data.previewHtml || '<div class="ee-chart-loading">Chart lädt...</div>'} 988 </div> 989 </div> 990 `; 991 } 992 993 renderFullscreen() { 994 return ` 995 <div class="ee-chart-fullscreen"> 996 <div class="ee-chart-container" id="chart-full-${this.id}"></div> 997 <div class="ee-chart-controls"> 998 ${this.data.showControls ? this._renderControls() : ''} 999 </div> 1000 </div> 1001 `; 1002 } 1003 1004 _renderControls() { 1005 return ` 1006 <div class="ee-chart-timerange"> 1007 <button class="ee-chart-btn active" data-range="1M">1M</button> 1008 <button class="ee-chart-btn" data-range="3M">3M</button> 1009 <button class="ee-chart-btn" data-range="1Y">1Y</button> 1010 <button class="ee-chart-btn" data-range="ALL">Alle</button> 1011 </div> 1012 `; 1013 } 1014} 1015 1016/** 1017 * Widget for network connection management 1018 * @class GridConnectionWidget 1019 * @extends BaseWidget 1020 */ 1021class GridConnectionWidget extends BaseWidget { 1022 renderPreview() { 1023 const { distance, operator, voltage, status } = this.data; 1024 const statusClass = status === 'connected' ? 'success' : status === 'pending' ? 'warning' : 'neutral'; 1025 const statusText = status === 'connected' ? 'Verbunden' : status === 'pending' ? 'Ausstehend' : 'Nicht verbunden'; 1026 1027 return ` 1028 <div class="ee-grid-widget"> 1029 <div class="ee-grid-status ${statusClass}">
1030 <span class="ee-status-dot"></span> 1031 ${statusText} 1032 </div> 1033 <div class="ee-grid-metrics"> 1034 <div class="ee-grid-metric"> 1035 <span class="ee-grid-metric-value">${distance || 'â'}</span> 1036 <span class="ee-grid-metric-label">Entfernung</span> 1037 </div> 1038 <div class="ee-grid-metric"> 1039 <span class="ee-grid-metric-value">${voltage || 'â'}</span> 1040 <span class="ee-grid-metric-label">Spannung</span> 1041 </div> 1042 </div> 1043 ${operator ? `<div class="ee-grid-operator">Netzbetreiber: ${operator}</div>` : ''} 1044 </div> 1045 `; 1046 } 1047 1048 renderFullscreen() { 1049 return ` 1050 <div class="ee-grid-fullscreen"> 1051 <div class="ee-grid-tabs"> 1052 <button class="ee-tab active" data-tab="overview">Ãbersicht</button> 1053 <button class="ee-tab" data-tab="map">Karte</button> 1054 <button class="ee-tab" data-tab="documents">Dokumente</button> 1055 <button class="ee-tab" data-tab="e8form">E8 Formular</button> 1056 </div> 1057 <div class="ee-grid-content"> 1058 <div class="ee-tab-content active" id="grid-tab-overview"> 1059 <div id="grid-connection-overview-widget"></div> 1060 </div> 1061 <div class="ee-tab-content" id="grid-tab-map"> 1062 <div id="grid-map-widget"></div> 1063 </div> 1064 <div class="ee-tab-content" id="grid-tab-documents"> 1065 <div id="grid-documents-widget"></div> 1066 </div> 1067 <div class="ee-tab-content" id="grid-tab-e8form"> 1068 <div id="grid-e8form-widget"></div> 1069 </div> 1070 </div> 1071 </div> 1072 `; 1073 } 1074 1075 initFullscreen(container) { 1076 this._setupTabs(container); 1077 this._loadGridConnectionContent(container); 1078 } 1079 1080 _loadGridConnectionContent(container) { 1081 // Copy overview content 1082 const overviewSource = safeGetById('grid-connection-overview-container'); 1083 const overviewTarget = container.querySelector('#grid-connection-overview-widget'); 1084 if (overviewSource && overviewTarget) { 1085 overviewTarget.innerHTML = overviewSource.innerHTML; 1086 } 1087 1088 // Copy E8 form content 1089 const e8Source = safeGetById('grid-connection-pdf-container'); 1090 const e8Target = container.querySelector('#grid-e8form-widget'); 1091 if (e8Source && e8Target) { 1092 e8Target.innerHTML = e8Source.innerHTML; 1093 } 1094 1095 // Trigger existing fill functions if available 1096 if (typeof fillGridConection === 'function') { 1097 fillGridConection(); 1098 } 1099 } 1100 1101 onFullscreenOpen() { 1102 if (typeof mapEntry !== 'undefined' && mapEntry?.resize) { 1103 setTimeout(() => mapEntry.resize(), 100); 1104 } 1105 } 1106} 1107 1108/** 1109 * Widget for financial overview and analytics 1110 * @class FinanceWidget 1111 * @extends BaseWidget 1112 */ 1113class FinanceWidget extends BaseWidget { 1114 renderPreview() { 1115 const fd = typeof financeData !== 'undefined' ? financeData : this.data; 1116 1117 const irr = fd?.economics?.irrE ? (fd.economics.irrE * 100).toFixed(1) : 'â'; 1118 const roi = fd?.economics?.roiE ? (fd.economics.roiE * 100).toFixed(1) : 'â'; 1119 const npv = fd?.economics?.npvE ? formatCurrency(fd.economics.npvE) : 'â'; 1120 const rawAmort = fd?.economics?.amortization; 1121 const payback = (!rawAmort || rawAmort == -1 || isNaN(rawAmort)) ? 'â' : rawAmort; 1122 1123 return ` 1124 <div class="ee-finance-widget"> 1125 <div class="ee-finance-grid"> 1126 <div class="ee-finance-metric"> 1127 <span class="ee-finance-value">${irr}<small>%</small></span> 1128 <span class="ee-finance-label">IRR</span> 1129 </div> 1130 <div class="ee-finance-metric">
1131 <span class="ee-finance-value">${roi}<small>%</small></span> 1132 <span class="ee-finance-label">ROI</span> 1133 </div> 1134 <div class="ee-finance-metric"> 1135 <span class="ee-finance-value">${payback}<small>J</small></span> 1136 <span class="ee-finance-label">Payback</span> 1137 </div> 1138 </div> 1139 <div class="ee-finance-npv"> 1140 <span class="ee-finance-npv-label">NPV (20J)</span> 1141 <span class="ee-finance-npv-value">${npv}</span> 1142 </div> 1143 </div> 1144 `; 1145 } 1146 1147 renderFullscreen() { 1148 return ` 1149 <div class="ee-finance-fullscreen"> 1150 <div class="ee-finance-tabs"> 1151 <button class="ee-tab active" data-tab="overview">Ãbersicht</button> 1152 <button class="ee-tab" data-tab="cashflow">Cashflow</button> 1153 <button class="ee-tab" data-tab="costs">Kosten</button> 1154 <button class="ee-tab" data-tab="revenue">Einnahmen</button> 1155 <button class="ee-tab" data-tab="sensitivity">Sensitivität</button> 1156 </div> 1157 <div class="ee-finance-content"> 1158 <div class="ee-tab-content active" id="finance-tab-overview"> 1159 <div id="finance-overview-widget"></div> 1160 </div> 1161 <div class="ee-tab-content" id="finance-tab-cashflow"> 1162 <div id="finance-cashflow-widget"></div> 1163 </div> 1164 <div class="ee-tab-content" id="finance-tab-costs"> 1165 <div id="finance-costs-widget"></div> 1166 </div> 1167 <div class="ee-tab-content" id="finance-tab-revenue"> 1168 <div id="finance-revenue-widget"></div> 1169 </div> 1170 <div class="ee-tab-content" id="finance-tab-sensitivity"> 1171 <div id="finance-sensitivity-widget"></div> 1172 </div> 1173 </div> 1174 </div> 1175 `; 1176 } 1177 1178 initFullscreen(container) { 1179 this._setupTabs(container); 1180 this._loadFinanceContent(container); 1181 } 1182 1183 _loadFinanceContent(container) { 1184 // Copy existing finance dashboard content 1185 const dashboardSource = safeGetById('financeDashboard'); 1186 const overviewTarget = container.querySelector('#finance-overview-widget'); 1187 if (dashboardSource && overviewTarget) { 1188 overviewTarget.innerHTML = dashboardSource.innerHTML; 1189 } 1190 1191 // Copy finance table 1192 const tableSource = safeGetById('financeTableContainer'); 1193 const cashflowTarget = container.querySelector('#finance-cashflow-widget'); 1194 if (tableSource && cashflowTarget) { 1195 cashflowTarget.innerHTML = tableSource.innerHTML; 1196 } 1197 1198 // Copy finance graphs 1199 const graphsSource = safeGetById('financeGraphs'); 1200 const revenueTarget = container.querySelector('#finance-revenue-widget'); 1201 if (graphsSource && revenueTarget) { 1202 revenueTarget.innerHTML = graphsSource.innerHTML; 1203 } 1204 1205 // Trigger existing fill functions 1206 if (typeof fillFinanceOverview === 'function') fillFinanceOverview(); 1207 if (typeof fillFinanceDashboard === 'function') fillFinanceDashboard(); 1208 } 1209 1210 onFullscreenOpen() { 1211 if (typeof financeChart !== 'undefined' && financeChart?.resize) { 1212 setTimeout(() => financeChart.resize(), 100); 1213 } 1214 } 1215} 1216 1217/** 1218 * Widget for permit/approval process management 1219 * @class PermitWidget 1220 * @extends BaseWidget 1221 */ 1222class PermitWidget extends BaseWidget { 1223 renderPreview() { 1224 const { steps, completedSteps, currentStep } = this.data; 1225 const total = steps?.length || 5; 1226 const completed = completedSteps || 0; 1227 const progress = Math.round((completed / total) * 100); 1228 1229 return ` 1230 <div class="ee-permit-widget"> 1231 <div class="ee-permit-progress"> 1232 <div class="ee-permit-progress-bar"> 1233 <div class="ee-permit-progress-fill" style="width: ${progress}%"></div> 1234 </div>
1235 <span class="ee-permit-progress-text">${completed}/${total} Schritte</span> 1236 </div> 1237 <div class="ee-permit-current"> 1238 ${currentStep ? ` 1239 <span class="ee-permit-current-label">Aktuell:</span> 1240 <span class="ee-permit-current-step">${currentStep}</span> 1241 ` : '<span class="ee-permit-complete">â Alle Schritte abgeschlossen</span>'} 1242 </div> 1243 </div> 1244 `; 1245 } 1246 1247 renderFullscreen() { 1248 return ` 1249 <div class="ee-permit-fullscreen"> 1250 <div class="ee-permit-header-section"> 1251 <div class="ee-permit-overview-cards" id="permit-overview-widget"></div> 1252 </div> 1253 <div class="ee-permit-main"> 1254 <div class="ee-permit-steps-container" id="permit-steps-widget"></div> 1255 <div class="ee-permit-details-container" id="permit-details-widget"></div> 1256 </div> 1257 </div> 1258 `; 1259 } 1260 1261 initFullscreen(container) { 1262 this._loadPermitContent(container); 1263 } 1264 1265 _loadPermitContent(container) { 1266 const copyContent = (sourceId, targetSelector) => { 1267 const source = safeGetById(sourceId); 1268 const target = container.querySelector(targetSelector); 1269 if (source && target) { 1270 target.innerHTML = source.innerHTML; 1271 } 1272 }; 1273 1274 copyContent('permitProcess-steps', '#permit-steps-widget'); 1275 copyContent('permitProcess-content', '#permit-details-widget'); 1276 copyContent('permitDashboard', '#permit-overview-widget'); 1277 } 1278} 1279 1280/** 1281 * Widget for construction phase management 1282 * @class ConstructionWidget 1283 * @extends BaseWidget 1284 */ 1285class ConstructionWidget extends BaseWidget { 1286 renderPreview() { 1287 const { phase, progress, startDate, endDate } = this.data; 1288 1289 return ` 1290 <div class="ee-construction-widget"> 1291 <div class="ee-construction-phase"> 1292 <span class="ee-construction-phase-label">Phase:</span> 1293 <span class="ee-construction-phase-value">${phase || 'Planung'}</span> 1294 </div> 1295 <div class="ee-construction-progress"> 1296 <div class="ee-construction-progress-bar"> 1297 <div class="ee-construction-progress-fill" style="width: ${progress || 0}%"></div> 1298 </div> 1299 <span class="ee-construction-progress-text">${progress || 0}%</span> 1300 </div> 1301 ${(startDate || endDate) ? ` 1302 <div class="ee-construction-dates"> 1303 ${startDate ? `<span>Start: ${startDate}</span>` : ''} 1304 ${endDate ? `<span>Ende: ${endDate}</span>` : ''} 1305 </div> 1306 ` : ''} 1307 </div> 1308 `; 1309 } 1310 1311 renderFullscreen() { 1312 return ` 1313 <div class="ee-construction-fullscreen"> 1314 <div class="ee-construction-tabs"> 1315 <button class="ee-tab active" data-tab="timeline">Timeline</button> 1316 <button class="ee-tab" data-tab="tasks">Aufgaben</button> 1317 <button class="ee-tab" data-tab="resources">Ressourcen</button> 1318 </div> 1319 <div class="ee-construction-content"> 1320 <div class="ee-tab-content active" id="construction-tab-timeline"> 1321 <div id="construction-timeline-widget"></div> 1322 </div> 1323 <div class="ee-tab-content" id="construction-tab-tasks"> 1324 <div id="construction-tasks-widget"></div> 1325 </div> 1326 <div class="ee-tab-content" id="construction-tab-resources"> 1327 <div id="construction-resources-widget"></div> 1328 </div> 1329 </div> 1330 </div> 1331 `; 1332 } 1333 1334 initFullscreen(container) { 1335 this._setupTabs(container); 1336 this._loadConstructionContent(container); 1337 } 1338 1339 _loadConstructionContent(container) { 1340 const copyContent = (sourceId, targetSelector) => { 1341 const source = safeGetById(sourceId);
1342 const target = container.querySelector(targetSelector); 1343 if (source && target) target.innerHTML = source.innerHTML; 1344 }; 1345 1346 copyContent('constructionProcess-steps', '#construction-timeline-widget'); 1347 copyContent('constructionDashboard', '#construction-tasks-widget'); 1348 } 1349} 1350 1351/** 1352 * Widget for operations and maintenance 1353 * @class OperationWidget 1354 * @extends BaseWidget 1355 */ 1356class OperationWidget extends BaseWidget { 1357 renderPreview() { 1358 const { status, uptime, alerts } = this.data; 1359 const statusClass = status === 'operational' ? 'success' : status === 'warning' ? 'warning' : 'error'; 1360 const statusText = status === 'operational' ? 'Betriebsbereit' : status === 'warning' ? 'Warnung' : 'Störung'; 1361 1362 return ` 1363 <div class="ee-operation-widget"> 1364 <div class="ee-operation-status ${statusClass}"> 1365 <span class="ee-status-indicator"></span> 1366 ${statusText} 1367 </div> 1368 <div class="ee-operation-stats"> 1369 ${uptime ? ` 1370 <div class="ee-operation-stat"> 1371 <span class="ee-operation-stat-value">${uptime}</span> 1372 <span class="ee-operation-stat-label">Uptime</span> 1373 </div> 1374 ` : ''} 1375 ${alerts !== undefined ? ` 1376 <div class="ee-operation-stat"> 1377 <span class="ee-operation-stat-value">${alerts}</span> 1378 <span class="ee-operation-stat-label">Alerts</span> 1379 </div> 1380 ` : ''} 1381 </div> 1382 </div> 1383 `; 1384 } 1385 1386 renderFullscreen() { 1387 return ` 1388 <div class="ee-operation-fullscreen"> 1389 <div class="ee-operation-tabs"> 1390 <button class="ee-tab active" data-tab="status">Status</button> 1391 <button class="ee-tab" data-tab="maintenance">Wartung</button> 1392 <button class="ee-tab" data-tab="alerts">Meldungen</button> 1393 </div> 1394 <div class="ee-operation-content"> 1395 <div class="ee-tab-content active" id="operation-tab-status"> 1396 <div id="operation-status-widget"></div> 1397 </div> 1398 <div class="ee-tab-content" id="operation-tab-maintenance"> 1399 <div id="operation-maintenance-widget"></div> 1400 </div> 1401 <div class="ee-tab-content" id="operation-tab-alerts"> 1402 <div id="operation-alerts-widget"></div> 1403 </div> 1404 </div> 1405 </div> 1406 `; 1407 } 1408 1409 initFullscreen(container) { 1410 this._setupTabs(container); 1411 this._loadOperationContent(container); 1412 } 1413 1414 _loadOperationContent(container) { 1415 const copyContent = (sourceId, targetSelector) => { 1416 const source = safeGetById(sourceId); 1417 const target = container.querySelector(targetSelector); 1418 if (source && target) target.innerHTML = source.innerHTML; 1419 }; 1420 1421 copyContent('operationProcess-steps', '#operation-status-widget'); 1422 copyContent('operationDashboard', '#operation-maintenance-widget'); 1423 } 1424} 1425 1426/** 1427 * Widget for main overview dashboard 1428 * @class DashboardWidget 1429 * @extends BaseWidget 1430 */ 1431class DashboardWidget extends BaseWidget { 1432 renderPreview() { 1433 const fd = typeof financeData !== 'undefined' ? financeData : {}; 1434 1435 const power = fd?.investment?.power ? (fd.investment.power / 1000).toFixed(2) : 'â'; 1436 const yieldValue = fd?.economics?.energyYield ? Math.round(fd.economics.energyYield / 1000) : 'â'; 1437 1438 return ` 1439 <div class="ee-dashboard-widget"> 1440 <div class="ee-dashboard-hero"> 1441 <div class="ee-dashboard-hero-value">${yieldValue}<small>MWh</small></div> 1442 <div class="ee-dashboard-hero-label">Jahresertrag</div> 1443 </div> 1444 <div class="ee-dashboard-stats"> 1445 <div class="ee-dashboard-stat">
1446 <span class="ee-dashboard-stat-value">${power}</span> 1447 <span class="ee-dashboard-stat-label">MWp</span> 1448 </div> 1449 </div> 1450 </div> 1451 `; 1452 } 1453 1454 renderFullscreen() { 1455 return ` 1456 <div class="ee-dashboard-fullscreen"> 1457 <div id="dashboard-full-content"></div> 1458 </div> 1459 `; 1460 } 1461 1462 initFullscreen(container) { 1463 const source = safeQuery('.dashboardView-content'); 1464 const target = container.querySelector('#dashboard-full-content'); 1465 if (source && target) { 1466 target.innerHTML = source.innerHTML; 1467 } 1468 1469 if (typeof updateSolarDashboard === 'function') { 1470 updateSolarDashboard(); 1471 } 1472 } 1473} 1474 1475/** 1476 * Widget for custom notes and annotations 1477 * @class NotesWidget 1478 * @extends BaseWidget 1479 */ 1480class NotesWidget extends BaseWidget { 1481 renderPreview() { 1482 const { notes, lastUpdated } = this.data; 1483 const preview = notes 1484 ? notes.substring(0, 100) + (notes.length > 100 ? '...' : '') 1485 : 'Keine Notizen'; 1486 1487 return ` 1488 <div class="ee-notes-widget"> 1489 <div class="ee-notes-preview">${preview}</div> 1490 ${lastUpdated ? `<div class="ee-notes-updated">Zuletzt: ${lastUpdated}</div>` : ''} 1491 </div> 1492 `; 1493 } 1494 1495 renderFullscreen() { 1496 const { notes } = this.data; 1497 1498 return ` 1499 <div class="ee-notes-fullscreen"> 1500 <div class="ee-notes-toolbar"> 1501 <button class="ee-notes-btn" data-action="bold"><b>B</b></button> 1502 <button class="ee-notes-btn" data-action="italic"><i>I</i></button> 1503 <button class="ee-notes-btn" data-action="list">â°</button> 1504 <div class="ee-notes-spacer"></div> 1505 <button class="ee-notes-save-btn">Speichern</button> 1506 </div> 1507 <textarea class="ee-notes-editor" placeholder="Notizen hier eingeben...">${notes || ''}</textarea> 1508 </div> 1509 `; 1510 } 1511 1512 initFullscreen(container) { 1513 const saveBtn = container.querySelector('.ee-notes-save-btn'); 1514 const editor = container.querySelector('.ee-notes-editor'); 1515 1516 saveBtn?.addEventListener('click', () => { 1517 this.data.notes = editor.value; 1518 this.data.lastUpdated = new Date().toLocaleDateString('de-DE'); 1519 this.refresh(); 1520 }); 1521 } 1522} 1523 1524// ============================================================================= 1525// GLOBAL INITIALIZATION 1526// ============================================================================= 1527 1528/** @type {WidgetManager|null} */ 1529let widgetManager = null; 1530 1531/** 1532 * Initialize the widget system 1533 * @param {string} [containerId='ee-widget-container'] - Container element ID 1534 * @returns {WidgetManager} 1535 */ 1536function initWidgetSystem(containerId = 'ee-widget-container') { 1537 if (widgetManager) { 1538 return widgetManager; 1539 } 1540 1541 widgetManager = new WidgetManager(containerId); 1542 widgetManager.init(); 1543 1544 // Make globally available 1545 window.widgetManager = widgetManager; 1546 window.WidgetManager = WidgetManager; 1547 window.BaseWidget = BaseWidget; 1548 1549 return widgetManager; 1550} 1551 1552/** 1553 * Set global saving state across all widgets 1554 * @param {boolean} saving - Whether currently saving 1555 */ 1556function setGlobalWidgetSaving(saving) { 1557 if (typeof financeWidgetDashboard !== 'undefined' && financeWidgetDashboard?.manager) { 1558 financeWidgetDashboard.manager.setSaving(saving); 1559 return; 1560 } 1561 if (window.widgetManager) { 1562 window.widgetManager.setSaving(saving); 1563 } 1564} 1565 1566// Auto-initialize when DOM is ready 1567document.addEventListener('DOMContentLoaded', () => { 1568 if (safeGetById('ee-widget-container')) { 1569 initWidgetSystem(); 1570 } 1571}); 1572 1573// ============================================================================= 1574// EXPORTS 1575// ============================================================================= 1576 1577// Make globally available 1578window.setGlobalWidgetSaving = setGlobalWidgetSaving; 1579window.WIDGET_SIZES = WIDGET_SIZES; 1580window.formatCurrency = formatCurrency; 1581 1582// ---------------------------------------------------------------------------- 1583// Phase 4 â Tracker-Tilt constraint-badge helpers (cross-widget / cross-dashboard) 1584// ---------------------------------------------------------------------------- 1585 1586/** 1587 * Determine the "applied alpha_max" currently active in the project variation. 1588 * Returns the *minimum* alpha across all area-type tracker systems that have 1589 * tiltZones persisted. If no system has constraints â returns null. 1590 * 1591 * Uses EE_Tracker_Zones-equivalent derivation: alpha = asin(min((h_axis-minG)/r, 1592 * (maxH-h_axis)/r)) where r = moduleWidth/2 and h_axis = (minG+maxH)/2 derived 1593 * from the inputs. We fall back to the persisted `tiltZones[0].max` if shape 1594 * is too irregular to derive. 1595 */ 1596window.EE_getAppliedAlphaForProject = function() { 1597 try { 1598 if (typeof projectData === 'undefined' || !projectData || !projectData.variations) return null; 1599 var v = (typeof variation !== 'undefined') ? variation : 0; 1600 var polys = projectData.variations[v] && projectData.variations[v].polygons;
1601 if (!Array.isArray(polys) || polys.length === 0) return null; 1602 var minAlpha = null; 1603 for (var i = 0; i < polys.length; i++) { 1604 var p = polys[i]; 1605 if (!p || p.type !== 'area' || !p.system) continue; 1606 var sys = p.system; 1607 var zones = sys.tiltZones; 1608 // tiltZones may arrive as JSON-string or already-parsed object/array 1609 if (typeof zones === 'string') { 1610 try { zones = JSON.parse(zones); } catch (_) { zones = null; } 1611 } 1612 if (!zones) continue; 1613 // Shape: { "0": {min: -X, max: +X}, ... } or plain {min,max} 1614 var a = null; 1615 if (zones.max !== undefined && zones.min !== undefined) { 1616 a = Math.min(Math.abs(zones.max), Math.abs(zones.min)); 1617 } else if (zones['0'] && zones['0'].max !== undefined) { 1618 a = Math.min(Math.abs(zones['0'].max), Math.abs(zones['0'].min)); 1619 } else { 1620 // Derive from persisted heights if present 1621 var minG = parseFloat(sys.minGroundClearance); 1622 var maxH = parseFloat(sys.maxHeight); 1623 var mW = parseFloat(sys.moduleWidth || sys.modulWidth || sys.panelLength); 1624 if (!isNaN(minG) && !isNaN(maxH) && !isNaN(mW) && mW > 0 && maxH > minG) { 1625 var r = mW / 2; 1626 var hAxis = (minG + maxH) / 2; 1627 var argBot = (hAxis - minG) / r; 1628 var argTop = (maxH - hAxis) / r; 1629 var arg = Math.min(argBot, argTop); 1630 if (arg > 0 && arg <= 1) { 1631 a = Math.asin(arg) * 180 / Math.PI; 1632 } 1633 } 1634 } 1635 if (typeof a === 'number' && a > 0) { 1636 minAlpha = (minAlpha === null) ? a : Math.min(minAlpha, a); 1637 } 1638 } 1639 return minAlpha; 1640 } catch (err) { 1641 if (window.console) console.warn('[EE_getAppliedAlphaForProject]', err); 1642 return null; 1643 } 1644}; 1645 1646/** 1647 * Refresh all constraint-badges across every registered WidgetManager. 1648 * Called by TrackerTiltWidget's apply-handler post-success. 1649 * Also refreshes the currently-open fullscreen badge if applicable. 1650 */ 1651window.EE_refreshAllConstraintBadges = function() { 1652 try { 1653 if (!window.__eeWidgetManagers) return; 1654 window.__eeWidgetManagers.forEach(function(mgr) { 1655 if (!mgr || !mgr.widgets || typeof mgr.widgets.forEach !== 'function') return; 1656 mgr.widgets.forEach(function(widget) { 1657 if (widget && typeof widget._updateConstraintBadge === 'function') { 1658 try { widget._updateConstraintBadge(); } catch (_) {} 1659 } 1660 }); 1661 // Refresh the fullscreen badge of this manager if currently open 1662 if (mgr.activeFullscreenWidget && mgr.fullscreenOverlay) { 1663 var w = mgr.activeFullscreenWidget; 1664 var host = mgr.fullscreenOverlay.querySelector('[data-fullscreen-constraint-badge]'); 1665 if (host && w.showConstraintBadge && 1666 typeof window.EE_getAppliedAlphaForProject === 'function' && 1667 typeof window.EE_renderConstraintBadge === 'function') { 1668 var alpha = window.EE_getAppliedAlphaForProject(); 1669 if (typeof alpha === 'number' && alpha > 0) { 1670 window.EE_renderConstraintBadge(host, 'applied', alpha); 1671 } else if (host) { 1672 host.innerHTML = ''; 1673 } 1674 } 1675 } 1676 }); 1677 } catch (err) { 1678 if (window.console) console.warn('[EE_refreshAllConstraintBadges]', err); 1679 } 1680}; 1681 1682// Export for module systems 1683if (typeof module !== 'undefined' && module.exports) { 1684 module.exports = { 1685 WidgetManager, 1686 BaseWidget, 1687 MetricWidget, 1688 ChartWidget, 1689 GridConnectionWidget, 1690 FinanceWidget, 1691 PermitWidget, 1692 ConstructionWidget, 1693 OperationWidget, 1694 DashboardWidget, 1695 NotesWidget, 1696 initWidgetSystem, 1697 setGlobalWidgetSaving, 1698 WIDGET_SIZES, 1699 formatCurrency 1700 }; 1701}
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.