reports.blade.php 60 KB

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