PageSourceSearch

https://energyexperts.ai/wp-content/plugins/energyExperts/assets/js/ee-widget-system.js?ver=2.25.616

js energyexperts.ai collected 2026-10-05 12:12:29 UTC 58,544 bytes, 1,701 lines download raw bytes

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.