CoolFace
Apppublic

Completeyourprofile111/voi-multi-ride-manager

sourceHugging Faceupdated 11mo agoView on Hugging Face
0likes
script.js753 linesDownload Raw Back to root
1// Voi Multi-Account Manager2class VoiManager {3    constructor() {4        this.accounts = this.loadAccounts();5        this.currentAccount = null;6        this.currentTab = 'dashboard';7        this.apiBaseUrl = window.location.origin + '/api/voi'; // Backend proxy8        this.init();9    }10 11    init() {12        this.renderAccountSelector();13        if (this.accounts.length > 0) {14            this.selectAccount(this.accounts[0].id);15        }16        this.switchTab('dashboard');17    }18 19    // Account Management20    loadAccounts() {21        const stored = localStorage.getItem('voi_accounts');22        return stored ? JSON.parse(stored) : [];23    }24 25    saveAccounts() {26        localStorage.setItem('voi_accounts', JSON.stringify(this.accounts));27    }28 29    async selectAccount(accountId) {30        this.currentAccount = this.accounts.find(a => a.id === accountId);31        if (this.currentAccount) {32            await this.authenticateAccount();33            this.renderAccountSelector();34            this.loadCurrentTabContent();35        }36    }37    async authenticateAccount() {38        try {39            const response = await fetch(`${this.apiBaseUrl}/v1/auth/session`, {40                method: 'POST',41                headers: { 'Content-Type': 'application/json' },42                body: JSON.stringify({ 43                    authenticationToken: this.currentAccount.authToken 44                })45            });46            47            if (response.ok) {48                const data = await response.json();49                this.currentAccount.accessToken = data.accessToken;50                if (data.authenticationToken) {51                    this.currentAccount.authToken = data.authenticationToken;52                    this.saveAccounts();53                }54            } else {55                throw new Error(`Authentication failed: ${response.status}`);56            }57        } catch (error) {58            console.error('Authentication failed:', error);59            this.showNotification('Authentication failed. Please check your token.', 'error');60            throw error;61        }62    }63addAccount(name, authToken) {64        const account = {65            id: 'acc_' + Date.now(),66            name: name,67            authToken: authToken,68            accessToken: null,69            addedAt: new Date().toISOString()70        };71        72        this.accounts.push(account);73        this.saveAccounts();74        this.selectAccount(account.id);75        this.showNotification('Account added successfully', 'success');76    }77 78    removeAccount(accountId) {79        this.accounts = this.accounts.filter(a => a.id !== accountId);80        this.saveAccounts();81        82        if (this.currentAccount?.id === accountId) {83            this.currentAccount = this.accounts.length > 0 ? this.accounts[0] : null;84            if (this.currentAccount) {85                this.selectAccount(this.currentAccount.id);86            } else {87                this.renderAccountSelector();88                this.loadCurrentTabContent();89            }90        }91        92        this.showNotification('Account removed', 'info');93    }94 95    // UI Rendering96    renderAccountSelector() {97        const selector = document.getElementById('accountSelector');98        99        if (this.accounts.length === 0) {100            selector.innerHTML = `101                <div class="text-gray-500 py-8 text-center w-full">102                    <i data-feather="users" class="w-12 h-12 mx-auto mb-2"></i>103                    <p>No accounts added yet</p>104                </div>105            `;106            feather.replace();107            return;108        }109 110        selector.innerHTML = this.accounts.map(account => `111            <div class="account-chip ${this.currentAccount?.id === account.id ? 'active' : 'bg-gray-100'} 112                        px-4 py-2 rounded-full flex items-center gap-2"113                 onclick="voiManager.selectAccount('${account.id}')">114                <div class="w-2 h-2 rounded-full ${this.currentAccount?.id === account.id ? 'bg-white' : 'bg-green-500'}"></div>115                <span class="font-medium">${account.name}</span>116                <button onclick="event.stopPropagation(); voiManager.removeAccount('${account.id}')" 117                        class="ml-2 hover:opacity-70">118                    <i data-feather="x" class="w-4 h-4"></i>119                </button>120            </div>121        `).join('');122        123        feather.replace();124    }125 126    // Tab Management127    switchTab(tabName) {128        this.currentTab = tabName;129        130        // Update tab buttons131        document.querySelectorAll('.tab-btn').forEach(btn => {132            btn.classList.toggle('active', btn.dataset.tab === tabName);133        });134        135        // Disable tabs if no accounts136        if (this.accounts.length === 0) {137            document.getElementById('tabContent').innerHTML = `138                <div class="text-center py-12 text-gray-500">139                    <i data-feather="user-plus" class="w-16 h-16 mx-auto mb-4"></i>140                    <p class="text-xl mb-4">Please add an account to continue</p>141                    <button type="button" onclick="openAddAccountModal()" class="bg-purple-600 text-white px-6 py-2 rounded-lg hover:bg-purple-700 transition">142                        Add Your First Account143                    </button>144                </div>145            `;146            feather.replace();147            return;148        }149        150        this.loadCurrentTabContent();151    }152async loadCurrentTabContent() {153        const content = document.getElementById('tabContent');154        155        if (!this.currentAccount) {156            content.innerHTML = `157                <div class="text-center py-12 text-gray-500">158                    <i data-feather="user-plus" class="w-16 h-16 mx-auto mb-4"></i>159                    <p class="text-xl">Please add an account to continue</p>160                </div>161            `;162            feather.replace();163            return;164        }165 166        // Show loading skeleton167        content.innerHTML = this.getLoadingSkeleton();168        169        try {170            switch (this.currentTab) {171                case 'dashboard':172                    await this.loadDashboard();173                    break;174                case 'vehicles':175                    await this.loadVehicles();176                    break;177                case 'rides':178                    await this.loadRides();179                    break;180                case 'wallet':181                    await this.loadWallet();182                    break;183            }184            185            // Update quick stats186            await this.updateQuickStats();187        } catch (error) {188            console.error('Failed to load tab content:', error);189            content.innerHTML = `190                <div class="text-center py-12 text-red-500">191                    <i data-feather="alert-circle" class="w-16 h-16 mx-auto mb-4"></i>192                    <p class="text-xl">Failed to load content</p>193                    <button onclick="voiManager.loadCurrentTabContent()" class="mt-4 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600">194                        Retry195                    </button>196            </div>197        `;198        } catch (error) {199            console.error('Failed to load dashboard:', error);200            this.showNotification('Failed to load dashboard', 'error');201        }202    }203feather.replace();204    }205 206    // Content Loaders207    async loadDashboard() {208        try {209            const [userInfo, rideState] = await Promise.all([210                this.makeApiCall('/v1/users/me'),211                this.makeApiCall('/v1/rides/state')212            ]);213 214            const content = document.getElementById('tabContent');215            content.innerHTML = `216                <div class="space-y-6">217                    <div class="bg-gradient-to-r from-purple-500 to-pink-500 rounded-xl p-6 text-white">218                        <h3 class="text-2xl font-bold mb-2">Welcome back, ${userInfo?.user?.name || 'Rider'}!</h3>219                        <p class="opacity-90">${rideState?.data?.activeRide ? 'You have an active ride' : 'Ready for your next ride'}</p>220                    </div>221                    222                    <div class="grid grid-cols-1 md:grid-cols-2 gap-6">223                        <div class="border border-gray-200 rounded-lg p-4">224                            <h4 class="font-semibold mb-2 flex items-center gap-2">225                                <i data-feather="phone" class="w-5 h-5"></i>226                                Contact227                            </h4>228                            <p class="text-gray-600">${userInfo?.user?.email || 'Not set'}</p>229                            <p class="text-gray-600">${userInfo?.user?.phone || 'Not set'}</p>230                        </div>231                        232                        <div class="border border-gray-200 rounded-lg p-4">233                            <h4 class="font-semibold mb-2 flex items-center gap-2">234                                <i data-feather="map-pin" class="w-5 h-5"></i>235                                Location236                            </h4>237                            <p class="text-gray-600">${userInfo?.user?.country || 'Unknown'}</p>238                            <p class="text-gray-600">Member since ${new Date(userInfo?.user?.createdAt || userInfo?.user?.addedAt || Date.now()).toLocaleDateString()}</p>239                        </div>240                    </div>241                    242                    ${rideState?.data?.activeRide ? this.getActiveRideCard(rideState.data.activeRide) : ''}243                </div>244            `;245            feather.replace();246        } catch (error) {247            console.error('Failed to load dashboard:', error);248            this.showNotification('Failed to load dashboard', 'error');249        }250    }251    async loadVehicles() {252        try {253            const zones = await this.makeApiCall('/v1/zones');254            const selectedZone = localStorage.getItem('selectedZone') || 'de_ber_1';255            256            // Show loading state257            const content = document.getElementById('tabContent');258            content.innerHTML = this.getLoadingSkeleton();259            260            const vehicles = await this.makeApiCall(`/v2/rides/vehicles?zone_id=${selectedZone}&include_suggestion=true`);261 262            content.innerHTML = `263                <div class="space-y-6">264                    <div class="flex items-center justify-between">265                        <h3 class="text-xl font-bold">Available Vehicles</h3>266                        <select id="zoneSelect" onchange="voiManager.loadVehiclesForZone(this.value)" 267                                class="px-4 py-2 border border-gray-300 rounded-lg">268                            ${zones?.zones?.map(zone => 269                                `<option value="${zone.zone_id}" ${zone.zone_id === selectedZone ? 'selected' : ''}>${zone.name}, ${zone.city}</option>`270                            ).join('') || '<option>No zones available</option>'}271                        </select>272                    </div>273                    274                    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">275                        ${vehicles?.data?.vehicle_groups?.[0]?.vehicles?.map(vehicle => 276                            this.getVehicleCard(vehicle)277                        ).join('') || '<p class="text-gray-500 col-span-full text-center py-8">No vehicles available in this zone</p>'}278                    </div>279                </div>280            `;281            feather.replace();282        } catch (error) {283            console.error('Failed to load vehicles:', error);284            this.showNotification('Failed to load vehicles', 'error');285        }286    }287async loadVehiclesForZone(zoneId) {288        localStorage.setItem('selectedZone', zoneId);289        await this.loadVehicles();290    }291    async loadRides() {292        try {293            // Show loading state294            const content = document.getElementById('tabContent');295            content.innerHTML = this.getLoadingSkeleton();296            297            const history = await this.makeApiCall('/v2/rides/history?limit=10');298 299            content.innerHTML = `300                <div class="space-y-6">301                    <div class="flex items-center justify-between">302                        <h3 class="text-xl font-bold">Ride History</h3>303                        <button type="button" onclick="voiManager.loadCurrentTabContent()" class="text-purple-600 hover:text-purple-700 text-sm flex items-center gap-1">304                            <i data-feather="refresh-cw" class="w-4 h-4"></i>305                            Refresh306                        </button>307                    </div>308                    309                    ${history?.data?.[0]?.history?.length ? `310                        <div class="space-y-4">311                            ${history.data[0].history.map(ride => this.getRideCard(ride)).join('')}312                        </div>313                    ` : '<p class="text-gray-500 text-center py-8">No rides found</p>'}314                </div>315            `;316            feather.replace();317        } catch (error) {318            console.error('Failed to load rides:', error);319            this.showNotification('Failed to load ride history', 'error');320        }321    }322async loadWallet() {323        try {324            // Show loading state325            const content = document.getElementById('tabContent');326            content.innerHTML = this.getLoadingSkeleton();327            328            const [wallet, payments] = await Promise.all([329                this.makeApiCall('/v1/user/wallet'),330                this.makeApiCall('/v2/payments/profiles?all=true')331            ]);332 333            content.innerHTML = `334                <div class="space-y-6">335                    <div class="bg-gradient-to-r from-green-500 to-teal-500 rounded-xl p-6 text-white">336                        <h3 class="text-2xl font-bold mb-2">Voi Credits</h3>337                        <p class="text-4xl font-bold">€${(wallet?.data?.voiCreditsBalance || 0).toFixed(2)}</p>338                    </div>339                    340                    <div class="border border-gray-200 rounded-lg p-4">341                        <h4 class="font-semibold mb-4 flex items-center gap-2">342                            <i data-feather="credit-card" class="w-5 h-5"></i>343                            Payment Methods344                        </h4>345                        ${payments?.data?.length ? payments.data.map(profile => 346                            profile.payment_methods?.map(method => `347                                <div class="flex items-center justify-between py-3 border-b border-gray-100 last:border-0">348                                    <div class="flex items-center gap-3">349                                        <div class="w-10 h-6 bg-gradient-to-r from-blue-600 to-blue-400 rounded flex items-center justify-center">350                                            <span class="text-white text-xs font-bold">${method.brand?.toUpperCase() || 'CARD'}</span>351                                        </div>352                                        <span>•••• ${method.hint || method.last4 || '****'}</span>353                                        ${method.default ? '<span class="bg-green-100 text-green-800 text-xs px-2 py-1 rounded">Default</span>' : ''}354                                    </div>355                                </div>356                            `).join('') || '<p class="text-gray-500">No payment methods in profile</p>'357                        ) : '<p class="text-gray-500">No payment methods added</p>'}358                    </div>359                </div>360            `;361            feather.replace();362        } catch (error) {363            console.error('Failed to load wallet:', error);364            this.showNotification('Failed to load wallet', 'error');365        }366    }367    async updateQuickStats() {368        const stats = document.getElementById('quickStats');369        370        try {371            const [wallet, rideState, history] = await Promise.all([372                this.makeApiCall('/v1/user/wallet'),373                this.makeApiCall('/v1/rides/state'),374                this.makeApiCall('/v2/rides/history?limit=1')375            ]);376 377            stats.innerHTML = `378                <div class="bg-white rounded-lg p-4 border border-gray-200 hover-card">379                    <div class="flex items-center justify-between">380                        <div>381                            <p class="text-gray-500 text-sm">Balance</p>382                            <p class="text-2xl font-bold text-gray-900">€${(wallet?.data?.voiCreditsBalance || 0).toFixed(2)}</p>383                        </div>384                        <i data-feather="wallet" class="w-8 h-8 text-purple-500"></i>385                    </div>386                </div>387                388                <div class="bg-white rounded-lg p-4 border border-gray-200 hover-card">389                    <div class="flex items-center justify-between">390                        <div>391                            <p class="text-gray-500 text-sm">Status</p>392                            <p class="text-2xl font-bold text-gray-900">393                                ${rideState?.data?.activeRide ? 'Active' : 'Idle'}394                            </p>395                        </div>396                        <i data-feather="activity" class="w-8 h-8 ${rideState?.data?.activeRide ? 'text-green-500' : 'text-gray-400'}"></i>397                    </div>398                </div>399                400                <div class="bg-white rounded-lg p-4 border border-gray-200 hover-card">401                    <div class="flex items-center justify-between">402                        <div>403                            <p class="text-gray-500 text-sm">Total Rides</p>404                            <p class="text-2xl font-bold text-gray-900">${history?.data?.[0]?.history?.length || 0}</p>405                        </div>406                        <i data-feather="trending-up" class="w-8 h-8 text-blue-500"></i>407                    </div>408                </div>409                410                <div class="bg-white rounded-lg p-4 border border-gray-200 hover-card">411                    <div class="flex items-center justify-between">412                        <div>413                            <p class="text-gray-500 text-sm">Accounts</p>414                            <p class="text-2xl font-bold text-gray-900">${this.accounts.length}</p>415                        </div>416                        <i data-feather="users" class="w-8 h-8 text-indigo-500"></i>417                    </div>418                </div>419            `;420        } catch (error) {421            console.error('Failed to update stats:', error);422        }423        424        feather.replace();425    }426// API Helper427    async makeApiCall(endpoint, options = {}) {428        if (!this.currentAccount?.accessToken) {429            throw new Error('No active account');430        }431 432        const response = await fetch(`${this.apiBaseUrl}${endpoint}`, {433            ...options,434            headers: {435                'x-access-token': this.currentAccount.accessToken,436                'x-app-name': 'Rider',437                'x-device-name': 'Web',438                'x-app-version': '3.267.0',439                'Content-Type': 'application/json',440                ...options.headers441            }442        });443 444        if (response.status === 401) {445            // Token expired, re-authenticate446            try {447                await this.authenticateAccount();448                // Retry the request with new token449                const retryResponse = await fetch(`${this.apiBaseUrl}${endpoint}`, {450                    ...options,451                    headers: {452                        'x-access-token': this.currentAccount.accessToken,453                        'x-app-name': 'Rider',454                        'x-device-name': 'Web',455                        'x-app-version': '3.267.0',456                        'Content-Type': 'application/json',457                        ...options.headers458                    }459                });460                if (!retryResponse.ok) {461                    throw new Error(`API Error: ${retryResponse.status}`);462                }463                return retryResponse.json();464            } catch (authError) {465                console.error('Re-authentication failed:', authError);466                throw new Error('Authentication failed. Please re-add your account.');467            }468        }469 470        if (!response.ok) {471            throw new Error(`API Error: ${response.status}`);472        }473 474        return response.json();475    }476// UI Components477    getLoadingSkeleton() {478        return `479            <div class="space-y-4">480                <div class="skeleton h-8 w-1/3 rounded"></div>481                <div class="skeleton h-32 w-full rounded"></div>482                <div class="skeleton h-32 w-full rounded"></div>483            </div>484        `;485    }486 487    getVehicleCard(vehicle) {488        return `489            <div class="border border-gray-200 rounded-lg p-4 hover-card">490                <div class="flex items-center justify-between mb-3">491                    <span class="font-bold text-lg">${vehicle.short}</span>492                    <span class="bg-green-100 text-green-800 text-xs px-2 py-1 rounded">493                        ${vehicle.battery}% battery494                    </span>495                </div>496                <div class="text-gray-600 text-sm mb-3">497                    <i data-feather="map-pin" class="w-4 h-4 inline mr-1"></i>498                    ${vehicle.location.lat.toFixed(4)}, ${vehicle.location.lng.toFixed(4)}499                </div>500                <button onclick="voiManager.scanVehicle('${vehicle.short}')" 501                        class="w-full bg-purple-600 text-white py-2 rounded-lg hover:bg-purple-700 transition">502                    Scan QR503                </button>504            </div>505        `;506    }507 508    getRideCard(ride) {509        return `510            <div class="border border-gray-200 rounded-lg p-4 hover-card">511                <div class="flex items-center justify-between mb-2">512                    <span class="font-semibold">Ride #${ride.id.slice(-6)}</span>513                    <span class="text-lg font-bold text-purple-600">€${(ride.cost / 100).toFixed(2)}</span>514                </div>515                <div class="text-gray-600 text-sm">516                    <div class="flex items-center gap-2 mb-1">517                        <i data-feather="clock" class="w-4 h-4"></i>518                        ${new Date(ride.startTime).toLocaleString()} - ${new Date(ride.end).toLocaleTimeString()}519                    </div>520                    <div class="flex items-center gap-2">521                        <i data-feather="navigation" class="w-4 h-4"></i>522                        Vehicle: ${ride.vehicleShort}523                    </div>524                </div>525            </div>526        `;527    }528 529    getActiveRideCard(ride) {530        return `531            <div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">532                <div class="flex items-center justify-between mb-3">533                    <h4 class="font-semibold text-yellow-800">Active Ride</h4>534                    <span class="bg-yellow-100 text-yellow-800 text-xs px-2 py-1 rounded animate-pulse">535                        In Progress536                    </span>537                </div>538                <div class="text-sm text-gray-600 mb-3">539                    <p>Vehicle: ${ride.vehicleShort}</p>540                    <p>Started: ${new Date(ride.startTimeInSec * 1000).toLocaleTimeString()}</p>541                </div>542                <button onclick="voiManager.endRide()" 543                        class="w-full bg-red-500 text-white py-2 rounded-lg hover:bg-red-600 transition">544                    End Ride545                </button>546            </div>547        `;548    }549 550    // Actions551    async scanVehicle(vehicleCode) {552        try {553            const preride = await this.makeApiCall(`/v2/gateway/preride?vehicle_qr_code=${vehicleCode}`);554            console.log('Vehicle scanned:', preride);555            this.showNotification(`Vehicle ${vehicleCode} scanned successfully`, 'success');556        } catch (error) {557            console.error('Scan failed:', error);558            this.showNotification('Failed to scan vehicle: ' + (error.message || 'Unknown error'), 'error');559        }560    }561    async endRide() {562        if (!confirm('Are you sure you want to end this ride?')) return;563        564        try {565            // Show loading state566            const endButton = event.target;567            const originalText = endButton.innerHTML;568            endButton.innerHTML = '<div class="loading"></div> Ending...';569            endButton.disabled = true;570            571            // Get current position or use default572            let location = {573                radialAccuracy: 8.0,574                timestamp: Date.now() / 1000,575                latitude: 52.5250,576                longitude: 13.4150577            };578            579            if (navigator.geolocation) {580                try {581                    const position = await new Promise((resolve, reject) => {582                        navigator.geolocation.getCurrentPosition(resolve, reject, {583                            timeout: 10000,584                            enableHighAccuracy: true585                        });586                    });587                    location.latitude = position.coords.latitude;588                    location.longitude = position.coords.longitude;589                    location.radialAccuracy = position.coords.accuracy || 8.0;590                } catch (geoError) {591                    console.log('Geolocation error, using default location', geoError);592                }593            }594            595            await this.makeApiCall('/v2/rides/end_session', {596                method: 'POST',597                headers: {598                    'Content-Type': 'application/json'599                },600                body: JSON.stringify({601                    liftParkingRestrictions: false,602                    location: location,603                    vpsStatus: "NOT_APPLICABLE"604                })605            });606            607            this.showNotification('Ride ended successfully', 'success');608            await this.loadCurrentTabContent();609        } catch (error) {610            console.error('Failed to end ride:', error);611            this.showNotification('Failed to end ride: ' + (error.message || 'Please try again.'), 'error');612        } finally {613            // Reset button state614            const endButton = document.querySelector('.bg-red-500');615            if (endButton) {616                endButton.innerHTML = 'End Ride';617                endButton.disabled = false;618            }619        }620    }621showNotification(message, type = 'info') {622        const colors = {623            success: 'bg-green-500',624            error: 'bg-red-500',625            info: 'bg-blue-500',626            warning: 'bg-yellow-500'627        };628 629        const icons = {630            success: 'check-circle',631            error: 'x-circle',632            info: 'info',633            warning: 'alert-triangle'634        };635 636        // Remove any existing notifications637        document.querySelectorAll('.notification-toast').forEach(n => n.remove());638 639        const notification = document.createElement('div');640        notification.className = `notification-toast fixed top-4 right-4 ${colors[type]} text-white px-6 py-3 rounded-lg shadow-lg z-50 fade-in`;641        notification.innerHTML = `642            <div class="flex items-center gap-2">643                <i data-feather="${icons[type]}" class="w-5 h-5"></i>644                <span>${message}</span>645            </div>646        `;647        648        document.body.appendChild(notification);649        feather.replace();650        651        setTimeout(() => {652            notification.style.opacity = '0';653            notification.style.transition = 'opacity 0.3s ease-out';654            setTimeout(() => {655                if (notification.parentNode) {656                    notification.remove();657                }658            }, 300);659        }, 4000);660    }661}662// Initialize global instance663let voiManager;664 665// Global error handler666window.addEventListener('error', (event) => {667    console.error('Global error:', event.error);668    if (voiManager) {669        voiManager.showNotification('An unexpected error occurred', 'error');670    }671});672 673// Unhandled promise rejection handler674window.addEventListener('unhandledrejection', (event) => {675    console.error('Unhandled promise rejection:', event.reason);676    if (voiManager) {677        voiManager.showNotification('A network error occurred', 'error');678    }679    event.preventDefault();680});681// UI Functions682function initializeApp() {683    voiManager = new VoiManager();684}685 686function switchTab(tabName) {687    if (voiManager) {688        voiManager.switchTab(tabName);689    }690}691 692function openAddAccountModal() {693    const modal = document.getElementById('addAccountModal');694    if (modal) {695        modal.classList.remove('hidden');696    }697}698 699function closeAddAccountModal() {700    const modal = document.getElementById('addAccountModal');701    if (modal) {702        modal.classList.add('hidden');703        const accountName = document.getElementById('accountName');704        const authToken = document.getElementById('authToken');705        if (accountName) accountName.value = '';706        if (authToken) authToken.value = '';707    }708}709 710function addAccount(event) {711    event.preventDefault();712    713    const accountName = document.getElementById('accountName');714    const authToken = document.getElementById('authToken');715    716    if (!accountName || !authToken) return;717    718    const name = accountName.value.trim();719    const token = authToken.value.trim();720    721    if (!name || !token) {722        if (voiManager) {723            voiManager.showNotification('Please fill in all fields', 'error');724        }725        return;726    }727    728    if (voiManager) {729        voiManager.addAccount(name, token);730    }731    closeAddAccountModal();732}733// Performance optimizations734if ('serviceWorker' in navigator) {735    window.addEventListener('load', () => {736        navigator.serviceWorker.register('/sw.js')737            .then(registration => console.log('SW registered'))738            .catch(err => console.log('SW registration failed'));739    });740}741 742// Debounce helper743function debounce(func, wait) {744    let timeout;745    return function executedFunction(...args) {746        const later = () => {747            clearTimeout(timeout);748            func(...args);749        };750        clearTimeout(timeout);751        timeout = setTimeout(later, wait);752    };753}