| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314 |
- {{-- resources/views/livewire/reports.blade.php --}}
- <div>
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
- <link rel="stylesheet" href="/css/chart-reports.css">
- <div class="dashboard-container">
- <div wire:ignore class="chart-row" style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: start;">
- <div class="chart-card">
- <div class="chart-header">
- <h3 class="chart-title">Entrate/Uscite totali</h3>
- </div>
- <div class="chart-body">
- <div class="yearly-table-container" id="yearly-table"></div>
- </div>
- </div>
- {{-- <div class="chart-card">
- <div class="chart-header">
- <h3 class="chart-title">Tesserati per Stagione</h3>
- </div>
- <div class="chart-body">
- <div class="members-table-container" id="members-table"></div>
- </div>
- </div> --}}
- </div>
- <div class="controls-section">
- <div class="control-group">
- <label for="season-filter">Stagione di Riferimento:</label>
- <select class="form-select" wire:model="seasonFilter" {{--wire:change="updateCharts"--}}>
- @foreach($this->getAvailableSeasons() as $season)
- <option value="{{ $season }}">{{ $season }}</option>
- @endforeach
- </select>
- </div>
- </div>
- @php
- $summary = $this->getYearlySummary();
- @endphp
- <div class="summary-cards">
- <div class="summary-card income">
- <h3>Entrate Totali</h3>
- <div class="value">€{{ number_format($summary['totalIncome'], 2, ',', '.') }}</div>
- </div>
- <div class="summary-card expense">
- <h3>Uscite Totali</h3>
- <div class="value">€{{ number_format($summary['totalExpenses'], 2, ',', '.') }}</div>
- </div>
- <div class="summary-card delta {{ $summary['delta'] < 0 ? 'negative' : '' }}">
- <h3>Bilancio Netto</h3>
- <div class="value">€{{ number_format($summary['delta'], 2, ',', '.') }}</div>
- </div>
- </div>
- <div wire:ignore>
- <div class="chart-row">
- <div class="chart-card">
- <div class="chart-header">
- <h3 class="chart-title">Entrate e Uscite Mensili - <span id="monthly-season-title">{{ $seasonFilter }}</span></h3>
- </div>
- <div class="chart-body">
- <div style="display: grid; grid-template-columns: 1fr 300px; align-items: start;">
- <div class="chart-container">
- <canvas id="monthly-chart-{{ str_replace('-', '', $seasonFilter) }}"></canvas>
- </div>
- <div class="monthly-table-container" id="monthly-table">
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="chart-row">
- <div class="chart-card">
- <div class="chart-header">
- <h3 class="chart-title">Redditività per Causale - <span id="causals-season-title">{{ $seasonFilter }}</span></h3>
- </div>
- <div class="chart-body">
- <div style="display: grid; grid-template-columns: 1fr 700px; gap: 1rem; align-items: start;">
- <div class="causals-table-container" id="causals-table">
- </div>
- <div class="chart-container">
- <canvas id="causals-chart-{{ str_replace('-', '', $seasonFilter) }}"></canvas>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="chart-row">
- <div class="chart-card">
- <div class="chart-header">
- <h3 class="chart-title">Tesserati per Stagione</h3>
- </div>
- <div class="chart-body">
- <div style="display: grid; grid-template-columns: 1fr 500px; gap: 1rem; align-items: start;">
- <div class="chart-container">
- <canvas id="members-chart"></canvas>
- </div>
- <div class="chart-body">
- <div class="members-table-container" id="members-table"></div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
-
- <div class="chart-row">
- <div class="chart-card modern-course-card">
- <div class="chart-header">
- <h3 class="chart-title">Analisi incassi corsi</h3>
- </div>
- <div class="chart-body">
- <div class="course-controls">
- <div class="control-group">
- <label>Seleziona Corso ({{ $seasonFilter }}):</label>
- <select class="form-select modern-select" wire:model.live="selectedCourse">
- <option value="">Seleziona un Corso</option>
- @foreach($this->getCoursesForSelect() as $course)
- <option value="{{ $course['id'] }}">{{ $course['full_name'] }}</option>
- @endforeach
- </select>
- </div>
- </div>
- @if($selectedCourse)
- <div wire:ignore wire:key="course-chart-{{ $seasonFilter }}-{{ $selectedCourse }}">
- <div class="modern-chart-layout">
- <div class="course-delta-table" id="course-delta-table-{{ str_replace('-', '', $seasonFilter) }}-{{ $selectedCourse }}">
- </div>
- <div class="modern-chart-container">
- <canvas id="courses-chart-{{ str_replace('-', '', $seasonFilter) }}-{{ $selectedCourse }}"></canvas>
- </div>
- </div>
- </div>
- @else
- <div class="chart-placeholder">
- <div style="text-align: center;">
- <div style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.3;">📊</div>
- <p style="font-size: 1.25rem; font-weight: 600; margin: 0;">Seleziona un corso per
- visualizzare il grafico</p>
- <p style="font-size: 1rem; opacity: 0.7; margin-top: 0.5rem;">Usa il menu a tendina sopra
- per scegliere un corso</p>
- </div>
- </div>
- @endif
- </div>
- </div>
- </div>
-
- </div>
- <script>
- window.ReportsChartManager = window.ReportsChartManager || {
- charts: {},
- currentSeason: null,
- destroyChart: function (chartId) {
- if (this.charts[chartId]) {
- this.charts[chartId].destroy();
- delete this.charts[chartId];
- }
- },
- destroyAllCharts: function () {
- Object.keys(this.charts).forEach(chartId => {
- this.destroyChart(chartId);
- });
- },
- destroySeasonCharts: function (oldSeasonKey) {
- const chartsToDestroy = Object.keys(this.charts).filter(chartId =>
- chartId.includes(oldSeasonKey)
- );
- chartsToDestroy.forEach(chartId => this.destroyChart(chartId));
- },
- updateMainCharts: function () {
- console.log('=== updateMainCharts called ===');
- const seasonFilter = @this.get('seasonFilter');
- const monthlyTitle = document.getElementById('monthly-season-title');
- const causalsTitle = document.getElementById('causals-season-title');
- if (monthlyTitle) {
- monthlyTitle.textContent = seasonFilter;
- }
- if (causalsTitle) {
- causalsTitle.textContent = seasonFilter;
- }
- const originalSeasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
- console.log('Using original season key for canvas IDs:', originalSeasonKey);
- @this.call('getMonthlyTotals').then(monthlyData => {
- console.log('Got monthly data:', monthlyData);
- this.createMonthlyChart(originalSeasonKey, monthlyData);
- this.updateMonthlyTable(monthlyData);
- });
- @this.call('getTopCausalsByAmount').then(causalsData => {
- console.log('Got causals data:', causalsData);
- this.createCausalsChart(originalSeasonKey, causalsData);
- });
- @this.call('getTesseratiData').then(membersData => {
- console.log('Got members data:', membersData);
- this.createMembersChart(originalSeasonKey, membersData);
- this.updateMembersTable(membersData);
- });
- },
- forceUpdateCharts: function () {
- console.log('Force updating charts...');
- this.currentSeason = null;
- this.updateMainCharts();
- },
- createMonthlyChart: function (seasonKey, monthlyData) {
- const chartId = `monthly-chart-${seasonKey}`;
- const canvas = document.getElementById(chartId);
- if (!canvas) {
- console.error('Canvas not found for ID:', chartId);
- return;
- }
- this.destroyChart(chartId);
- const ctx = canvas.getContext('2d');
- const incomeGradient = ctx.createLinearGradient(0, 0, 0, 400);
- incomeGradient.addColorStop(0, 'rgba(0, 184, 148, 1)');
- incomeGradient.addColorStop(1, 'rgba(0, 184, 148, 1)');
- const expenseGradient = ctx.createLinearGradient(0, 0, 0, 400);
- expenseGradient.addColorStop(0, 'rgba(255, 107, 107, 1)');
- expenseGradient.addColorStop(1, 'rgba(255, 107, 107, 1)');
- this.charts[chartId] = new Chart(ctx, {
- type: 'bar',
- data: {
- labels: monthlyData.labels,
- datasets: [
- {
- label: 'Entrate',
- data: monthlyData.datasets[0].data,
- backgroundColor: incomeGradient,
- borderColor: '#00b894',
- borderWidth: 0,
- borderRadius: 0,
- borderSkipped: false,
- barThickness: "flex",
- barPercentage: 0.65,
- categoryPercentage: 0.4,
- },
- {
- label: 'Uscite',
- data: monthlyData.datasets[1].data,
- backgroundColor: expenseGradient,
- borderColor: '#ff6b6b',
- borderWidth: 0,
- borderRadius: 0,
- borderSkipped: false,
- barThickness: "flex",
- barPercentage: 0.65,
- categoryPercentage: 0.4,
- }
- ]
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- interaction: {
- mode: 'index',
- intersect: false,
- },
- plugins: {
- legend: {
- position: 'bottom',
- labels: {
- usePointStyle: true,
- padding: 20,
- pointStyle: "rect",
- font: { weight: '500' }
- }
- },
- tooltip: {
- backgroundColor: 'rgba(255, 255, 255, 1)',
- titleColor: '#212529',
- bodyColor: '#495057',
- borderColor: '#e9ecef',
- borderWidth: 2,
- cornerRadius: 0,
- callbacks: {
- label: function (context) {
- return context.dataset.label + ': €' +
- new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(context.parsed.y);
- }
- }
- },
- },
- scales: {
- x: {
- grid: { display: false },
- ticks: { font: { weight: '500' } }
- },
- y: {
- beginAtZero: true,
- grid: { color: 'rgba(0, 0, 0, 0.05)' },
- ticks: {
- callback: function (value) {
- return '€' + new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(value);
- }
- }
- }
- },
- elements: {
- bar: {
- borderRadius: {
- topLeft: 12,
- topRight: 12,
- bottomLeft: 0,
- bottomRight: 0
- }
- }
- },
- animation: {
- duration: 1000,
- easing: 'easeOutQuart'
- }
- }
- });
- },
- createCausalsChart: function (seasonKey, causalsData) {
- const chartId = `causals-chart-${seasonKey}`;
- const canvas = document.getElementById(chartId);
- if (!canvas) return;
- this.destroyChart(chartId);
- const ctx = canvas.getContext('2d');
- const colors = [
- 'rgba(59, 91, 219, 0.8)',
- 'rgba(0, 184, 148, 0.8)',
- 'rgba(34, 184, 207, 0.8)',
- 'rgba(255, 212, 59, 0.8)',
- 'rgba(255, 107, 107, 0.8)',
- 'rgba(142, 68, 173, 0.8)',
- 'rgba(230, 126, 34, 0.8)',
- 'rgba(149, 165, 166, 0.8)',
- 'rgba(241, 196, 15, 0.8)',
- 'rgba(231, 76, 60, 0.8)'
- ];
- const dataValues = causalsData.inData.map(item => parseFloat(item.value));
- const total = dataValues.reduce((sum, value) => sum + value, 0);
- // alwaysTooltip
- const alwaysShowTooltip = {
- id: "alwaysShowTooltip",
- afterDraw(chart) {
- const { ctx } = chart;
- ctx.save();
- const linePad = 12; // distanza dal bordo della ciambella
- const textOffset = 20; // distanza fissa del testo dal bordo
- const lineGap = 8; // margine tra linea e testo
- const lineWidth = 1;
- chart.data.datasets.forEach((dataset, di) => {
- const meta = chart.getDatasetMeta(di);
- const total = dataset.data.reduce((s, v) => s + v, 0);
- meta.data.forEach((datapoint, index) => {
- const value = dataset.data[index];
- const perc = total > 0 ? (value / total) * 100 : 0;
- const text = `${perc.toFixed(1)}%`;
- // calcolo angolo e anchor
- const { x, y } = datapoint.tooltipPosition();
- const coords = outsideLabelPoint({
- cx: datapoint.x,
- cy: datapoint.y,
- px: x,
- py: y,
- radius: datapoint.outerRadius,
- pad: linePad,
- });
- const ux = Math.cos(coords.theta);
- const uy = Math.sin(coords.theta);
- const ax = coords.anchor.x;
- const ay = coords.anchor.y;
- // centro testo
- const tx = ax + ux * textOffset;
- const ty = ay + uy * textOffset;
- // fine della linea = poco prima del testo
- const lx = tx - ux * lineGap;
- const ly = ty - uy * lineGap;
- // linea: anchor -> vicino al testo
- ctx.lineWidth = lineWidth;
- ctx.strokeStyle = datapoint.options.borderColor;
- ctx.beginPath();
- ctx.moveTo(ax, ay);
- ctx.lineTo(lx, ly);
- ctx.stroke();
- // testo
- ctx.font = "12px greycliff-cf, sans-serif";
- ctx.fillStyle = datapoint.options.borderColor;
- ctx.textAlign = "center";
- ctx.textBaseline = "middle";
- ctx.fillText(text, tx, ty);
- });
- });
- ctx.restore();
- },
- };
- function outsideLabelPoint({ cx, cy, px, py, radius, pad = 20 }) {
- const theta = Math.atan2(py - cy, px - cx); // angolo rad
- const anchor = { // punto sul bordo del pie
- x: cx + radius * Math.cos(theta),
- y: cy + radius * Math.sin(theta)
- };
- const label = { // punto esterno per il tooltip/etichetta
- x: cx + (radius + pad) * Math.cos(theta),
- y: cy + (radius + pad) * Math.sin(theta)
- };
- const align = {
- textAlign: Math.cos(theta) >= 0 ? 'left' : 'right',
- textBaseline: Math.sin(theta) > 0 ? 'top' : 'bottom'
- };
- return { theta, anchor, label, align };
- }
- this.charts[chartId] = new Chart(ctx, {
- type: 'doughnut',
- data: {
- labels: causalsData.inLabels,
- datasets: [{
- label: 'Importo',
- data: dataValues,
- backgroundColor: colors,
- borderColor: colors.map(color => color.replace('0.8', '1')),
- borderWidth: 2,
- hoverOffset: 8
- }]
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- cutout: '30%',
- layout: {
- padding: {
- top: 30,
- right: 30,
- bottom: 30,
- left: 30
- }
- },
- plugins: {
- legend: {
- display: false
- },
- tooltip: {
- backgroundColor: 'rgba(255, 255, 255, 1)',
- titleColor: '#212529',
- bodyColor: '#495057',
- borderColor: '#e9ecef',
- borderWidth: 2,
- cornerRadius: 0,
- titleFont: {
- size: 13,
- weight: 'bold'
- },
- bodyFont: {
- size: 12,
- weight: '500'
- },
- padding: 12,
- callbacks: {
- title: function (context) {
- return context[0].label;
- },
- label: function (context) {
- const value = context.raw;
- const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
- return [
- `Importo: €${new Intl.NumberFormat('it-IT', {
- minimumFractionDigits: 2,
- maximumFractionDigits: 2
- }).format(value)}`,
- `Percentuale: ${percentage}%`
- ];
- }
- }
- },
- },
- animation: {
- animateRotate: true,
- duration: 1000
- }
- },
- plugins: [alwaysShowTooltip]
- });
- this.updateCausalsTable(causalsData, dataValues, total);
- },
- updateCausalsTable: function (causalsData, dataValues, total) {
- const container = document.getElementById('causals-table');
- if (!container) return;
- const colors = [
- '#3b5bdb', '#00b894', '#22b8cf', '#ffd43b', '#ff6b6b',
- '#8e44ad', '#e67e22', '#95a5a6', '#f1c40f', '#e74c3c'
- ];
- let tableHtml = `
- <div class="causals-table compact">
- <div class="table-header">
- <div class="table-cell causale">Causale</div>
- <div class="table-cell euro">Importo</div>
- <!-- <div class="table-cell percent">%</div> -->
- </div>
- `;
- causalsData.inLabels.forEach((label, index) => {
- const value = dataValues[index] || 0;
- const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
- const color = colors[index % colors.length];
- tableHtml += `
- <div class="table-row">
- <div class="table-cell causale">
- <span class="causale-indicator" style="background-color: ${color}"></span>
- ${label}
- </div>
- <div class="table-cell euro">€${new Intl.NumberFormat('it-IT', {
- minimumFractionDigits: 2,
- maximumFractionDigits: 2
- }).format(value)}</div>
- <!-- <div class="table-cell percent">${percentage}%</div> -->
- </div>
- `;
- });
- tableHtml += '</div>';
- container.innerHTML = tableHtml;
- },
- createMembersChart: function (seasonKey, membersData) {
- const chartId = `members-chart`;
- const canvas = document.getElementById(chartId);
- if (!canvas) return;
- this.destroyChart(chartId);
- const ctx = canvas.getContext('2d');
- const gradient = ctx.createLinearGradient(0, 0, 0, 400);
- gradient.addColorStop(0, 'rgba(59, 91, 219, 0.3)');
- gradient.addColorStop(1, 'rgba(59, 91, 219, 0.05)');
- const processedDatasets = membersData.datasets.map((dataset, index) => {
- if (dataset.label === 'Totale Membri Tesserati') {
- return {
- ...dataset,
- // backgroundColor: gradient,
- backgroundColor: dataset.borderColor,
- borderColor: dataset.borderColor,
- borderWidth: 0,
- type: 'bar',
- order: 1,
- fill: true,
- barThickness: "flex",
- barPercentage: 0.65,
- categoryPercentage: 0.4,
- };
- } else {
- return {
- ...dataset,
- backgroundColor: dataset.borderColor,
- borderColor: dataset.borderColor,
- borderWidth: 0,
- type: 'bar',
- order: 2,
- fill: true,
- barThickness: "flex",
- barPercentage: 0.65,
- categoryPercentage: 0.4,
- };
- }
- });
- let options = {
- type: 'bar',
- data: {
- labels: membersData.labels,
- datasets: processedDatasets
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- interaction: {
- mode: 'index',
- intersect: false,
- },
- plugins: {
- legend: {
- position: 'bottom',
- labels: {
- usePointStyle: true,
- padding: 20,
- pointStyle: "rect",
- font: { weight: '500' }
- }
- },
- tooltip: {
- backgroundColor: 'rgba(255, 255, 255, 1)',
- titleColor: '#212529',
- bodyColor: '#495057',
- borderColor: '#e9ecef',
- borderWidth: 2,
- cornerRadius: 0,
- callbacks: {
- title: function (context) {
- return 'Stagione: ' + context[0].label;
- },
- label: function (context) {
- return context.dataset.label + ': ' + context.parsed.y;
- }
- }
- }
- },
- scales: {
- x: {
- grid: { display: false },
- ticks: {
- font: { weight: '500' }
- }
- },
- y: {
- beginAtZero: true,
- grid: { color: 'rgba(0, 0, 0, 0.05)' },
- ticks: {
- precision: 0,
- callback: function (value) {
- return Math.floor(value); // Ensure integer values
- }
- }
- }
- },
- animation: {
- duration: 1000,
- easing: 'easeOutQuart'
- }
- }
- };
- this.charts[chartId] = new Chart(ctx, options);
- },
- updateMonthlyTable: function (monthlyData) {
- const container = document.getElementById('monthly-table');
- if (!container) return;
- const incomeData = monthlyData.datasets[0].data;
- const expenseData = monthlyData.datasets[1].data;
- const monthNames = monthlyData.labels;
- let tableHtml = `
- <div class="monthly-table">
- <div class="table-header">
- <div class="table-cell month">Mese</div>
- <div class="table-cell">Entrate</div>
- <div class="table-cell">Uscite</div>
- <div class="table-cell">Delta</div>
- </div>
- `;
- monthNames.forEach((month, index) => {
- const income = parseFloat(incomeData[index] || 0);
- const expense = parseFloat(expenseData[index] || 0);
- const net = income - expense;
- const rowClass = net < 0 ? 'negative' : (net > 0 ? 'positive' : 'neutral');
- const plusSymbol = net > 0 ? "+" : "";
- tableHtml += `
- <div class="table-row ${rowClass}">
- <div class="table-cell month-name">${month}</div>
- <div class="table-cell income">€${new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(income)}</div>
- <div class="table-cell expense">€${new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(expense)}</div>
- <div class="table-cell net">€${plusSymbol}${new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(net)}</div>
- </div>
- `;
- });
- tableHtml += '</div>';
- container.innerHTML = tableHtml;
- },
- updateYearlyTable: function (yearlyData) {
- const container = document.getElementById('yearly-table');
- if (!container) return;
- const incomeData = yearlyData.datasets[0].data;
- const expenseData = yearlyData.datasets[1].data;
- const years = yearlyData.labels;
- let tableHtml = `
- <div class="monthly-table">
- <div class="table-header">
- <div class="table-cell month">Anno</div>
- <div class="table-cell">Entrate</div>
- <div class="table-cell">Uscite</div>
- <div class="table-cell">Delta</div>
- </div>
- `;
- years.forEach((year, index) => {
- const income = parseFloat(incomeData[index] || 0);
- const expense = parseFloat(expenseData[index] || 0);
- const net = income - expense;
- const rowClass = net < 0 ? 'negative' : (net > 0 ? 'positive' : 'neutral');
- tableHtml += `
- <div class="table-row ${rowClass}">
- <div class="table-cell month-name">${year}</div>
- <div class="table-cell income">€${new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(income)}</div>
- <div class="table-cell expense">€${new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(expense)}</div>
- <div class="table-cell net">€${new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(net)}</div>
- </div>
- `;
- });
- tableHtml += '</div>';
- container.innerHTML = tableHtml;
- },
- updateMembersTable: function (membersData) {
- const container = document.getElementById('members-table');
- if (!container) return;
- const seasonLabels = membersData.labels;
- const totalDataset = membersData.datasets.find(d => d.label === 'Totale Membri Tesserati');
- const cardTypeDatasets = membersData.datasets.filter(d => d.label !== 'Totale Membri Tesserati');
- const memberCounts = totalDataset ? totalDataset.data : [];
- let tableHtml = `
- <div class="members-table">
- <div class="table-header">
- <div class="table-cell">Stagione</div>
- <div class="table-cell">Totale</div>
- <div class="table-cell">Variazione</div>
- <div class="table-cell">Ente</div>
- </div>
- `;
- seasonLabels.forEach((season, index) => {
- const current = parseInt(memberCounts[index] || 0);
- const previous = index > 0 ? parseInt(memberCounts[index - 1] || 0) : 0;
- const variation = index > 0 ? current - previous : 0;
- const variationPercent = previous > 0 ? Math.round((variation / previous) * 100 * 10) / 10 : 0;
- const rowClass = variation > 0 ? 'positive' : (variation < 0 ? 'negative' : 'neutral');
- let variationText = '—';
- if (index > 0) {
- if (variation > 0) {
- variationText = `<span class="variation-positive">+${variation} (+${variationPercent}%)</span>`;
- } else if (variation < 0) {
- variationText = `<span class="variation-negative">${variation} (${variationPercent}%)</span>`;
- } else {
- variationText = `<span class="variation-neutral">${variation}</span>`;
- }
- }
- // Build card type breakdown
- let cardTypeBreakdown = '';
- cardTypeDatasets.forEach((dataset, datasetIndex) => {
- const count = dataset.data[index] || 0;
- if (count > 0) {
- const color = dataset.borderColor || '#6b7280';
- cardTypeBreakdown += `
- <div class="card-type-item">
- <span class="card-type-indicator" style="background-color: ${color}"></span>
- <span class="card-type-name">${dataset.label}</span>
- <span class="card-type-count">${count}</span>
- </div>
- `;
- }
- });
- if (!cardTypeBreakdown) {
- cardTypeBreakdown = '<div class="no-card-types">Nessun dettaglio</div>';
- }
- tableHtml += `
- <div class="table-row ${rowClass}">
- <div class="table-cell season-name">${season}</div>
- <div class="table-cell members-count">${new Intl.NumberFormat('it-IT').format(current)}</div>
- <div class="table-cell variation">${variationText}</div>
- <div class="table-cell card-types">
- <div class="card-types-container">
- ${cardTypeBreakdown}
- </div>
- </div>
- </div>
- `;
- });
- tableHtml += '</div>';
- container.innerHTML = tableHtml;
- },
- createCourseChart: function () {
- console.log('Creating course chart...');
- const seasonFilter = @this.seasonFilter;
- const selectedCourse = @this.selectedCourse ?? '';
- const seasonKey = seasonFilter.replace('-', '');
- console.log('Selected course:', selectedCourse, 'for season:', seasonFilter);
- if (!selectedCourse || selectedCourse.trim() === '') {
- console.log('No course selected, skipping chart creation');
- return;
- }
- const chartId = `courses-chart-${seasonKey}-${selectedCourse}`;
- const canvas = document.getElementById(chartId);
- if (!canvas) return;
- this.destroyChart(chartId);
- const courseData = @json($this->getCourseMonthlyEarnings());
- const ctx = canvas.getContext('2d');
- this.charts[chartId] = new Chart(ctx, {
- type: 'bar',
- data: {
- labels: courseData.labels,
- datasets: courseData.datasets.map(dataset => {
- if (dataset.type === 'line') {
- return {
- ...dataset,
- type: 'line',
- fill: false,
- backgroundColor: 'transparent'
- };
- }
- return dataset;
- })
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- interaction: {
- mode: 'index',
- intersect: false,
- },
- scales: {
- x: {
- grid: { display: false },
- ticks: { font: { weight: '500' } }
- },
- y: {
- beginAtZero: true,
- grid: {
- color: 'rgba(0, 0, 0, 1)',
- borderDash: [5, 5]
- },
- ticks: {
- callback: function (value) {
- return '€' + new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(value);
- }
- }
- }
- },
- plugins: {
- legend: {
- position: 'bottom',
- labels: {
- usePointStyle: true,
- padding: 20,
- pointStyle: "rect",
- font: { weight: '500' }
- }
- },
- tooltip: {
- backgroundColor: 'rgba(255, 255, 255, 1)',
- titleColor: '#212529',
- bodyColor: '#495057',
- borderColor: '#e9ecef',
- borderWidth: 2,
- cornerRadius: 0,
- callbacks: {
- label: function (context) {
- return context.dataset.label + ': €' +
- new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(context.parsed.y);
- }
- }
- }
- },
- animation: {
- duration: 1000,
- easing: 'easeOutQuart'
- }
- }
- });
- },
- createCourseChartWithValue: function (selectedCourseValue) {
- console.log('Creating modern course chart with value:', selectedCourseValue);
- const seasonFilter = @this.seasonFilter;
- const seasonKey = seasonFilter.replace('-', '');
- const chartId = `courses-chart-${seasonKey}-${selectedCourseValue}`;
- const tableId = `course-delta-table-${seasonKey}-${selectedCourseValue}`;
- let canvas = document.getElementById(chartId);
- const tableContainer = document.getElementById(tableId);
- if (!canvas) {
- console.log('Canvas not found for chart ID:', chartId);
- const chartContainer = document.querySelector('.modern-chart-container');
- if (chartContainer) {
- chartContainer.innerHTML = `<div class="chart-empty-state">
- <div style="text-align: center; padding: 4rem 2rem;">
- <div style="font-size: 4rem; margin-bottom: 1.5rem; opacity: 0.3;">📊</div>
- <h3 style="font-size: 1.5rem; font-weight: 600; margin-bottom: 1rem; color: #374151;">
- Grafico non disponibile
- </h3>
- <p style="font-size: 1rem; opacity: 0.7; margin: 0; max-width: 400px; margin-left: auto; margin-right: auto; line-height: 1.5;">
- Il grafico per questo corso non può essere visualizzato nella stagione selezionata.
- </p>
- </div>
- </div>`;
- }
- if (tableContainer) {
- tableContainer.innerHTML = '';
- }
- return;
- }
- this.destroyChart(chartId);
- @this.call('getCourseData', selectedCourseValue).then(courseData => {
- console.log('Received course data:', courseData);
- if (courseData.isEmpty) {
- console.log('No data available for course, showing message');
- if (tableContainer) {
- tableContainer.innerHTML = '';
- }
- const chartContainer = canvas.parentElement;
- chartContainer.innerHTML = `<div class="chart-empty-state">
- <div style="text-align: center; padding: 4rem 2rem;">
- <div style="font-size: 4rem; margin-bottom: 1.5rem; opacity: 0.3;">📊</div>
- <h3 style="font-size: 1.5rem; font-weight: 600; margin-bottom: 1rem; color: #374151;">
- ${courseData.message}
- </h3>
- <p style="font-size: 1rem; opacity: 0.7; margin: 0; max-width: 400px; margin-left: auto; margin-right: auto; line-height: 1.5;">
- Questo corso non ha pagamenti registrati per la stagione selezionata.
- </p>
- </div>
- </div>`;
- return;
- }
- if (!courseData || !courseData.labels || courseData.labels.length === 0) {
- console.log('No data available for chart');
- return;
- }
- let canvasElement = document.getElementById(chartId);
- if (!canvasElement) {
- const chartContainer = canvas.parentElement;
- chartContainer.innerHTML = `<canvas id="${chartId}"></canvas>`;
- canvasElement = document.getElementById(chartId);
- }
- this.updateCourseTable(tableContainer, courseData.tableData);
- const participantData = courseData.datasets.find(d => d.participantData)?.participantData || [];
- const suspendedData = courseData.datasets.find(d => d.suspendedData)?.suspendedData || [];
- const monthNamesExtended = courseData.datasets.find(d => d.monthNamesExtended)?.monthNamesExtended || [];
- const ctx = canvasElement.getContext('2d');
- const earnedGradient = ctx.createLinearGradient(0, 0, 0, 400);
- earnedGradient.addColorStop(0, 'rgba(16, 185, 129, 1)');
- earnedGradient.addColorStop(1, 'rgba(16, 185, 129, 1)');
- const totalData = courseData.datasets.find(d => d.label === 'TOT. DA INCASSARE')?.data || [];
- const earnedData = courseData.datasets.find(d => d.label === 'TOT. INCASSATO')?.data || [];
- this.charts[chartId] = new Chart(ctx, {
- type: 'bar',
- data: {
- labels: courseData.labels,
- datasets: [
- {
- label: 'TOT. INCASSATO',
- backgroundColor: earnedGradient,
- borderColor: 'rgba(16, 185, 129, 1)',
- borderWidth: 0,
- borderRadius: {
- topLeft: 8,
- topRight: 8,
- bottomLeft: 0,
- bottomRight: 0,
- },
- borderSkipped: true,
- data: earnedData,
- type: 'bar',
- barThickness: "flex",
- barPercentage: 0.65,
- categoryPercentage: 0.25,
- order: 1,
- participantData: participantData,
- suspendedData: suspendedData,
- monthNamesExtended: monthNamesExtended,
- },
- {
- label: 'TOT. DA INCASSARE',
- backgroundColor: '#F28322',
- borderColor: '#F28322',
- borderWidth: 0,
- borderRadius: {
- topLeft: 8,
- topRight: 8,
- bottomLeft: 0,
- bottomRight: 0,
- },
- borderSkipped: true,
- data: totalData,
- type: 'bar',
- barThickness: "flex",
- barPercentage: 0.65,
- categoryPercentage: 0.25,
- order: 2,
- participantData: participantData,
- suspendedData: suspendedData,
- monthNamesExtended: monthNamesExtended,
- }
- ]
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- interaction: {
- mode: 'index',
- intersect: false,
- },
- layout: {
- padding: {
- top: 20,
- right: 20,
- bottom: 20,
- left: 10
- }
- },
- scales: {
- x: {
- stacked: true,
- grid: {
- display: false
- },
- ticks: {
- font: {
- weight: '600',
- size: 13
- },
- color: '#6b7280'
- },
- border: {
- display: false
- }
- },
- y: {
- stacked: true,
- beginAtZero: true,
- grid: {
- color: 'rgba(156, 163, 175, 0.15)',
- borderDash: [3, 3]
- },
- border: {
- display: false
- },
- ticks: {
- font: {
- size: 12,
- weight: '500'
- },
- color: '#6b7280',
- callback: function (value) {
- return '€' + new Intl.NumberFormat('it-IT', {
- minimumFractionDigits: 0,
- maximumFractionDigits: 0
- }).format(value);
- }
- }
- }
- },
- plugins: {
- legend: {
- position: 'bottom',
- labels: {
- usePointStyle: true,
- padding: 20,
- pointStyle: "rect",
- font: { weight: '500' }
- }
- },
- tooltip: {
- backgroundColor: 'rgba(255, 255, 255, 1)',
- borderColor: 'rgba(229, 231, 235, 0.8)',
- borderWidth: 2,
- cornerRadius: 0,
- titleFont: {
- size: 15,
- weight: 'bold',
- },
- titleColor: '#111827',
- bodyFont: {
- size: 14,
- weight: '400',
- },
- bodyColor: '#111827',
- footerFont: {
- size: 14,
- weight: '400',
- },
- // footerColor: '#0C6197',
- footerSpacing: 0,
- footerMarginTop: 0,
- padding: 16,
- boxPadding: 8,
- usePointStyle: true,
- displayColors: true,
- callbacks: {
- title: function (tooltipItems) {
- let sum = 0;
- tooltipItems.forEach(function(tooltipItem) {
- sum += tooltipItem.parsed.y;
- });
- let item = tooltipItems[0];
- let index = item.dataIndex;
- let monthNameExtended = item.dataset["monthNamesExtended"] ? item.dataset["monthNamesExtended"][index] : 0;
- // return item.label + '\n' + 'TOTALE ATTESO: €' + new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(sum);
- return monthNameExtended + '\n' + 'TOTALE ATTESO: €' + new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(sum);
- },
- // labelTextColor: function(tooltipItems) {
- // return tooltipItems.dataset.backgroundColor;
- // },
- label: function (tooltipItems) {
- let label = tooltipItems.dataset.label + ': €' + new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(tooltipItems.parsed.y);
- return label;
- },
- // footer: function (tooltipItems) {
- // let item = tooltipItems[0];
- // let index = item.dataIndex;
- // let suspendedData = item.dataset["suspendedData"] ? item.dataset["suspendedData"][index] : 0;
- // return "TOTALE SOSPESI: " + suspendedData;
- // }
- }
- }
- },
- animation: {
- duration: 1500,
- easing: 'easeOutCubic'
- },
- elements: {
- bar: {
- borderRadius: {
- topLeft: 8,
- topRight: 8,
- bottomLeft: 0,
- bottomRight: 0
- }
- },
- line: {
- borderCapStyle: 'round',
- borderJoinStyle: 'round'
- },
- point: {
- hoverBorderWidth: 2,
- borderWidth: 1
- }
- }
- }
- });
- }).catch(error => {
- console.error('Error calling getCourseData:', error);
- });
- },
- updateCourseTable: function (container, tableData) {
- if (!container || !tableData) return;
- let tableHtml = `<div class="course-table">
- <div class="table-header">
- <div class="table-cell month">MESE</div>
- <div class="table-cell percentage">TOT. INCASSATO</div>
- <div class="table-cell delta">TOT. DA INCASSARE</div>
- <div class="table-cell suspended">SOSPESI</div>
- </div>`;
- tableData.forEach(row => {
- const earned = parseFloat(row.earned) || 0;
- const total = parseFloat(row.total) || 0;
- const delta = Math.max(0, total - earned);
- let percentage = 0;
- let percentageDisplay = '—';
- let percentageClass = 'neutral';
- if (total > 0) {
- percentage = Math.round((earned / total) * 100);
- percentageDisplay = percentage + '%';
- // Color based on completion
- if (percentage >= 100) {
- percentageClass = 'good';
- } else if (percentage >= 80) {
- percentageClass = 'warning';
- } else {
- percentageClass = 'bad';
- }
- }
- // Delta styling: positive when delta is 0 (fully paid), negative when there's missing amount
- const deltaClass = (total > 0 && delta === 0) ? 'positive' : (delta > 0) ? 'negative' : 'neutral';
- const earnedClass = (earned > 0) ? 'positive' : 'neutral';
- tableHtml += `
- <div class="table-row">
- <div class="table-cell month">${row.month}</div>
- <!-- <div class="table-cell percentage ${percentageClass}">${percentageDisplay}</div> -->
- <div class="table-cell earned ${earnedClass}">€${new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(earned)}</div>
- <div class="table-cell delta ${deltaClass}">€${new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(delta)}</div>
- <div class="table-cell suspended">${row.suspended}</div>
- </div>
- `;
- });
- tableHtml += '</div>';
- container.innerHTML = tableHtml;
- },
- updateCourseChart: function () {
- if (this.selectedCourse) {
- const seasonFilter = @json($seasonFilter);
- const seasonKey = seasonFilter.replace('-', '');
- this.createCourseChartWithValue(this.selectedCourse);
- }
- }
- };
- document.addEventListener('DOMContentLoaded', function () {
- setTimeout(() => {
- window.ReportsChartManager.updateMainCharts();
-
- @this.call('getYearlyTotals').then(yearlyData => {
- console.log('Got yearly data:', yearlyData);
- window.ReportsChartManager.updateYearlyTable(yearlyData);
- });
- }, 100);
- });
- document.addEventListener('livewire:navigated', function () {
- setTimeout(() => {
- window.ReportsChartManager.updateMainCharts();
- }, 100);
- });
- document.addEventListener('livewire:updated', function (event) {
- console.log('Livewire updated, waiting for component to fully update...');
- setTimeout(() => {
- console.log('Now updating charts after delay');
- window.ReportsChartManager.forceUpdateCharts();
- }, 800);
- });
- document.addEventListener('livewire:load', function () {
- Livewire.on('courseSelected', (courseId) => {
- console.log('Course selected event received:', courseId);
- setTimeout(() => {
- window.ReportsChartManager.createCourseChartWithValue(courseId);
- }, 200);
- });
- Livewire.on('chartsUpdated', () => {
- console.log('Charts updated event received from Livewire');
- setTimeout(() => {
- window.ReportsChartManager.forceUpdateCharts();
- }, 200);
- });
- });
- </script>
- </div>
|