/** * Hierarchy Data Manager * Handles fetching and caching of league hierarchy data */ class HierarchyDataManager { constructor() { this.cacheKey = 'ft_hierarchy_data'; this.cacheVersionKey = 'ft_hierarchy_version'; this.cacheTimestampKey = 'ft_hierarchy_timestamp'; this.cacheExpiry = 24 * 60 * 60 * 1000; // 24 hours in milliseconds this.currentVersion = '1.0'; // Should match API version this.data = null; this.loading = false; this.loadPromise = null; } /** * Get hierarchy data (from cache or API) * @returns {Promise} The hierarchy data */ async getData() { // If we already have data in memory, return it if (this.data) { return this.data; } // If we're already loading, return the existing promise if (this.loading && this.loadPromise) { return this.loadPromise; } this.loading = true; this.loadPromise = this._loadData(); try { this.data = await this.loadPromise; return this.data; } finally { this.loading = false; this.loadPromise = null; } } /** * Internal method to load data from cache or API * @private */ async _loadData() { // Check if we have valid cached data const cachedData = this._getCachedData(); if (cachedData) { console.log('Using cached hierarchy data'); return cachedData; } // Fetch from API console.log('Fetching hierarchy data from API'); return await this._fetchFromAPI(); } /** * Get cached data if valid * @private */ _getCachedData() { try { const data = localStorage.getItem(this.cacheKey); const version = localStorage.getItem(this.cacheVersionKey); const timestamp = localStorage.getItem(this.cacheTimestampKey); if (!data || !version || !timestamp) { return null; } // Check version if (version !== this.currentVersion) { console.log('Cache version mismatch, invalidating'); this._clearCache(); return null; } // Check expiry const age = Date.now() - parseInt(timestamp); if (age > this.cacheExpiry) { console.log('Cache expired, invalidating'); this._clearCache(); return null; } return JSON.parse(data); } catch (error) { console.error('Error reading cached hierarchy data:', error); this._clearCache(); return null; } } /** * Fetch data from API * @private */ async _fetchFromAPI() { try { // Generate AJAX token (same logic as in index.php) const token = await this._generateToken(); const response = await fetch(`/api/hierarchy.php?token=${encodeURIComponent(token)}`, { method: 'GET', headers: { 'Accept': 'application/json', } }); if (!response.ok) { throw new Error(`API request failed: ${response.status}`); } const result = await response.json(); if (result.error) { throw new Error(result.error); } // Cache the data this._cacheData(result.data, result.version); return result.data; } catch (error) { console.error('Error fetching hierarchy data:', error); // Try to return stale cache as fallback const staleData = this._getStaleCache(); if (staleData) { console.log('Using stale cache as fallback'); return staleData; } throw error; } } /** * Generate AJAX token * @private */ async _generateToken() { // Use the global ajaxToken if available (from index.php) if (window.ajaxToken) { return window.ajaxToken; } // Fallback: generate token client-side (same logic as server) const now = new Date(); const dateString = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}-${now.getHours()}`; const message = dateString + 'followteams_secret_2025'; // Use Web Crypto API to generate SHA-256 hash const encoder = new TextEncoder(); const data = encoder.encode(message); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } /** * Cache data in localStorage * @private */ _cacheData(data, version) { try { localStorage.setItem(this.cacheKey, JSON.stringify(data)); localStorage.setItem(this.cacheVersionKey, version || this.currentVersion); localStorage.setItem(this.cacheTimestampKey, Date.now().toString()); console.log('Hierarchy data cached successfully'); } catch (error) { console.error('Error caching hierarchy data:', error); // Continue without caching - not critical } } /** * Get stale cache data (ignoring expiry) * @private */ _getStaleCache() { try { const data = localStorage.getItem(this.cacheKey); const version = localStorage.getItem(this.cacheVersionKey); if (!data || !version || version !== this.currentVersion) { return null; } return JSON.parse(data); } catch (error) { return null; } } /** * Clear all cached data * @private */ _clearCache() { try { localStorage.removeItem(this.cacheKey); localStorage.removeItem(this.cacheVersionKey); localStorage.removeItem(this.cacheTimestampKey); } catch (error) { console.error('Error clearing hierarchy cache:', error); } } /** * Force refresh data from API */ async refresh() { this._clearCache(); this.data = null; return await this.getData(); } /** * Preload data (non-blocking) */ preload() { // Start loading in background, but don't wait for it this.getData().catch(error => { console.warn('Preload of hierarchy data failed:', error); }); } } // Create global instance window.hierarchyManager = new HierarchyDataManager(); // Backward compatibility: provide hierarchyData as a promise window.getHierarchyData = () => window.hierarchyManager.getData(); // Preload data when script loads (non-blocking) document.addEventListener('DOMContentLoaded', () => { window.hierarchyManager.preload(); });