Initial commit

This commit is contained in:
Ronald Huynen
2026-03-23 21:37:59 +01:00
commit 2547717edb
2193 changed files with 972171 additions and 0 deletions

View File

@@ -0,0 +1,260 @@
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;
}
};