reports.blade.php 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177
  1. {{-- resources/views/livewire/reports.blade.php --}}
  2. <div>
  3. <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  4. <link rel="stylesheet" href="{{ asset('css/chart-reports.css') }}">
  5. <div class="dashboard-container">
  6. <div class="dashboard-card controls-section">
  7. <div class="control-group">
  8. <label for="season-filter">Stagione di Riferimento:</label>
  9. <select class="form-select" wire:model="seasonFilter" wire:change="updateCharts">
  10. @foreach($this->getAvailableSeasons() as $season)
  11. <option value="{{ $season }}">{{ $season }}</option>
  12. @endforeach
  13. </select>
  14. </div>
  15. </div>
  16. @php
  17. $summary = $this->getYearlySummary();
  18. @endphp
  19. <div class="summary-cards">
  20. <div class="dashboard-card dashboard-stat card-inverted card-income">
  21. <div class="dashboard-card-header">
  22. <div class="dashboard-card-title">Entrate totali</div>
  23. <i class="dashboard-card-icon fa-solid fa-money-bill-trend-up"></i>
  24. </div>
  25. <div class="dashboard-card-value">€{{number_format($summary['totalIncome'], 2, ",", ".")}}</div>
  26. </div>
  27. <div class="dashboard-card dashboard-stat card-inverted card-expense">
  28. <div class="dashboard-card-header">
  29. <div class="dashboard-card-title">Uscite totali</div>
  30. <i class="dashboard-card-icon fa-solid fa-money-bill-transfer"></i>
  31. </div>
  32. <div class="dashboard-card-value">€{{number_format($summary['totalExpenses'], 2, ",", ".")}}</div>
  33. </div>
  34. <div class="dashboard-card dashboard-stat card-inverted card-delta">
  35. <div class="dashboard-card-header">
  36. <div class="dashboard-card-title">Bilancio netto</div>
  37. <i class="dashboard-card-icon fa-solid fa-money-bill-transfer"></i>
  38. </div>
  39. {{-- {{ $summary['delta'] < 0 ? 'negative' : '' }} --}} <div class="dashboard-card-value">€{{number_format($summary['delta'], 2, ",", ".")}}
  40. </div>
  41. </div>
  42. </div>
  43. <div wire:ignore>
  44. <div class="chart-row">
  45. <div class="dashboard-card chart-card">
  46. <div class="dashboard-card-title">Entrate e Uscite Mensili - <span id="causals-season-title">{{ $seasonFilter }}</span></div>
  47. <div class="chart-body">
  48. <div style="display: grid; grid-template-columns: 1fr 300px; align-items: start;">
  49. <div class="chart-container">
  50. <canvas id="monthly-chart-{{ str_replace('-', '', $seasonFilter) }}"></canvas>
  51. </div>
  52. <div class="monthly-table-container" id="monthly-table">
  53. </div>
  54. </div>
  55. </div>
  56. </div>
  57. </div>
  58. <div class="chart-row">
  59. <div class="dashboard-card chart-card">
  60. <div class="dashboard-card-title">Redditività per Causale - <span id="causals-season-title">{{ $seasonFilter }}</span></div>
  61. <div class="chart-body">
  62. <div style="display: grid; grid-template-columns: 1fr 700px; gap: 1rem; align-items: start;">
  63. <div class="causals-table-container" id="causals-table">
  64. </div>
  65. <div class="chart-container">
  66. <canvas id="causals-chart-{{ str_replace('-', '', $seasonFilter) }}"></canvas>
  67. </div>
  68. </div>
  69. </div>
  70. </div>
  71. </div>
  72. <div class="chart-row">
  73. <div class="dashboard-card chart-card">
  74. <div class="dashboard-card-title">Tesserati per Stagione</div>
  75. <div class="chart-body">
  76. <div style="display: grid; grid-template-columns: 1fr 500px; gap: 1rem; align-items: start;">
  77. <div class="chart-container">
  78. <canvas id="members-chart-{{ str_replace('-', '', $seasonFilter) }}"></canvas>
  79. </div>
  80. <div class="members-table-container" id="members-table">
  81. </div>
  82. </div>
  83. </div>
  84. </div>
  85. </div>
  86. </div>
  87. <div class="chart-row">
  88. <div class="dashboard-card chart-card modern-course-card">
  89. <div class="dashboard-card-title">Analisi incassi corsi</div>
  90. <div class="chart-body">
  91. <div class="course-controls">
  92. <div class="control-group">
  93. <label>Seleziona Corso ({{ $seasonFilter }}):</label>
  94. <select class="form-select modern-select" wire:model.live="selectedCourse">
  95. <option value="">Seleziona un Corso</option>
  96. @foreach($this->getCoursesForSelect() as $course)
  97. <option value="{{ $course['id'] }}">{{ $course['full_name'] }}</option>
  98. @endforeach
  99. </select>
  100. </div>
  101. </div>
  102. @if($selectedCourse)
  103. <div wire:ignore wire:key="course-chart-{{ $seasonFilter }}-{{ $selectedCourse }}">
  104. <div class="modern-chart-layout">
  105. <div class="course-delta-table" id="course-delta-table-{{ str_replace('-', '', $seasonFilter) }}-{{ $selectedCourse }}">
  106. </div>
  107. <div class="modern-chart-container">
  108. <canvas id="courses-chart-{{ str_replace('-', '', $seasonFilter) }}-{{ $selectedCourse }}"></canvas>
  109. </div>
  110. </div>
  111. </div>
  112. @else
  113. <div class="chart-placeholder">
  114. <div style="text-align: center;">
  115. <div style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.3;">📊</div>
  116. <p style="font-size: 1.25rem; font-weight: 600; margin: 0;">Seleziona un corso per
  117. visualizzare il grafico</p>
  118. <p style="font-size: 1rem; opacity: 0.7; margin-top: 0.5rem;">Usa il menu a tendina sopra
  119. per scegliere un corso</p>
  120. </div>
  121. </div>
  122. @endif
  123. </div>
  124. </div>
  125. </div>
  126. </div>
  127. <script>
  128. window.ReportsChartManager = window.ReportsChartManager || {
  129. charts: {},
  130. currentSeason: null,
  131. destroyChart: function (chartId) {
  132. if (this.charts[chartId]) {
  133. this.charts[chartId].destroy();
  134. delete this.charts[chartId];
  135. }
  136. },
  137. destroyAllCharts: function () {
  138. Object.keys(this.charts).forEach(chartId => {
  139. this.destroyChart(chartId);
  140. });
  141. },
  142. destroySeasonCharts: function (oldSeasonKey) {
  143. const chartsToDestroy = Object.keys(this.charts).filter(chartId =>
  144. chartId.includes(oldSeasonKey)
  145. );
  146. chartsToDestroy.forEach(chartId => this.destroyChart(chartId));
  147. },
  148. updateMainCharts: function () {
  149. console.log('=== updateMainCharts called ===');
  150. const seasonFilter = @this.get('seasonFilter');
  151. const monthlyTitle = document.getElementById('monthly-season-title');
  152. const causalsTitle = document.getElementById('causals-season-title');
  153. if (monthlyTitle) {
  154. monthlyTitle.textContent = seasonFilter;
  155. }
  156. if (causalsTitle) {
  157. causalsTitle.textContent = seasonFilter;
  158. }
  159. const originalSeasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
  160. console.log('Using original season key for canvas IDs:', originalSeasonKey);
  161. @this.call('getMonthlyTotals').then(monthlyData => {
  162. console.log('Got monthly data:', monthlyData);
  163. this.createMonthlyChart(originalSeasonKey, monthlyData);
  164. this.updateMonthlyTable(monthlyData);
  165. });
  166. @this.call('getTopCausalsByAmount').then(causalsData => {
  167. console.log('Got causals data:', causalsData);
  168. this.createCausalsChart(originalSeasonKey, causalsData);
  169. });
  170. @this.call('getTesseratiData').then(membersData => {
  171. console.log('Got members data:', membersData);
  172. this.createMembersChart(originalSeasonKey, membersData);
  173. this.updateMembersTable(membersData);
  174. });
  175. },
  176. forceUpdateCharts: function () {
  177. console.log('Force updating charts...');
  178. this.currentSeason = null;
  179. this.updateMainCharts();
  180. },
  181. createMonthlyChart: function (seasonKey, monthlyData) {
  182. const chartId = `monthly-chart-${seasonKey}`;
  183. const canvas = document.getElementById(chartId);
  184. if (!canvas) {
  185. console.error('Canvas not found for ID:', chartId);
  186. return;
  187. }
  188. this.destroyChart(chartId);
  189. const ctx = canvas.getContext('2d');
  190. const incomeGradient = ctx.createLinearGradient(0, 0, 0, 400);
  191. incomeGradient.addColorStop(0, 'rgba(0, 184, 148, 1)');
  192. incomeGradient.addColorStop(1, 'rgba(0, 184, 148, 1)');
  193. const expenseGradient = ctx.createLinearGradient(0, 0, 0, 400);
  194. expenseGradient.addColorStop(0, 'rgba(255, 107, 107, 1)');
  195. expenseGradient.addColorStop(1, 'rgba(255, 107, 107, 1)');
  196. this.charts[chartId] = new Chart(ctx, {
  197. type: 'bar',
  198. data: {
  199. labels: monthlyData.labels,
  200. datasets: [
  201. {
  202. label: 'Entrate',
  203. data: monthlyData.datasets[0].data,
  204. backgroundColor: incomeGradient,
  205. borderColor: '#00b894',
  206. borderWidth: 0,
  207. borderRadius: {
  208. topLeft: 8,
  209. topRight: 8,
  210. bottomLeft: 0,
  211. bottomRight: 0,
  212. },
  213. borderSkipped: false,
  214. barThickness: "flex",
  215. barPercentage: 0.65,
  216. categoryPercentage: 0.4,
  217. },
  218. {
  219. label: 'Uscite',
  220. data: monthlyData.datasets[1].data,
  221. backgroundColor: expenseGradient,
  222. borderColor: '#ff6b6b',
  223. borderWidth: 0,
  224. borderRadius: {
  225. topLeft: 8,
  226. topRight: 8,
  227. bottomLeft: 0,
  228. bottomRight: 0,
  229. },
  230. borderSkipped: false,
  231. barThickness: "flex",
  232. barPercentage: 0.65,
  233. categoryPercentage: 0.4,
  234. }
  235. ]
  236. },
  237. options: {
  238. responsive: true,
  239. maintainAspectRatio: false,
  240. interaction: {
  241. mode: 'index',
  242. intersect: false,
  243. },
  244. plugins: {
  245. legend: {
  246. position: 'bottom',
  247. labels: {
  248. usePointStyle: true,
  249. padding: 20,
  250. pointStyle: "rect",
  251. font: { weight: '500' }
  252. }
  253. },
  254. tooltip: {
  255. backgroundColor: 'rgba(255, 255, 255, 1)',
  256. titleColor: '#212529',
  257. bodyColor: '#495057',
  258. borderColor: '#e9ecef',
  259. borderWidth: 2,
  260. cornerRadius: 0,
  261. callbacks: {
  262. label: function (context) {
  263. return context.dataset.label + ': €' +
  264. new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(context.parsed.y);
  265. }
  266. }
  267. },
  268. },
  269. scales: {
  270. x: {
  271. grid: { display: false },
  272. ticks: { font: { weight: '500' } }
  273. },
  274. y: {
  275. beginAtZero: true,
  276. grid: { color: 'rgba(0, 0, 0, 0.05)' },
  277. ticks: {
  278. callback: function (value) {
  279. return '€' + new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(value);
  280. }
  281. }
  282. }
  283. },
  284. elements: {
  285. bar: {
  286. borderRadius: {
  287. topLeft: 8,
  288. topRight: 8,
  289. bottomLeft: 0,
  290. bottomRight: 0
  291. }
  292. }
  293. },
  294. animation: {
  295. duration: 1000,
  296. easing: 'easeOutQuart'
  297. }
  298. }
  299. });
  300. },
  301. createCausalsChart: function (seasonKey, causalsData) {
  302. const chartId = `causals-chart-${seasonKey}`;
  303. const canvas = document.getElementById(chartId);
  304. if (!canvas) return;
  305. this.destroyChart(chartId);
  306. const ctx = canvas.getContext('2d');
  307. const colors = [
  308. 'rgba(59, 91, 219, 0.8)',
  309. 'rgba(0, 184, 148, 0.8)',
  310. 'rgba(34, 184, 207, 0.8)',
  311. 'rgba(255, 212, 59, 0.8)',
  312. 'rgba(255, 107, 107, 0.8)',
  313. 'rgba(142, 68, 173, 0.8)',
  314. 'rgba(230, 126, 34, 0.8)',
  315. 'rgba(149, 165, 166, 0.8)',
  316. 'rgba(241, 196, 15, 0.8)',
  317. 'rgba(231, 76, 60, 0.8)'
  318. ];
  319. const dataValues = causalsData.inData.map(item => parseFloat(item.value));
  320. const total = dataValues.reduce((sum, value) => sum + value, 0);
  321. this.charts[chartId] = new Chart(ctx, {
  322. type: 'doughnut',
  323. data: {
  324. labels: causalsData.inLabels,
  325. datasets: [{
  326. label: 'Importo',
  327. data: dataValues,
  328. backgroundColor: colors,
  329. borderColor: colors.map(color => color.replace('0.8', '1')),
  330. borderWidth: 2,
  331. hoverOffset: 8
  332. }]
  333. },
  334. options: {
  335. responsive: true,
  336. maintainAspectRatio: false,
  337. cutout: '30%',
  338. layout: {
  339. padding: {
  340. top: 30,
  341. right: 30,
  342. bottom: 30,
  343. left: 30
  344. }
  345. },
  346. plugins: {
  347. legend: {
  348. display: false
  349. },
  350. tooltip: {
  351. backgroundColor: 'rgba(255, 255, 255, 1)',
  352. titleColor: '#212529',
  353. bodyColor: '#495057',
  354. borderColor: '#e9ecef',
  355. borderWidth: 2,
  356. cornerRadius: 0,
  357. titleFont: {
  358. size: 13,
  359. weight: 'bold'
  360. },
  361. bodyFont: {
  362. size: 12,
  363. weight: '500'
  364. },
  365. padding: 12,
  366. callbacks: {
  367. title: function (context) {
  368. return context[0].label;
  369. },
  370. label: function (context) {
  371. const value = context.raw;
  372. const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
  373. return [
  374. `Importo: €${new Intl.NumberFormat('it-IT', {
  375. minimumFractionDigits: 2,
  376. maximumFractionDigits: 2
  377. }).format(value)}`,
  378. `Percentuale: ${percentage}%`
  379. ];
  380. }
  381. }
  382. },
  383. },
  384. animation: {
  385. animateRotate: true,
  386. duration: 1000
  387. }
  388. }
  389. });
  390. this.updateCausalsTable(causalsData, dataValues, total);
  391. },
  392. updateCausalsTable: function (causalsData, dataValues, total) {
  393. const container = document.getElementById('causals-table');
  394. if (!container) return;
  395. const colors = [
  396. '#3b5bdb', '#00b894', '#22b8cf', '#ffd43b', '#ff6b6b',
  397. '#8e44ad', '#e67e22', '#95a5a6', '#f1c40f', '#e74c3c'
  398. ];
  399. let tableHtml = `<div class="causals-table compact">
  400. <div class="table-header">
  401. <div class="table-cell causale">Causale</div>
  402. <div class="table-cell euro">Importo</div>
  403. <div class="table-cell percent">%</div>
  404. </div>`;
  405. if (causalsData.inLabels.length) {
  406. causalsData.inLabels.forEach((label, index) => {
  407. const value = dataValues[index] || 0;
  408. const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
  409. const color = colors[index % colors.length];
  410. tableHtml += `<div class="table-row">
  411. <div class="table-cell causale">
  412. <span class="causale-indicator" style="background-color: ${color}"></span>
  413. ${label}
  414. </div>
  415. <div class="table-cell euro">€${new Intl.NumberFormat('it-IT', {
  416. minimumFractionDigits: 2,
  417. maximumFractionDigits: 2
  418. }).format(value)}</div>
  419. <div class="table-cell percent">${percentage}%</div>
  420. </div>`;
  421. });
  422. } else {
  423. tableHtml += `<div class="table-row">
  424. <p>Nessun dato disponibile</p>
  425. </div>`;
  426. }
  427. tableHtml += '</div>';
  428. container.innerHTML = tableHtml;
  429. },
  430. createMembersChart: function (seasonKey, membersData) {
  431. const chartId = `members-chart-${seasonKey}`;
  432. const canvas = document.getElementById(chartId);
  433. if (!canvas) return;
  434. this.destroyChart(chartId);
  435. const ctx = canvas.getContext('2d');
  436. const gradient = ctx.createLinearGradient(0, 0, 0, 400);
  437. gradient.addColorStop(0, 'rgba(59, 91, 219, 0.3)');
  438. gradient.addColorStop(1, 'rgba(59, 91, 219, 0.05)');
  439. const processedDatasets = membersData.datasets.map((dataset, index) => {
  440. if (dataset.label === 'Totale Membri Tesserati') {
  441. return {
  442. ...dataset,
  443. borderColor: '#8979ff',
  444. borderWidth: 2,
  445. pointBackgroundColor: '#ffffff',
  446. pointBorderColor: '#8979ff',
  447. pointBorderWidth: 2,
  448. pointRadius: 3,
  449. pointHoverRadius: 3,
  450. type: 'line',
  451. order: 1,
  452. backgroundColor: "#8979ff40",
  453. fill: true
  454. };
  455. } else {
  456. return {
  457. ...dataset,
  458. backgroundColor: dataset.backgroundColor,
  459. borderWidth: 2,
  460. pointRadius: 3,
  461. pointHoverRadius: 3,
  462. pointBackgroundColor: '#ffffff',
  463. pointBorderWidth: 2,
  464. type: 'line',
  465. order: 2,
  466. fill: true
  467. };
  468. }
  469. });
  470. this.charts[chartId] = new Chart(ctx, {
  471. type: 'line',
  472. data: {
  473. labels: membersData.labels,
  474. datasets: processedDatasets
  475. },
  476. options: {
  477. responsive: true,
  478. maintainAspectRatio: false,
  479. interaction: {
  480. mode: 'index',
  481. intersect: false,
  482. },
  483. plugins: {
  484. legend: {
  485. position: 'bottom',
  486. labels: {
  487. usePointStyle: true,
  488. padding: 20,
  489. pointStyle: "rect",
  490. font: { weight: '500' }
  491. }
  492. },
  493. tooltip: {
  494. backgroundColor: 'rgba(255, 255, 255, 1)',
  495. titleColor: '#212529',
  496. bodyColor: '#495057',
  497. borderColor: '#e9ecef',
  498. borderWidth: 2,
  499. cornerRadius: 0,
  500. callbacks: {
  501. title: function (context) {
  502. return 'Stagione: ' + context[0].label;
  503. },
  504. label: function (context) {
  505. return context.dataset.label + ': ' + context.parsed.y;
  506. }
  507. }
  508. }
  509. },
  510. scales: {
  511. x: {
  512. grid: { display: false },
  513. ticks: {
  514. font: { weight: '500' }
  515. }
  516. },
  517. y: {
  518. beginAtZero: true,
  519. grid: { color: 'rgba(0, 0, 0, 0.05)' },
  520. ticks: {
  521. precision: 0,
  522. callback: function (value) {
  523. return Math.floor(value); // Ensure integer values
  524. }
  525. }
  526. }
  527. },
  528. animation: {
  529. duration: 1000,
  530. easing: 'easeOutQuart'
  531. }
  532. }
  533. });
  534. },
  535. updateMonthlyTable: function (monthlyData) {
  536. const container = document.getElementById('monthly-table');
  537. if (!container) return;
  538. const incomeData = monthlyData.datasets[0].data;
  539. const expenseData = monthlyData.datasets[1].data;
  540. const monthNames = monthlyData.labels;
  541. let tableHtml = `
  542. <div class="monthly-table">
  543. <div class="table-header">
  544. <div class="table-cell month">Mese</div>
  545. <div class="table-cell">Entrate</div>
  546. <div class="table-cell">Uscite</div>
  547. <div class="table-cell">Delta</div>
  548. </div>
  549. `;
  550. monthNames.forEach((month, index) => {
  551. const income = parseFloat(incomeData[index] || 0);
  552. const expense = parseFloat(expenseData[index] || 0);
  553. const net = income - expense;
  554. const rowClass = net < 0 ? 'negative' : (net > 0 ? 'positive' : 'neutral');
  555. tableHtml += `
  556. <div class="table-row ${rowClass}">
  557. <div class="table-cell month-name">${month}</div>
  558. <div class="table-cell income">€${new Intl.NumberFormat('it-IT').format(income)}</div>
  559. <div class="table-cell expense">€${new Intl.NumberFormat('it-IT').format(expense)}</div>
  560. <div class="table-cell net">€${new Intl.NumberFormat('it-IT').format(net)}</div>
  561. </div>
  562. `;
  563. });
  564. tableHtml += '</div>';
  565. container.innerHTML = tableHtml;
  566. },
  567. updateMembersTable: function (membersData) {
  568. const container = document.getElementById('members-table');
  569. if (!container) return;
  570. const seasonLabels = membersData.labels;
  571. const totalDataset = membersData.datasets.find(d => d.label === 'Totale Membri Tesserati');
  572. const cardTypeDatasets = membersData.datasets.filter(d => d.label !== 'Totale Membri Tesserati');
  573. const memberCounts = totalDataset ? totalDataset.data : [];
  574. let tableHtml = `
  575. <div class="members-table">
  576. <div class="table-header">
  577. <div class="table-cell">Stagione</div>
  578. <div class="table-cell">Totale</div>
  579. <div class="table-cell">Variazione</div>
  580. <div class="table-cell">Tipologie</div>
  581. </div>
  582. `;
  583. seasonLabels.forEach((season, index) => {
  584. const current = parseInt(memberCounts[index] || 0);
  585. const previous = index > 0 ? parseInt(memberCounts[index - 1] || 0) : 0;
  586. const variation = index > 0 ? current - previous : 0;
  587. const variationPercent = previous > 0 ? Math.round((variation / previous) * 100 * 10) / 10 : 0;
  588. const rowClass = variation > 0 ? 'positive' : (variation < 0 ? 'negative' : 'neutral');
  589. let variationText = '—';
  590. if (index > 0) {
  591. if (variation > 0) {
  592. variationText = `<span class="variation-positive">+${variation} (+${variationPercent}%)</span>`;
  593. } else if (variation < 0) {
  594. variationText = `<span class="variation-negative">${variation} (${variationPercent}%)</span>`;
  595. } else {
  596. variationText = `<span class="variation-neutral">${variation}</span>`;
  597. }
  598. }
  599. // Build card type breakdown
  600. let cardTypeBreakdown = '';
  601. cardTypeDatasets.forEach((dataset, datasetIndex) => {
  602. const count = dataset.data[index] || 0;
  603. if (count > 0) {
  604. const color = dataset.borderColor || '#6b7280';
  605. cardTypeBreakdown += `
  606. <div class="card-type-item">
  607. <span class="card-type-indicator" style="background-color: ${color}"></span>
  608. <span class="card-type-name">${dataset.label}</span>
  609. <span class="card-type-count">${count}</span>
  610. </div>
  611. `;
  612. }
  613. });
  614. if (!cardTypeBreakdown) {
  615. cardTypeBreakdown = '<div class="no-card-types">Nessun dettaglio</div>';
  616. }
  617. tableHtml += `
  618. <div class="table-row ${rowClass}">
  619. <div class="table-cell season-name">${season}</div>
  620. <div class="table-cell members-count">${new Intl.NumberFormat('it-IT').format(current)}</div>
  621. <div class="table-cell variation">${variationText}</div>
  622. <div class="table-cell card-types">
  623. <div class="card-types-container">
  624. ${cardTypeBreakdown}
  625. </div>
  626. </div>
  627. </div>
  628. `;
  629. });
  630. tableHtml += '</div>';
  631. container.innerHTML = tableHtml;
  632. },
  633. createCourseChart: function () {
  634. console.log('Creating course chart...');
  635. const seasonFilter = @this.seasonFilter;
  636. const selectedCourse = @this.selectedCourse ?? '';
  637. const seasonKey = seasonFilter.replace('-', '');
  638. console.log('Selected course:', selectedCourse, 'for season:', seasonFilter);
  639. if (!selectedCourse || selectedCourse.trim() === '') {
  640. console.log('No course selected, skipping chart creation');
  641. return;
  642. }
  643. const chartId = `courses-chart-${seasonKey}-${selectedCourse}`;
  644. const canvas = document.getElementById(chartId);
  645. if (!canvas) return;
  646. this.destroyChart(chartId);
  647. const courseData = @json($this->getCourseMonthlyEarnings());
  648. const ctx = canvas.getContext('2d');
  649. this.charts[chartId] = new Chart(ctx, {
  650. type: 'bar',
  651. data: {
  652. labels: courseData.labels,
  653. datasets: courseData.datasets.map(dataset => {
  654. if (dataset.type === 'line') {
  655. return {
  656. ...dataset,
  657. type: 'line',
  658. fill: false,
  659. backgroundColor: 'transparent'
  660. };
  661. }
  662. return dataset;
  663. })
  664. },
  665. options: {
  666. responsive: true,
  667. maintainAspectRatio: false,
  668. interaction: {
  669. mode: 'index',
  670. intersect: false,
  671. },
  672. scales: {
  673. x: {
  674. grid: { display: false },
  675. ticks: { font: { weight: '500' } }
  676. },
  677. y: {
  678. beginAtZero: true,
  679. grid: {
  680. color: 'rgba(0, 0, 0, 1)',
  681. borderDash: [5, 5]
  682. },
  683. ticks: {
  684. callback: function (value) {
  685. return '€' + new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(value);
  686. }
  687. }
  688. }
  689. },
  690. plugins: {
  691. legend: {
  692. position: 'bottom',
  693. labels: {
  694. usePointStyle: true,
  695. padding: 20,
  696. pointStyle: "rect",
  697. font: { weight: '500' }
  698. }
  699. },
  700. tooltip: {
  701. backgroundColor: 'rgba(255, 255, 255, 1)',
  702. titleColor: '#212529',
  703. bodyColor: '#495057',
  704. borderColor: '#e9ecef',
  705. borderWidth: 2,
  706. cornerRadius: 0,
  707. callbacks: {
  708. label: function (context) {
  709. return context.dataset.label + ': €' +
  710. new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(context.parsed.y);
  711. }
  712. }
  713. }
  714. },
  715. animation: {
  716. duration: 1000,
  717. easing: 'easeOutQuart'
  718. }
  719. }
  720. });
  721. },
  722. createCourseChartWithValue: function (selectedCourseValue) {
  723. console.log('Creating modern course chart with value:', selectedCourseValue);
  724. const seasonFilter = @this.seasonFilter;
  725. const seasonKey = seasonFilter.replace('-', '');
  726. const chartId = `courses-chart-${seasonKey}-${selectedCourseValue}`;
  727. const tableId = `course-delta-table-${seasonKey}-${selectedCourseValue}`;
  728. let canvas = document.getElementById(chartId);
  729. const tableContainer = document.getElementById(tableId);
  730. if (!canvas) {
  731. console.log('Canvas not found for chart ID:', chartId);
  732. const chartContainer = document.querySelector('.modern-chart-container');
  733. if (chartContainer) {
  734. chartContainer.innerHTML = `<div class="chart-empty-state">
  735. <div style="text-align: center; padding: 4rem 2rem;">
  736. <div style="font-size: 4rem; margin-bottom: 1.5rem; opacity: 0.3;">📊</div>
  737. <h3 style="font-size: 1.5rem; font-weight: 600; margin-bottom: 1rem; color: #374151;">
  738. Grafico non disponibile
  739. </h3>
  740. <p style="font-size: 1rem; opacity: 0.7; margin: 0; max-width: 400px; margin-left: auto; margin-right: auto; line-height: 1.5;">
  741. Il grafico per questo corso non può essere visualizzato nella stagione selezionata.
  742. </p>
  743. </div>
  744. </div>`;
  745. }
  746. if (tableContainer) {
  747. tableContainer.innerHTML = '';
  748. }
  749. return;
  750. }
  751. this.destroyChart(chartId);
  752. @this.call('getCourseData', selectedCourseValue).then(courseData => {
  753. console.log('Received course data:', courseData);
  754. if (courseData.isEmpty) {
  755. console.log('No data available for course, showing message');
  756. if (tableContainer) {
  757. tableContainer.innerHTML = '';
  758. }
  759. const chartContainer = canvas.parentElement;
  760. chartContainer.innerHTML = `<div class="chart-empty-state">
  761. <div style="text-align: center; padding: 4rem 2rem;">
  762. <div style="font-size: 4rem; margin-bottom: 1.5rem; opacity: 0.3;">📊</div>
  763. <h3 style="font-size: 1.5rem; font-weight: 600; margin-bottom: 1rem; color: #374151;">
  764. ${courseData.message}
  765. </h3>
  766. <p style="font-size: 1rem; opacity: 0.7; margin: 0; max-width: 400px; margin-left: auto; margin-right: auto; line-height: 1.5;">
  767. Questo corso non ha pagamenti registrati per la stagione selezionata.
  768. </p>
  769. </div>
  770. </div>`;
  771. return;
  772. }
  773. if (!courseData || !courseData.labels || courseData.labels.length === 0) {
  774. console.log('No data available for chart');
  775. return;
  776. }
  777. let canvasElement = document.getElementById(chartId);
  778. if (!canvasElement) {
  779. const chartContainer = canvas.parentElement;
  780. chartContainer.innerHTML = `<canvas id="${chartId}"></canvas>`;
  781. canvasElement = document.getElementById(chartId);
  782. }
  783. this.updateCourseTable(tableContainer, courseData.tableData);
  784. const participantData = courseData.datasets.find(d => d.participantData)?.participantData || [];
  785. const suspendedData = courseData.datasets.find(d => d.suspendedData)?.suspendedData || [];
  786. const monthNamesExtended = courseData.datasets.find(d => d.monthNamesExtended)?.monthNamesExtended || [];
  787. const ctx = canvasElement.getContext('2d');
  788. const earnedGradient = ctx.createLinearGradient(0, 0, 0, 400);
  789. earnedGradient.addColorStop(0, 'rgba(16, 185, 129, 1)');
  790. earnedGradient.addColorStop(1, 'rgba(16, 185, 129, 1)');
  791. const totalData = courseData.datasets.find(d => d.label === 'TOT. DA INCASSARE')?.data || [];
  792. const earnedData = courseData.datasets.find(d => d.label === 'TOT. INCASSATO')?.data || [];
  793. this.charts[chartId] = new Chart(ctx, {
  794. type: 'bar',
  795. data: {
  796. labels: courseData.labels,
  797. datasets: [
  798. {
  799. label: 'TOT. INCASSATO',
  800. backgroundColor: earnedGradient,
  801. borderColor: 'rgba(16, 185, 129, 1)',
  802. borderWidth: 0,
  803. borderRadius: {
  804. topLeft: 8,
  805. topRight: 8,
  806. bottomLeft: 0,
  807. bottomRight: 0,
  808. },
  809. // borderSkipped: true,
  810. data: earnedData,
  811. type: 'bar',
  812. barThickness: "flex",
  813. barPercentage: 0.50,
  814. categoryPercentage: 0.25,
  815. order: 1,
  816. participantData: participantData,
  817. suspendedData: suspendedData,
  818. monthNamesExtended: monthNamesExtended,
  819. },
  820. {
  821. label: 'TOT. DA INCASSARE',
  822. backgroundColor: '#F28322',
  823. borderColor: '#F28322',
  824. borderWidth: 0,
  825. borderRadius: {
  826. topLeft: 8,
  827. topRight: 8,
  828. bottomLeft: 0,
  829. bottomRight: 0,
  830. },
  831. // borderSkipped: true,
  832. data: totalData,
  833. type: 'bar',
  834. barThickness: "flex",
  835. barPercentage: 0.50,
  836. categoryPercentage: 0.25,
  837. order: 2,
  838. participantData: participantData,
  839. suspendedData: suspendedData,
  840. monthNamesExtended: monthNamesExtended,
  841. }
  842. ]
  843. },
  844. options: {
  845. responsive: true,
  846. maintainAspectRatio: false,
  847. interaction: {
  848. mode: 'index',
  849. intersect: false,
  850. },
  851. layout: {
  852. padding: {
  853. top: 20,
  854. right: 20,
  855. bottom: 20,
  856. left: 10
  857. }
  858. },
  859. scales: {
  860. x: {
  861. stacked: true,
  862. grid: {
  863. display: false
  864. },
  865. ticks: {
  866. font: {
  867. weight: '600',
  868. size: 13
  869. },
  870. color: '#6b7280'
  871. },
  872. border: {
  873. display: false
  874. }
  875. },
  876. y: {
  877. stacked: true,
  878. beginAtZero: true,
  879. grid: {
  880. color: 'rgba(156, 163, 175, 0.15)',
  881. borderDash: [3, 3]
  882. },
  883. border: {
  884. display: false
  885. },
  886. ticks: {
  887. font: {
  888. size: 12,
  889. weight: '500'
  890. },
  891. color: '#6b7280',
  892. callback: function (value) {
  893. return '€' + new Intl.NumberFormat('it-IT', {
  894. minimumFractionDigits: 0,
  895. maximumFractionDigits: 0
  896. }).format(value);
  897. }
  898. }
  899. }
  900. },
  901. plugins: {
  902. legend: {
  903. position: 'bottom',
  904. labels: {
  905. usePointStyle: true,
  906. padding: 20,
  907. pointStyle: "rect",
  908. font: { weight: '500' }
  909. }
  910. },
  911. tooltip: {
  912. backgroundColor: 'rgba(255, 255, 255, 1)',
  913. borderColor: 'rgba(229, 231, 235, 0.8)',
  914. borderWidth: 2,
  915. cornerRadius: 0,
  916. titleFont: {
  917. size: 15,
  918. weight: 'bold',
  919. },
  920. titleColor: '#111827',
  921. bodyFont: {
  922. size: 14,
  923. weight: '400',
  924. },
  925. bodyColor: '#111827',
  926. footerFont: {
  927. size: 14,
  928. weight: '400',
  929. },
  930. // footerColor: '#0C6197',
  931. footerSpacing: 0,
  932. footerMarginTop: 0,
  933. padding: 16,
  934. boxPadding: 8,
  935. usePointStyle: true,
  936. displayColors: true,
  937. callbacks: {
  938. title: function (tooltipItems) {
  939. let sum = 0;
  940. tooltipItems.forEach(function(tooltipItem) {
  941. sum += tooltipItem.parsed.y;
  942. });
  943. let item = tooltipItems[0];
  944. let index = item.dataIndex;
  945. let monthNameExtended = item.dataset["monthNamesExtended"] ? item.dataset["monthNamesExtended"][index] : 0;
  946. // return item.label + '\n' + 'TOTALE ATTESO: €' + new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(sum);
  947. return monthNameExtended + '\n' + 'TOTALE ATTESO: €' + new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(sum);
  948. },
  949. // labelTextColor: function(tooltipItems) {
  950. // return tooltipItems.dataset.backgroundColor;
  951. // },
  952. label: function (tooltipItems) {
  953. let label = tooltipItems.dataset.label + ': €' + new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(tooltipItems.parsed.y);
  954. return label;
  955. },
  956. // footer: function (tooltipItems) {
  957. // let item = tooltipItems[0];
  958. // let index = item.dataIndex;
  959. // let suspendedData = item.dataset["suspendedData"] ? item.dataset["suspendedData"][index] : 0;
  960. // return "TOTALE SOSPESI: " + suspendedData;
  961. // }
  962. }
  963. }
  964. },
  965. animation: {
  966. duration: 1500,
  967. easing: 'easeOutCubic'
  968. },
  969. elements: {
  970. bar: {
  971. borderRadius: {
  972. topLeft: 8,
  973. topRight: 8,
  974. bottomLeft: 0,
  975. bottomRight: 0
  976. }
  977. },
  978. line: {
  979. borderCapStyle: 'round',
  980. borderJoinStyle: 'round'
  981. },
  982. point: {
  983. hoverBorderWidth: 2,
  984. borderWidth: 1
  985. }
  986. }
  987. }
  988. });
  989. }).catch(error => {
  990. console.error('Error calling getCourseData:', error);
  991. });
  992. },
  993. updateCourseTable: function (container, tableData) {
  994. if (!container || !tableData) return;
  995. let tableHtml = `<div class="course-table">
  996. <div class="table-header">
  997. <div class="table-cell month">MESE</div>
  998. <div class="table-cell percentage">TOT. INCASSATO</div>
  999. <div class="table-cell delta">TOT. DA INCASSARE</div>
  1000. <div class="table-cell suspended">SOSPESI</div>
  1001. </div>`;
  1002. tableData.forEach(row => {
  1003. const earned = parseFloat(row.earned) || 0;
  1004. const total = parseFloat(row.total) || 0;
  1005. const delta = Math.max(0, total - earned);
  1006. let percentage = 0;
  1007. let percentageDisplay = '—';
  1008. let percentageClass = 'neutral';
  1009. if (total > 0) {
  1010. percentage = Math.round((earned / total) * 100);
  1011. percentageDisplay = percentage + '%';
  1012. // Color based on completion
  1013. if (percentage >= 100) {
  1014. percentageClass = 'good';
  1015. } else if (percentage >= 80) {
  1016. percentageClass = 'warning';
  1017. } else {
  1018. percentageClass = 'bad';
  1019. }
  1020. }
  1021. // Delta styling: positive when delta is 0 (fully paid), negative when there's missing amount
  1022. const deltaClass = (total > 0 && delta === 0) ? 'positive' : (delta > 0) ? 'negative' : 'neutral';
  1023. const earnedClass = (earned > 0) ? 'positive' : 'neutral';
  1024. tableHtml += `
  1025. <div class="table-row">
  1026. <div class="table-cell month">${row.month}</div>
  1027. <!-- <div class="table-cell percentage ${percentageClass}">${percentageDisplay}</div> -->
  1028. <div class="table-cell earned ${earnedClass}">€${new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(earned)}</div>
  1029. <div class="table-cell delta ${deltaClass}">€${new Intl.NumberFormat('it-IT', {minimumFractionDigits: 2, maximumFractionDigits: 2}).format(delta)}</div>
  1030. <div class="table-cell suspended">${row.suspended}</div>
  1031. </div>
  1032. `;
  1033. });
  1034. tableHtml += '</div>';
  1035. container.innerHTML = tableHtml;
  1036. },
  1037. updateCourseChart: function () {
  1038. if (this.selectedCourse) {
  1039. const seasonFilter = @json($seasonFilter);
  1040. const seasonKey = seasonFilter.replace('-', '');
  1041. this.createCourseChartWithValue(this.selectedCourse);
  1042. }
  1043. }
  1044. };
  1045. document.addEventListener('DOMContentLoaded', function () {
  1046. setTimeout(() => {
  1047. window.ReportsChartManager.updateMainCharts();
  1048. }, 100);
  1049. });
  1050. document.addEventListener('livewire:navigated', function () {
  1051. setTimeout(() => {
  1052. window.ReportsChartManager.updateMainCharts();
  1053. }, 100);
  1054. });
  1055. document.addEventListener('livewire:updated', function (event) {
  1056. console.log('Livewire updated, waiting for component to fully update...');
  1057. setTimeout(() => {
  1058. console.log('Now updating charts after delay');
  1059. window.ReportsChartManager.forceUpdateCharts();
  1060. }, 800);
  1061. });
  1062. document.addEventListener('livewire:load', function () {
  1063. Livewire.on('courseSelected', (courseId) => {
  1064. console.log('Course selected event received:', courseId);
  1065. setTimeout(() => {
  1066. window.ReportsChartManager.createCourseChartWithValue(courseId);
  1067. }, 200);
  1068. });
  1069. Livewire.on('chartsUpdated', () => {
  1070. console.log('Charts updated event received from Livewire');
  1071. setTimeout(() => {
  1072. window.ReportsChartManager.forceUpdateCharts();
  1073. }, 200);
  1074. });
  1075. });
  1076. </script>
  1077. </div>