reports.blade.php 52 KB

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