reports.blade.php 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662
  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">
  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" 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 class="legend-container">
  99. <div class="legend-item">
  100. <div class="legend-color" style="background: rgba(0, 184, 148, 1);"></div>
  101. <span>Pagamenti Effettuati</span>
  102. </div>
  103. <div class="legend-item">
  104. <div class="legend-color" style="background: rgba(48, 51, 107, 1);"></div>
  105. <span>Pagamenti Attesi</span>
  106. </div>
  107. </div>
  108. </div>
  109. @if($selectedCourse)
  110. <div wire:ignore wire:key="course-chart-{{ $seasonFilter }}-{{ $selectedCourse }}">
  111. <div class="chart-container">
  112. <canvas
  113. id="courses-chart-{{ str_replace('-', '', $seasonFilter) }}-{{ $selectedCourse }}"></canvas>
  114. </div>
  115. </div>
  116. @else
  117. <div class="chart-container"
  118. style="display: flex; align-items: center; justify-content: center; min-height: 400px; color: var(--secondary-color);">
  119. <p style="font-size: 1.1rem;">Seleziona un corso per visualizzare il grafico</p>
  120. </div>
  121. @endif
  122. </div>
  123. </div>
  124. </div>
  125. </div>
  126. <!-- Single JavaScript section -->
  127. <script>
  128. // Global chart manager
  129. window.ReportsChartManager = window.ReportsChartManager || {
  130. charts: {},
  131. currentSeason: null,
  132. destroyChart: function (chartId) {
  133. if (this.charts[chartId]) {
  134. this.charts[chartId].destroy();
  135. delete this.charts[chartId];
  136. }
  137. },
  138. destroyAllCharts: function () {
  139. Object.keys(this.charts).forEach(chartId => {
  140. this.destroyChart(chartId);
  141. });
  142. },
  143. updateMainCharts: function () {
  144. const seasonFilter = '{{ $seasonFilter }}';
  145. const seasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
  146. // Only update if season changed
  147. if (this.currentSeason === seasonFilter) {
  148. return;
  149. }
  150. this.currentSeason = seasonFilter;
  151. // Get fresh data
  152. const monthlyData = @json($this->getMonthlyTotals());
  153. const causalsData = @json($this->getTopCausalsByAmount());
  154. const membersData = @json($this->getTesseratiData());
  155. // Update titles
  156. document.getElementById('monthly-season-title').textContent = seasonFilter;
  157. document.getElementById('causals-season-title').textContent = seasonFilter;
  158. // Create/update charts
  159. this.createMonthlyChart(seasonKey, monthlyData);
  160. this.createCausalsChart(seasonKey, causalsData);
  161. this.createMembersChart(seasonKey, membersData);
  162. // Update tables
  163. this.updateMonthlyTable(monthlyData);
  164. this.updateMembersTable(membersData);
  165. },
  166. createMonthlyChart: function (seasonKey, monthlyData) {
  167. const chartId = `monthly-chart-${seasonKey}`;
  168. const canvas = document.getElementById(chartId);
  169. if (!canvas) return;
  170. this.destroyChart(chartId);
  171. const ctx = canvas.getContext('2d');
  172. const incomeGradient = ctx.createLinearGradient(0, 0, 0, 400);
  173. incomeGradient.addColorStop(0, 'rgba(0, 184, 148, 0.8)');
  174. incomeGradient.addColorStop(1, 'rgba(0, 184, 148, 0.2)');
  175. const expenseGradient = ctx.createLinearGradient(0, 0, 0, 400);
  176. expenseGradient.addColorStop(0, 'rgba(255, 107, 107, 0.8)');
  177. expenseGradient.addColorStop(1, 'rgba(255, 107, 107, 0.2)');
  178. this.charts[chartId] = new Chart(ctx, {
  179. type: 'bar',
  180. data: {
  181. labels: monthlyData.labels,
  182. datasets: [
  183. {
  184. label: 'Entrate',
  185. data: monthlyData.datasets[0].data,
  186. backgroundColor: incomeGradient,
  187. borderColor: '#00b894',
  188. borderWidth: 2,
  189. },
  190. {
  191. label: 'Uscite',
  192. data: monthlyData.datasets[1].data,
  193. backgroundColor: expenseGradient,
  194. borderColor: '#ff6b6b',
  195. borderWidth: 2,
  196. }
  197. ]
  198. },
  199. options: {
  200. responsive: true,
  201. maintainAspectRatio: false,
  202. plugins: {
  203. legend: {
  204. position: 'top',
  205. labels: {
  206. usePointStyle: true,
  207. padding: 20,
  208. font: { weight: '500' }
  209. }
  210. },
  211. tooltip: {
  212. backgroundColor: 'rgba(255, 255, 255, 0.95)',
  213. titleColor: '#212529',
  214. bodyColor: '#495057',
  215. borderColor: '#e9ecef',
  216. borderWidth: 1,
  217. cornerRadius: 8,
  218. callbacks: {
  219. label: function (context) {
  220. return context.dataset.label + ': €' +
  221. new Intl.NumberFormat('it-IT').format(context.parsed.y);
  222. }
  223. }
  224. }
  225. },
  226. scales: {
  227. x: {
  228. grid: { display: false },
  229. ticks: { font: { weight: '500' } }
  230. },
  231. y: {
  232. beginAtZero: true,
  233. grid: { color: 'rgba(0, 0, 0, 0.05)' },
  234. ticks: {
  235. callback: function (value) {
  236. return '€' + new Intl.NumberFormat('it-IT').format(value);
  237. }
  238. }
  239. }
  240. },
  241. animation: {
  242. duration: 1000,
  243. easing: 'easeOutQuart'
  244. }
  245. }
  246. });
  247. },
  248. createCausalsChart: function (seasonKey, causalsData) {
  249. const chartId = `causals-chart-${seasonKey}`;
  250. const canvas = document.getElementById(chartId);
  251. if (!canvas) return;
  252. this.destroyChart(chartId);
  253. const ctx = canvas.getContext('2d');
  254. const colors = [
  255. 'rgba(59, 91, 219, 0.8)',
  256. 'rgba(0, 184, 148, 0.8)',
  257. 'rgba(34, 184, 207, 0.8)',
  258. 'rgba(255, 212, 59, 0.8)',
  259. 'rgba(255, 107, 107, 0.8)',
  260. 'rgba(142, 68, 173, 0.8)',
  261. 'rgba(230, 126, 34, 0.8)',
  262. 'rgba(149, 165, 166, 0.8)',
  263. 'rgba(241, 196, 15, 0.8)',
  264. 'rgba(231, 76, 60, 0.8)'
  265. ];
  266. this.charts[chartId] = new Chart(ctx, {
  267. type: 'doughnut',
  268. data: {
  269. labels: causalsData.inLabels,
  270. datasets: [{
  271. label: 'Importo',
  272. data: causalsData.inData.map(item => item.value),
  273. backgroundColor: colors,
  274. borderColor: colors.map(color => color.replace('0.8', '1')),
  275. borderWidth: 2,
  276. hoverOffset: 8
  277. }]
  278. },
  279. options: {
  280. responsive: true,
  281. maintainAspectRatio: false,
  282. cutout: '60%',
  283. plugins: {
  284. legend: {
  285. position: 'left',
  286. labels: {
  287. usePointStyle: true,
  288. padding: 15,
  289. font: { size: 11, weight: '500' }
  290. }
  291. },
  292. tooltip: {
  293. backgroundColor: 'rgba(255, 255, 255, 0.95)',
  294. titleColor: '#212529',
  295. bodyColor: '#495057',
  296. borderColor: '#e9ecef',
  297. borderWidth: 1,
  298. cornerRadius: 8,
  299. callbacks: {
  300. label: function (context) {
  301. const value = context.raw;
  302. const total = context.dataset.data.reduce((a, b) => a + b, 0);
  303. const percentage = Math.round((value / total) * 100);
  304. return context.label + ': €' +
  305. new Intl.NumberFormat('it-IT').format(value) +
  306. ` (${percentage}%)`;
  307. }
  308. }
  309. }
  310. },
  311. animation: {
  312. animateRotate: true,
  313. duration: 1000
  314. }
  315. }
  316. });
  317. },
  318. createMembersChart: function (seasonKey, membersData) {
  319. const chartId = `members-chart-${seasonKey}`;
  320. const canvas = document.getElementById(chartId);
  321. if (!canvas) return;
  322. this.destroyChart(chartId);
  323. const ctx = canvas.getContext('2d');
  324. const gradient = ctx.createLinearGradient(0, 0, 0, 400);
  325. gradient.addColorStop(0, 'rgba(59, 91, 219, 0.3)');
  326. gradient.addColorStop(1, 'rgba(59, 91, 219, 0.05)');
  327. this.charts[chartId] = new Chart(ctx, {
  328. type: 'line',
  329. data: {
  330. labels: membersData.labels,
  331. datasets: [{
  332. label: 'Membri Tesserati',
  333. data: membersData.datasets[0].data,
  334. borderColor: '#3b5bdb',
  335. backgroundColor: gradient,
  336. borderWidth: 3,
  337. fill: true,
  338. tension: 0.4,
  339. pointBackgroundColor: '#3b5bdb',
  340. pointBorderColor: '#ffffff',
  341. pointBorderWidth: 2,
  342. pointRadius: 6,
  343. pointHoverRadius: 8
  344. }]
  345. },
  346. options: {
  347. responsive: true,
  348. maintainAspectRatio: false,
  349. plugins: {
  350. legend: { display: false },
  351. tooltip: {
  352. backgroundColor: 'rgba(255, 255, 255, 0.95)',
  353. titleColor: '#212529',
  354. bodyColor: '#495057',
  355. borderColor: '#e9ecef',
  356. borderWidth: 1,
  357. cornerRadius: 8,
  358. callbacks: {
  359. label: function (context) {
  360. return 'Tesserati: ' + context.parsed.y;
  361. }
  362. }
  363. }
  364. },
  365. scales: {
  366. x: { grid: { display: false } },
  367. y: {
  368. beginAtZero: true,
  369. grid: { color: 'rgba(0, 0, 0, 0.05)' },
  370. ticks: { precision: 0 }
  371. }
  372. },
  373. animation: {
  374. duration: 1000,
  375. easing: 'easeOutQuart'
  376. }
  377. }
  378. });
  379. },
  380. updateMonthlyTable: function (monthlyData) {
  381. const container = document.getElementById('monthly-table');
  382. if (!container) return;
  383. const incomeData = monthlyData.datasets[0].data;
  384. const expenseData = monthlyData.datasets[1].data;
  385. const monthNames = monthlyData.labels;
  386. let tableHtml = `
  387. <div class="monthly-table">
  388. <div class="table-header">
  389. <div class="table-cell">Mese</div>
  390. <div class="table-cell">Entrate</div>
  391. <div class="table-cell">Uscite</div>
  392. <div class="table-cell">Delta</div>
  393. </div>
  394. `;
  395. monthNames.forEach((month, index) => {
  396. const income = parseFloat(incomeData[index] || 0);
  397. const expense = parseFloat(expenseData[index] || 0);
  398. const net = income - expense;
  399. const rowClass = net < 0 ? 'negative' : (net > 0 ? 'positive' : 'neutral');
  400. tableHtml += `
  401. <div class="table-row ${rowClass}">
  402. <div class="table-cell month-name">${month}</div>
  403. <div class="table-cell income">€${new Intl.NumberFormat('it-IT').format(income)}</div>
  404. <div class="table-cell expense">€${new Intl.NumberFormat('it-IT').format(expense)}</div>
  405. <div class="table-cell net">€${new Intl.NumberFormat('it-IT').format(net)}</div>
  406. </div>
  407. `;
  408. });
  409. tableHtml += '</div>';
  410. container.innerHTML = tableHtml;
  411. },
  412. updateMembersTable: function (membersData) {
  413. const container = document.getElementById('members-table');
  414. if (!container) return;
  415. const seasonLabels = membersData.labels;
  416. const memberCounts = membersData.datasets[0].data;
  417. let tableHtml = `
  418. <h4 style="margin-bottom: 1rem; font-size: 1rem; font-weight: 600; color: var(--dark-color);">
  419. Riepilogo Tesserati
  420. </h4>
  421. <div class="members-table">
  422. <div class="table-header">
  423. <div class="table-cell">Stagione</div>
  424. <div class="table-cell">Tesserati</div>
  425. <div class="table-cell">Variazione</div>
  426. </div>
  427. `;
  428. seasonLabels.forEach((season, index) => {
  429. const current = parseInt(memberCounts[index] || 0);
  430. const previous = index > 0 ? parseInt(memberCounts[index - 1] || 0) : 0;
  431. const variation = index > 0 ? current - previous : 0;
  432. const variationPercent = previous > 0 ? Math.round((variation / previous) * 100 * 10) / 10 : 0;
  433. const rowClass = variation > 0 ? 'positive' : (variation < 0 ? 'negative' : 'neutral');
  434. let variationText = '—';
  435. if (index > 0) {
  436. if (variation > 0) {
  437. variationText = `<span class="variation-positive">+${variation} (+${variationPercent}%)</span>`;
  438. } else if (variation < 0) {
  439. variationText = `<span class="variation-negative">${variation} (${variationPercent}%)</span>`;
  440. } else {
  441. variationText = `<span class="variation-neutral">${variation}</span>`;
  442. }
  443. }
  444. tableHtml += `
  445. <div class="table-row ${rowClass}">
  446. <div class="table-cell season-name">${season}</div>
  447. <div class="table-cell members-count">${new Intl.NumberFormat('it-IT').format(current)}</div>
  448. <div class="table-cell variation">${variationText}</div>
  449. </div>
  450. `;
  451. });
  452. tableHtml += '</div>';
  453. container.innerHTML = tableHtml;
  454. },
  455. createCourseChart: function () {
  456. console.log('Creating course chart...');
  457. const seasonFilter = '{{ $seasonFilter }}';
  458. const selectedCourse = '{{ $selectedCourse ?? '' }}';
  459. const seasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
  460. console.log('Selected course:', selectedCourse, 'for season:', seasonFilter);
  461. // Add this check at the beginning
  462. if (!selectedCourse || selectedCourse.trim() === '') {
  463. console.log('No course selected, skipping chart creation');
  464. return;
  465. }
  466. const chartId = `courses-chart-${seasonKey}-${selectedCourse}`;
  467. const canvas = document.getElementById(chartId);
  468. if (!canvas) return;
  469. this.destroyChart(chartId);
  470. const courseData = @json($this->getCourseMonthlyEarnings());
  471. const ctx = canvas.getContext('2d');
  472. this.charts[chartId] = new Chart(ctx, {
  473. type: 'bar',
  474. data: {
  475. labels: courseData.labels,
  476. datasets: courseData.datasets.map(dataset => {
  477. if (dataset.type === 'line') {
  478. return {
  479. ...dataset,
  480. type: 'line',
  481. fill: false,
  482. backgroundColor: 'transparent'
  483. };
  484. }
  485. return dataset;
  486. })
  487. },
  488. options: {
  489. responsive: true,
  490. maintainAspectRatio: false,
  491. interaction: {
  492. mode: 'index',
  493. intersect: false,
  494. },
  495. scales: {
  496. x: {
  497. grid: { display: false },
  498. ticks: { font: { weight: '500' } }
  499. },
  500. y: {
  501. beginAtZero: true,
  502. grid: {
  503. color: 'rgba(0, 0, 0, 0.1)',
  504. borderDash: [5, 5]
  505. },
  506. ticks: {
  507. callback: function (value) {
  508. return '€' + new Intl.NumberFormat('it-IT').format(value);
  509. }
  510. }
  511. }
  512. },
  513. plugins: {
  514. legend: {
  515. display: true,
  516. position: 'top',
  517. labels: {
  518. usePointStyle: true,
  519. padding: 20,
  520. font: { weight: '500' }
  521. }
  522. },
  523. tooltip: {
  524. backgroundColor: 'rgba(255, 255, 255, 0.95)',
  525. titleColor: '#212529',
  526. bodyColor: '#495057',
  527. borderColor: '#e9ecef',
  528. borderWidth: 1,
  529. cornerRadius: 8,
  530. callbacks: {
  531. label: function (context) {
  532. return context.dataset.label + ': €' +
  533. new Intl.NumberFormat('it-IT').format(context.parsed.y);
  534. }
  535. }
  536. }
  537. },
  538. animation: {
  539. duration: 1000,
  540. easing: 'easeOutQuart'
  541. }
  542. }
  543. });
  544. },
  545. createCourseChartWithValue: function (selectedCourseValue) {
  546. console.log('Creating course chart with value:', selectedCourseValue);
  547. const seasonFilter = '{{ $seasonFilter }}';
  548. const seasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
  549. const chartId = `courses-chart-${seasonKey}-${selectedCourseValue}`;
  550. const canvas = document.getElementById(chartId);
  551. if (!canvas) {
  552. console.log('Canvas not found for chart ID:', chartId);
  553. return;
  554. }
  555. this.destroyChart(chartId);
  556. // Call Livewire method to get fresh data
  557. @this.call('getCourseData', selectedCourseValue).then(courseData => {
  558. console.log('Received course data:', courseData);
  559. if (!courseData || !courseData.labels || courseData.labels.length === 0) {
  560. console.log('No data available for chart');
  561. return;
  562. }
  563. const ctx = canvas.getContext('2d');
  564. this.charts[chartId] = new Chart(ctx, {
  565. type: 'bar',
  566. data: {
  567. labels: courseData.labels,
  568. datasets: courseData.datasets
  569. },
  570. options: {
  571. responsive: true,
  572. maintainAspectRatio: false,
  573. scales: {
  574. y: {
  575. beginAtZero: true
  576. }
  577. }
  578. }
  579. });
  580. }).catch(error => {
  581. console.error('Error calling getCourseData:', error);
  582. });
  583. },
  584. };
  585. document.addEventListener('DOMContentLoaded', function () {
  586. setTimeout(() => {
  587. window.ReportsChartManager.updateMainCharts();
  588. }, 100);
  589. });
  590. document.addEventListener('livewire:navigated', function () {
  591. setTimeout(() => {
  592. window.ReportsChartManager.updateMainCharts();
  593. }, 100);
  594. });
  595. document.addEventListener('livewire:load', function () {
  596. Livewire.on('courseSelected', (courseId) => {
  597. console.log('Course selected event received:', courseId);
  598. setTimeout(() => {
  599. window.ReportsChartManager.createCourseChartWithValue(courseId);
  600. }, 200);
  601. });
  602. });
  603. </script>
  604. </div>