reports.blade.php 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915
  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="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. <!-- Main Charts Section - Protected with wire:ignore -->
  34. <div wire:ignore>
  35. <div class="chart-row">
  36. <div class="chart-card">
  37. <div class="chart-header">
  38. <h3 class="chart-title">Entrate e Uscite Mensili - <span
  39. id="monthly-season-title">{{ $seasonFilter }}</span></h3>
  40. </div>
  41. <div class="chart-body">
  42. <div style="display: grid; grid-template-columns: 1fr 300px; align-items: start;">
  43. <div class="chart-container">
  44. <canvas id="monthly-chart-{{ str_replace('-', '', $seasonFilter) }}"></canvas>
  45. </div>
  46. <div class="monthly-table-container" id="monthly-table">
  47. </div>
  48. </div>
  49. </div>
  50. </div>
  51. </div>
  52. <div class="chart-row">
  53. <div class="chart-card">
  54. <div class="chart-header">
  55. <h3 class="chart-title">Causali Performanti - <span
  56. id="causals-season-title">{{ $seasonFilter }}</span></h3>
  57. </div>
  58. <div class="chart-body">
  59. <div class="chart-container">
  60. <canvas id="causals-chart-{{ str_replace('-', '', $seasonFilter) }}"></canvas>
  61. </div>
  62. </div>
  63. </div>
  64. </div>
  65. <div class="chart-row">
  66. <div class="chart-card">
  67. <div class="chart-header">
  68. <h3 class="chart-title">Tesserati per Stagione</h3>
  69. </div>
  70. <div class="chart-body">
  71. <div style="display: grid; grid-template-columns: 1fr 300px; gap: 1rem; align-items: start;">
  72. <div class="chart-container">
  73. <canvas id="members-chart-{{ str_replace('-', '', $seasonFilter) }}"></canvas>
  74. </div>
  75. <div class="members-table-container" id="members-table">
  76. </div>
  77. </div>
  78. </div>
  79. </div>
  80. </div>
  81. </div>
  82. <div class="chart-row">
  83. <div class="chart-card modern-course-card">
  84. <div class="chart-header">
  85. <h3 class="chart-title">Analisi Corsi</h3>
  86. </div>
  87. <div class="chart-body">
  88. <div class="course-controls">
  89. <div class="control-group">
  90. <label>Seleziona Corso ({{ $seasonFilter }}):</label>
  91. <select class="form-select modern-select" wire:model.live="selectedCourse">
  92. <option value="">Seleziona un Corso</option>
  93. @foreach($this->getCoursesForSelect() as $course)
  94. <option value="{{ $course['id'] }}">{{ $course['full_name'] }}</option>
  95. @endforeach
  96. </select>
  97. </div>
  98. </div>
  99. @if($selectedCourse)
  100. <div wire:ignore wire:key="course-chart-{{ $seasonFilter }}-{{ $selectedCourse }}">
  101. <div class="modern-chart-layout">
  102. <!-- Delta Table on the left -->
  103. <div class="course-delta-table"
  104. id="course-delta-table-{{ str_replace('-', '', $seasonFilter) }}-{{ $selectedCourse }}">
  105. <!-- Table will be populated by JavaScript -->
  106. </div>
  107. <!-- Chart on the right -->
  108. <div class="modern-chart-container">
  109. <canvas
  110. id="courses-chart-{{ str_replace('-', '', $seasonFilter) }}-{{ $selectedCourse }}"></canvas>
  111. </div>
  112. </div>
  113. </div>
  114. @else
  115. <div class="chart-placeholder">
  116. <div style="text-align: center;">
  117. <div style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.3;">📊</div>
  118. <p style="font-size: 1.25rem; font-weight: 600; margin: 0;">Seleziona un corso per
  119. visualizzare il grafico</p>
  120. <p style="font-size: 1rem; opacity: 0.7; margin-top: 0.5rem;">Usa il menu a tendina sopra
  121. per scegliere un corso</p>
  122. </div>
  123. </div>
  124. @endif
  125. </div>
  126. </div>
  127. </div>
  128. </div>
  129. <!-- Single JavaScript section -->
  130. <script>
  131. window.ReportsChartManager = window.ReportsChartManager || {
  132. charts: {},
  133. currentSeason: null,
  134. destroyChart: function (chartId) {
  135. if (this.charts[chartId]) {
  136. this.charts[chartId].destroy();
  137. delete this.charts[chartId];
  138. }
  139. },
  140. destroyAllCharts: function () {
  141. Object.keys(this.charts).forEach(chartId => {
  142. this.destroyChart(chartId);
  143. });
  144. },
  145. // NEW: Method to destroy season-specific charts
  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. // Replace your updateMainCharts method with this version that uses fixed canvas IDs:
  153. updateMainCharts: function () {
  154. console.log('=== updateMainCharts called ===');
  155. const seasonFilter = @this.get('seasonFilter');
  156. // Update titles
  157. const monthlyTitle = document.getElementById('monthly-season-title');
  158. const causalsTitle = document.getElementById('causals-season-title');
  159. if (monthlyTitle) {
  160. monthlyTitle.textContent = seasonFilter;
  161. }
  162. if (causalsTitle) {
  163. causalsTitle.textContent = seasonFilter;
  164. }
  165. // Use the original canvas IDs from the blade template
  166. const originalSeasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
  167. console.log('Using original season key for canvas IDs:', originalSeasonKey);
  168. // Get fresh data and update charts
  169. @this.call('getMonthlyTotals').then(monthlyData => {
  170. console.log('Got monthly data:', monthlyData);
  171. this.createMonthlyChart(originalSeasonKey, monthlyData);
  172. this.updateMonthlyTable(monthlyData);
  173. });
  174. @this.call('getTopCausalsByAmount').then(causalsData => {
  175. console.log('Got causals data:', causalsData);
  176. this.createCausalsChart(originalSeasonKey, causalsData);
  177. });
  178. @this.call('getTesseratiData').then(membersData => {
  179. console.log('Got members data:', membersData);
  180. this.createMembersChart(originalSeasonKey, membersData);
  181. this.updateMembersTable(membersData);
  182. });
  183. },
  184. // Add this new method to force chart updates
  185. forceUpdateCharts: function () {
  186. console.log('Force updating charts...');
  187. this.currentSeason = null; // Reset to force update
  188. this.updateMainCharts();
  189. },
  190. createMonthlyChart: function (seasonKey, monthlyData) {
  191. const chartId = `monthly-chart-${seasonKey}`;
  192. const canvas = document.getElementById(chartId);
  193. if (!canvas) {
  194. console.error('Canvas not found for ID:', chartId);
  195. return;
  196. }
  197. this.destroyChart(chartId);
  198. const ctx = canvas.getContext('2d');
  199. const incomeGradient = ctx.createLinearGradient(0, 0, 0, 400);
  200. incomeGradient.addColorStop(0, 'rgba(0, 184, 148, 0.8)');
  201. incomeGradient.addColorStop(1, 'rgba(0, 184, 148, 0.2)');
  202. const expenseGradient = ctx.createLinearGradient(0, 0, 0, 400);
  203. expenseGradient.addColorStop(0, 'rgba(255, 107, 107, 0.8)');
  204. expenseGradient.addColorStop(1, 'rgba(255, 107, 107, 0.2)');
  205. this.charts[chartId] = new Chart(ctx, {
  206. type: 'bar',
  207. data: {
  208. labels: monthlyData.labels,
  209. datasets: [
  210. {
  211. label: 'Entrate',
  212. data: monthlyData.datasets[0].data,
  213. backgroundColor: incomeGradient,
  214. borderColor: '#00b894',
  215. borderWidth: 2,
  216. },
  217. {
  218. label: 'Uscite',
  219. data: monthlyData.datasets[1].data,
  220. backgroundColor: expenseGradient,
  221. borderColor: '#ff6b6b',
  222. borderWidth: 2,
  223. }
  224. ]
  225. },
  226. options: {
  227. responsive: true,
  228. maintainAspectRatio: false,
  229. plugins: {
  230. legend: {
  231. position: 'top',
  232. labels: {
  233. usePointStyle: true,
  234. padding: 20,
  235. font: { weight: '500' }
  236. }
  237. },
  238. tooltip: {
  239. backgroundColor: 'rgba(255, 255, 255, 0.95)',
  240. titleColor: '#212529',
  241. bodyColor: '#495057',
  242. borderColor: '#e9ecef',
  243. borderWidth: 1,
  244. cornerRadius: 8,
  245. callbacks: {
  246. label: function (context) {
  247. return context.dataset.label + ': €' +
  248. new Intl.NumberFormat('it-IT').format(context.parsed.y);
  249. }
  250. }
  251. }
  252. },
  253. scales: {
  254. x: {
  255. grid: { display: false },
  256. ticks: { font: { weight: '500' } }
  257. },
  258. y: {
  259. beginAtZero: true,
  260. grid: { color: 'rgba(0, 0, 0, 0.05)' },
  261. ticks: {
  262. callback: function (value) {
  263. return '€' + new Intl.NumberFormat('it-IT').format(value);
  264. }
  265. }
  266. }
  267. },
  268. animation: {
  269. duration: 1000,
  270. easing: 'easeOutQuart'
  271. }
  272. }
  273. });
  274. },
  275. createCausalsChart: function (seasonKey, causalsData) {
  276. const chartId = `causals-chart-${seasonKey}`;
  277. const canvas = document.getElementById(chartId);
  278. if (!canvas) return;
  279. this.destroyChart(chartId);
  280. const ctx = canvas.getContext('2d');
  281. const colors = [
  282. 'rgba(59, 91, 219, 0.8)',
  283. 'rgba(0, 184, 148, 0.8)',
  284. 'rgba(34, 184, 207, 0.8)',
  285. 'rgba(255, 212, 59, 0.8)',
  286. 'rgba(255, 107, 107, 0.8)',
  287. 'rgba(142, 68, 173, 0.8)',
  288. 'rgba(230, 126, 34, 0.8)',
  289. 'rgba(149, 165, 166, 0.8)',
  290. 'rgba(241, 196, 15, 0.8)',
  291. 'rgba(231, 76, 60, 0.8)'
  292. ];
  293. this.charts[chartId] = new Chart(ctx, {
  294. type: 'doughnut',
  295. data: {
  296. labels: causalsData.inLabels,
  297. datasets: [{
  298. label: 'Importo',
  299. data: causalsData.inData.map(item => item.value),
  300. backgroundColor: colors,
  301. borderColor: colors.map(color => color.replace('0.8', '1')),
  302. borderWidth: 2,
  303. hoverOffset: 8
  304. }]
  305. },
  306. options: {
  307. responsive: true,
  308. maintainAspectRatio: false,
  309. cutout: '60%',
  310. plugins: {
  311. legend: {
  312. position: 'left',
  313. labels: {
  314. usePointStyle: true,
  315. padding: 15,
  316. font: { size: 11, weight: '500' }
  317. }
  318. },
  319. tooltip: {
  320. backgroundColor: 'rgba(255, 255, 255, 0.95)',
  321. titleColor: '#212529',
  322. bodyColor: '#495057',
  323. borderColor: '#e9ecef',
  324. borderWidth: 1,
  325. cornerRadius: 8,
  326. callbacks: {
  327. label: function (context) {
  328. const value = context.raw;
  329. const total = context.dataset.data.reduce((a, b) => a + b, 0);
  330. const percentage = Math.round((value / total) * 100);
  331. return context.label + ': €' +
  332. new Intl.NumberFormat('it-IT').format(value) +
  333. ` (${percentage}%)`;
  334. }
  335. }
  336. }
  337. },
  338. animation: {
  339. animateRotate: true,
  340. duration: 1000
  341. }
  342. }
  343. });
  344. },
  345. createMembersChart: function (seasonKey, membersData) {
  346. const chartId = `members-chart-${seasonKey}`;
  347. const canvas = document.getElementById(chartId);
  348. if (!canvas) return;
  349. this.destroyChart(chartId);
  350. const ctx = canvas.getContext('2d');
  351. const gradient = ctx.createLinearGradient(0, 0, 0, 400);
  352. gradient.addColorStop(0, 'rgba(59, 91, 219, 0.3)');
  353. gradient.addColorStop(1, 'rgba(59, 91, 219, 0.05)');
  354. this.charts[chartId] = new Chart(ctx, {
  355. type: 'line',
  356. data: {
  357. labels: membersData.labels,
  358. datasets: [{
  359. label: 'Membri Tesserati',
  360. data: membersData.datasets[0].data,
  361. borderColor: '#3b5bdb',
  362. backgroundColor: gradient,
  363. borderWidth: 3,
  364. fill: true,
  365. tension: 0.4,
  366. pointBackgroundColor: '#3b5bdb',
  367. pointBorderColor: '#ffffff',
  368. pointBorderWidth: 2,
  369. pointRadius: 6,
  370. pointHoverRadius: 8
  371. }]
  372. },
  373. options: {
  374. responsive: true,
  375. maintainAspectRatio: false,
  376. plugins: {
  377. legend: { display: false },
  378. tooltip: {
  379. backgroundColor: 'rgba(255, 255, 255, 0.95)',
  380. titleColor: '#212529',
  381. bodyColor: '#495057',
  382. borderColor: '#e9ecef',
  383. borderWidth: 1,
  384. cornerRadius: 8,
  385. callbacks: {
  386. label: function (context) {
  387. return 'Tesserati: ' + context.parsed.y;
  388. }
  389. }
  390. }
  391. },
  392. scales: {
  393. x: { grid: { display: false } },
  394. y: {
  395. beginAtZero: true,
  396. grid: { color: 'rgba(0, 0, 0, 0.05)' },
  397. ticks: { precision: 0 }
  398. }
  399. },
  400. animation: {
  401. duration: 1000,
  402. easing: 'easeOutQuart'
  403. }
  404. }
  405. });
  406. },
  407. updateMonthlyTable: function (monthlyData) {
  408. const container = document.getElementById('monthly-table');
  409. if (!container) return;
  410. const incomeData = monthlyData.datasets[0].data;
  411. const expenseData = monthlyData.datasets[1].data;
  412. const monthNames = monthlyData.labels;
  413. let tableHtml = `
  414. <div class="monthly-table">
  415. <div class="table-header">
  416. <div class="table-cell month">Mese</div>
  417. <div class="table-cell">Entrate</div>
  418. <div class="table-cell">Uscite</div>
  419. <div class="table-cell">Delta</div>
  420. </div>
  421. `;
  422. monthNames.forEach((month, index) => {
  423. const income = parseFloat(incomeData[index] || 0);
  424. const expense = parseFloat(expenseData[index] || 0);
  425. const net = income - expense;
  426. const rowClass = net < 0 ? 'negative' : (net > 0 ? 'positive' : 'neutral');
  427. tableHtml += `
  428. <div class="table-row ${rowClass}">
  429. <div class="table-cell month-name">${month}</div>
  430. <div class="table-cell income">€${new Intl.NumberFormat('it-IT').format(income)}</div>
  431. <div class="table-cell expense">€${new Intl.NumberFormat('it-IT').format(expense)}</div>
  432. <div class="table-cell net">€${new Intl.NumberFormat('it-IT').format(net)}</div>
  433. </div>
  434. `;
  435. });
  436. tableHtml += '</div>';
  437. container.innerHTML = tableHtml;
  438. },
  439. updateMembersTable: function (membersData) {
  440. const container = document.getElementById('members-table');
  441. if (!container) return;
  442. const seasonLabels = membersData.labels;
  443. const memberCounts = membersData.datasets[0].data;
  444. let tableHtml = `
  445. <div class="members-table">
  446. <div class="table-header">
  447. <div class="table-cell">Stagione</div>
  448. <div class="table-cell">Tesserati</div>
  449. <div class="table-cell">Variazione</div>
  450. </div>
  451. `;
  452. seasonLabels.forEach((season, index) => {
  453. const current = parseInt(memberCounts[index] || 0);
  454. const previous = index > 0 ? parseInt(memberCounts[index - 1] || 0) : 0;
  455. const variation = index > 0 ? current - previous : 0;
  456. const variationPercent = previous > 0 ? Math.round((variation / previous) * 100 * 10) / 10 : 0;
  457. const rowClass = variation > 0 ? 'positive' : (variation < 0 ? 'negative' : 'neutral');
  458. let variationText = '—';
  459. if (index > 0) {
  460. if (variation > 0) {
  461. variationText = `<span class="variation-positive">+${variation} (+${variationPercent}%)</span>`;
  462. } else if (variation < 0) {
  463. variationText = `<span class="variation-negative">${variation} (${variationPercent}%)</span>`;
  464. } else {
  465. variationText = `<span class="variation-neutral">${variation}</span>`;
  466. }
  467. }
  468. tableHtml += `
  469. <div class="table-row ${rowClass}">
  470. <div class="table-cell season-name">${season}</div>
  471. <div class="table-cell members-count">${new Intl.NumberFormat('it-IT').format(current)}</div>
  472. <div class="table-cell variation">${variationText}</div>
  473. </div>
  474. `;
  475. });
  476. tableHtml += '</div>';
  477. container.innerHTML = tableHtml;
  478. },
  479. createCourseChart: function () {
  480. console.log('Creating course chart...');
  481. const seasonFilter = '{{ $seasonFilter }}';
  482. const selectedCourse = '{{ $selectedCourse ?? '' }}';
  483. const seasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
  484. console.log('Selected course:', selectedCourse, 'for season:', seasonFilter);
  485. // Add this check at the beginning
  486. if (!selectedCourse || selectedCourse.trim() === '') {
  487. console.log('No course selected, skipping chart creation');
  488. return;
  489. }
  490. const chartId = `courses-chart-${seasonKey}-${selectedCourse}`;
  491. const canvas = document.getElementById(chartId);
  492. if (!canvas) return;
  493. this.destroyChart(chartId);
  494. const courseData = @json($this->getCourseMonthlyEarnings());
  495. const ctx = canvas.getContext('2d');
  496. this.charts[chartId] = new Chart(ctx, {
  497. type: 'bar',
  498. data: {
  499. labels: courseData.labels,
  500. datasets: courseData.datasets.map(dataset => {
  501. if (dataset.type === 'line') {
  502. return {
  503. ...dataset,
  504. type: 'line',
  505. fill: false,
  506. backgroundColor: 'transparent'
  507. };
  508. }
  509. return dataset;
  510. })
  511. },
  512. options: {
  513. responsive: true,
  514. maintainAspectRatio: false,
  515. interaction: {
  516. mode: 'index',
  517. intersect: false,
  518. },
  519. scales: {
  520. x: {
  521. grid: { display: false },
  522. ticks: { font: { weight: '500' } }
  523. },
  524. y: {
  525. beginAtZero: true,
  526. grid: {
  527. color: 'rgba(0, 0, 0, 0.1)',
  528. borderDash: [5, 5]
  529. },
  530. ticks: {
  531. callback: function (value) {
  532. return '€' + new Intl.NumberFormat('it-IT').format(value);
  533. }
  534. }
  535. }
  536. },
  537. plugins: {
  538. legend: {
  539. display: true,
  540. position: 'top',
  541. labels: {
  542. usePointStyle: true,
  543. padding: 20,
  544. font: { weight: '500' }
  545. }
  546. },
  547. tooltip: {
  548. backgroundColor: 'rgba(255, 255, 255, 0.95)',
  549. titleColor: '#212529',
  550. bodyColor: '#495057',
  551. borderColor: '#e9ecef',
  552. borderWidth: 1,
  553. cornerRadius: 8,
  554. callbacks: {
  555. label: function (context) {
  556. return context.dataset.label + ': €' +
  557. new Intl.NumberFormat('it-IT').format(context.parsed.y);
  558. }
  559. }
  560. }
  561. },
  562. animation: {
  563. duration: 1000,
  564. easing: 'easeOutQuart'
  565. }
  566. }
  567. });
  568. },
  569. createCourseChartWithValue: function (selectedCourseValue) {
  570. console.log('Creating modern course chart with value:', selectedCourseValue);
  571. const seasonFilter = '{{ $seasonFilter }}';
  572. const seasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
  573. const chartId = `courses-chart-${seasonKey}-${selectedCourseValue}`;
  574. const tableId = `course-delta-table-${seasonKey}-${selectedCourseValue}`;
  575. let canvas = document.getElementById(chartId);
  576. const tableContainer = document.getElementById(tableId);
  577. if (!canvas) {
  578. console.log('Canvas not found for chart ID:', chartId);
  579. return;
  580. }
  581. this.destroyChart(chartId);
  582. // Call Livewire method to get fresh data
  583. @this.call('getCourseData', selectedCourseValue).then(courseData => {
  584. console.log('Received course data:', courseData);
  585. // Check if data is empty
  586. if (courseData.isEmpty) {
  587. console.log('No data available for course, showing message');
  588. // Clear the table
  589. if (tableContainer) {
  590. tableContainer.innerHTML = '';
  591. }
  592. // Show message instead of chart
  593. const chartContainer = canvas.parentElement;
  594. chartContainer.innerHTML = `
  595. <div class="chart-empty-state">
  596. <div style="text-align: center; padding: 4rem 2rem;">
  597. <div style="font-size: 4rem; margin-bottom: 1.5rem; opacity: 0.3;">📊</div>
  598. <h3 style="font-size: 1.5rem; font-weight: 600; margin-bottom: 1rem; color: #374151;">
  599. ${courseData.message}
  600. </h3>
  601. <p style="font-size: 1rem; opacity: 0.7; margin: 0; max-width: 400px; margin-left: auto; margin-right: auto; line-height: 1.5;">
  602. Questo corso non ha pagamenti registrati per la stagione selezionata.
  603. </p>
  604. </div>
  605. </div>
  606. `;
  607. return;
  608. }
  609. if (!courseData || !courseData.labels || courseData.labels.length === 0) {
  610. console.log('No data available for chart');
  611. return;
  612. }
  613. // Restore canvas if it was replaced by empty state
  614. let canvasElement = document.getElementById(chartId);
  615. if (!canvasElement) {
  616. const chartContainer = canvas.parentElement;
  617. chartContainer.innerHTML = `<canvas id="${chartId}"></canvas>`;
  618. canvasElement = document.getElementById(chartId);
  619. }
  620. // Update the delta table
  621. this.updateCourseTable(tableContainer, courseData.tableData);
  622. // Store participant data for tooltip
  623. const participantData = courseData.datasets.find(d => d.participantData)?.participantData || [];
  624. const ctx = canvasElement.getContext('2d');
  625. // Create gradients
  626. const earnedGradient = ctx.createLinearGradient(0, 0, 0, 400);
  627. earnedGradient.addColorStop(0, 'rgba(16, 185, 129, 0.9)');
  628. earnedGradient.addColorStop(1, 'rgba(16, 185, 129, 0.3)');
  629. this.charts[chartId] = new Chart(ctx, {
  630. type: 'bar',
  631. data: {
  632. labels: courseData.labels,
  633. datasets: courseData.datasets.map(dataset => {
  634. if (dataset.type === 'line') {
  635. return {
  636. ...dataset,
  637. type: 'line',
  638. fill: false,
  639. backgroundColor: 'transparent'
  640. };
  641. } else {
  642. return {
  643. ...dataset,
  644. backgroundColor: earnedGradient
  645. };
  646. }
  647. })
  648. },
  649. options: {
  650. responsive: true,
  651. maintainAspectRatio: false,
  652. interaction: {
  653. mode: 'index',
  654. intersect: false,
  655. },
  656. layout: {
  657. padding: {
  658. top: 20,
  659. right: 20,
  660. bottom: 20,
  661. left: 10
  662. }
  663. },
  664. scales: {
  665. x: {
  666. grid: {
  667. display: false
  668. },
  669. ticks: {
  670. font: {
  671. weight: '600',
  672. size: 13
  673. },
  674. color: '#6b7280'
  675. },
  676. border: {
  677. display: false
  678. }
  679. },
  680. y: {
  681. beginAtZero: true,
  682. grid: {
  683. color: 'rgba(156, 163, 175, 0.15)',
  684. borderDash: [3, 3]
  685. },
  686. border: {
  687. display: false
  688. },
  689. ticks: {
  690. font: {
  691. size: 12,
  692. weight: '500'
  693. },
  694. color: '#6b7280',
  695. callback: function (value) {
  696. return '€' + new Intl.NumberFormat('it-IT', {
  697. minimumFractionDigits: 0,
  698. maximumFractionDigits: 0
  699. }).format(value);
  700. }
  701. }
  702. }
  703. },
  704. plugins: {
  705. legend: {
  706. display: false
  707. },
  708. tooltip: {
  709. backgroundColor: 'rgba(255, 255, 255, 0.98)',
  710. titleColor: '#111827',
  711. bodyColor: '#374151',
  712. borderColor: 'rgba(229, 231, 235, 0.8)',
  713. borderWidth: 1,
  714. cornerRadius: 12,
  715. titleFont: {
  716. weight: 'bold',
  717. size: 15
  718. },
  719. bodyFont: {
  720. size: 14,
  721. weight: '500'
  722. },
  723. padding: 16,
  724. boxPadding: 8,
  725. usePointStyle: true,
  726. displayColors: true,
  727. callbacks: {
  728. title: function (context) {
  729. return context[0].label;
  730. },
  731. label: function (context) {
  732. let label = context.dataset.label + ': €' +
  733. new Intl.NumberFormat('it-IT').format(context.parsed.y);
  734. if (context.dataset.label === 'Pagamenti Attesi' && participantData[context.dataIndex]) {
  735. label += '\n👥 Partecipanti: ' + participantData[context.dataIndex];
  736. }
  737. return label;
  738. }
  739. }
  740. }
  741. },
  742. animation: {
  743. duration: 1500,
  744. easing: 'easeOutCubic'
  745. },
  746. elements: {
  747. bar: {
  748. borderRadius: {
  749. topLeft: 8,
  750. topRight: 8,
  751. bottomLeft: 0,
  752. bottomRight: 0
  753. }
  754. },
  755. line: {
  756. borderCapStyle: 'round',
  757. borderJoinStyle: 'round'
  758. },
  759. point: {
  760. hoverBorderWidth: 4,
  761. borderWidth: 3
  762. }
  763. }
  764. }
  765. });
  766. }).catch(error => {
  767. console.error('Error calling getCourseData:', error);
  768. });
  769. },
  770. updateCourseTable: function (container, tableData) {
  771. if (!container || !tableData) return;
  772. let tableHtml = `
  773. <div class="course-table">
  774. <div class="table-header">
  775. <div class="table-cell month">Mese</div>
  776. <div class="table-cell participants">👥</div>
  777. <div class="table-cell delta">Mancanti</div>
  778. <div class="table-cell percentage">%</div>
  779. </div>
  780. `;
  781. tableData.forEach(row => {
  782. const deltaClass = row.delta > 0 ? 'negative' : (row.delta === 0 ? 'neutral' : 'positive');
  783. const percentageClass = row.percentage >= 80 ? 'good' : (row.percentage >= 50 ? 'warning' : 'bad');
  784. tableHtml += `
  785. <div class="table-row">
  786. <div class="table-cell month">${row.month}</div>
  787. <div class="table-cell participants">${row.participants}</div>
  788. <div class="table-cell delta ${deltaClass}">€${new Intl.NumberFormat('it-IT').format(Math.abs(row.delta))}</div>
  789. <div class="table-cell percentage ${percentageClass}">${row.percentage}%</div>
  790. </div>
  791. `;
  792. });
  793. tableHtml += '</div>';
  794. container.innerHTML = tableHtml;
  795. },
  796. updateCourseChart: function () {
  797. if (this.selectedCourse) {
  798. const seasonFilter = @json($seasonFilter);
  799. const seasonKey = seasonFilter.replace('-', '');
  800. this.createCourseChartWithValue(this.selectedCourse);
  801. }
  802. }
  803. };
  804. document.addEventListener('DOMContentLoaded', function () {
  805. setTimeout(() => {
  806. window.ReportsChartManager.updateMainCharts();
  807. }, 100);
  808. });
  809. document.addEventListener('livewire:navigated', function () {
  810. setTimeout(() => {
  811. window.ReportsChartManager.updateMainCharts();
  812. }, 100);
  813. });
  814. document.addEventListener('livewire:updated', function (event) {
  815. console.log('Livewire updated, waiting for component to fully update...');
  816. setTimeout(() => {
  817. console.log('Now updating charts after delay');
  818. window.ReportsChartManager.forceUpdateCharts();
  819. }, 800); // Increased delay to 800ms
  820. });
  821. document.addEventListener('livewire:load', function () {
  822. Livewire.on('courseSelected', (courseId) => {
  823. console.log('Course selected event received:', courseId);
  824. setTimeout(() => {
  825. window.ReportsChartManager.createCourseChartWithValue(courseId);
  826. }, 200);
  827. });
  828. // Listen for the chartsUpdated event from your updateCharts method
  829. Livewire.on('chartsUpdated', () => {
  830. console.log('Charts updated event received from Livewire');
  831. setTimeout(() => {
  832. window.ReportsChartManager.forceUpdateCharts();
  833. }, 200);
  834. });
  835. });
  836. </script>
  837. </div>