import { Chart, registerables } from 'chart.js'; Chart.register(...registerables); window.initAccountBalancesChart = function(timelineData, decimalFormat) { const chartCanvas = document.getElementById('accountBalancesChart'); if (!chartCanvas || !timelineData || timelineData.length === 0) { return; } const ctx = chartCanvas.getContext('2d'); if (!ctx) { return; } // Destroy existing chart if it exists if (chartCanvas.chart) { chartCanvas.chart.destroy(); } // Format minutes as either HH:MM or decimal hours function formatMinutes(minutes) { if (decimalFormat) { const isNegative = minutes < 0; const absMinutes = Math.abs(minutes); const decimal = (absMinutes / 60).toFixed(2).replace('.', ','); return (isNegative ? '-' : '') + decimal + ' h.'; } const isNegative = minutes < 0; const absMinutes = Math.abs(minutes); const wholeHours = Math.floor(absMinutes / 60); const restMinutes = String(absMinutes % 60).padStart(2, '0'); return 'H ' + (isNegative ? '-' : '') + wholeHours + ':' + restMinutes; } try { // Prepare datasets for each account const datasets = []; const allAccounts = new Map(); timelineData.forEach(month => { if (month.accounts && Array.isArray(month.accounts)) { month.accounts.forEach(account => { if (!allAccounts.has(account.account_id)) { allAccounts.set(account.account_id, { name: account.account_name, id: account.account_id }); } }); } }); const colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444']; let colorIndex = 0; allAccounts.forEach((accountInfo, accountId) => { const accountData = timelineData.map(month => { const accountBalance = month.accounts?.find(acc => acc.account_id === accountId); const minutes = accountBalance ? parseInt(accountBalance.balance) : 0; // Convert minutes to decimal hours for chart display return minutes / 60; }); datasets.push({ label: accountInfo.name, data: accountData, borderColor: colors[colorIndex % colors.length], backgroundColor: colors[colorIndex % colors.length] + '33', // 0.2 transparency (33 in hex = 20% opacity) borderWidth: 3, tension: 0.4, // More curved line fill: true, // Fill below the line pointRadius: 6, pointHoverRadius: 8, pointBackgroundColor: colors[colorIndex % colors.length], pointBorderColor: '#ffffff', pointBorderWidth: 2, }); colorIndex++; }); if (datasets.length === 0) { return; } const chartLabels = timelineData.map(item => item.label); const chartType = chartLabels.length === 1 ? 'bar' : 'line'; // Get translated labels from the first data point const translations = timelineData[0]?.translations || { period: 'Period', balance: 'Balance' }; const config = { type: chartType, data: { labels: chartLabels, datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false, }, plugins: { legend: { display: true, position: 'top', align: 'end', labels: { usePointStyle: true, color: '#1F2937' } }, tooltip: { callbacks: { label: function(context) { const minutes = Math.round(context.parsed.y * 60); return context.dataset.label + ': ' + formatMinutes(minutes); } } } }, scales: { x: { display: true, title: { display: true, text: translations.period, color: '#6B7280' }, grid: { color: 'rgba(0, 0, 0, 0.1)' }, ticks: { color: '#6B7280' } }, y: { display: true, title: { display: true, text: translations.balance, color: '#6B7280' }, grid: { color: 'rgba(0, 0, 0, 0.1)' }, ticks: { color: '#6B7280', callback: function(value) { const minutes = Math.round(value * 60); return formatMinutes(minutes); } } } } } }; chartCanvas.chart = new Chart(ctx, config); // Store chart instance globally for PDF export window.accountBalancesChart = chartCanvas.chart; } catch (error) { } }; // EXACT COPY of return ratio chart initialization pattern function initializeChart() { // Find chart container const chartContainer = document.querySelector('[data-balance-chart-data]'); if (!chartContainer) { return; } // Get chart data const timelineDataAttr = chartContainer.getAttribute('data-balance-chart-data'); if (!timelineDataAttr) { return; } try { const timelineData = JSON.parse(timelineDataAttr); const decimalFormat = chartContainer.getAttribute('data-decimal-format') === '1'; // Initialize the chart if (typeof window.initAccountBalancesChart === 'function') { window.initAccountBalancesChart(timelineData, decimalFormat); } } catch (error) { } } // Initialize chart on page load document.addEventListener('DOMContentLoaded', () => { setTimeout(initializeChart, 100); }); // Listen for Livewire updates - debounced to prevent rapid re-initialization document.addEventListener('livewire:init', () => { let morphTimer = null; Livewire.hook('morph.updated', () => { clearTimeout(morphTimer); morphTimer = setTimeout(() => { initializeChart(); }, 300); }); }); // Function to export Account Balances chart as base64 image for PDF window.exportAccountBalancesChartForPdf = async function() { if (!window.accountBalancesChart) { return null; } try { const originalChart = window.accountBalancesChart; // Render into an offscreen canvas to avoid touching the live DOM canvas // (which can be detached by Livewire morphs mid-export) const pdfWidth = 900; const pdfHeight = 400; const offscreen = document.createElement('canvas'); offscreen.width = pdfWidth; offscreen.height = pdfHeight; const ctx = offscreen.getContext('2d'); // Clone config from existing chart const offscreenChart = new Chart(ctx, { type: originalChart.config.type, data: JSON.parse(JSON.stringify(originalChart.config.data)), options: { ...JSON.parse(JSON.stringify(originalChart.config.options || {})), responsive: false, maintainAspectRatio: false, animation: false, } }); // Wait for render await new Promise(resolve => setTimeout(resolve, 300)); const chartImage = offscreen.toDataURL('image/png', 1.0); offscreenChart.destroy(); return chartImage; } catch (error) { return null; } };