reports.blade.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579
  1. <!-- filepath: /Users/fabiofratini/Desktop/Projects/iao_team/resources/views/livewire/reports.blade.php -->
  2. <div class="col card--ui" id="card--dashboard">
  3. <header id="title--section" style="display:none !important"
  4. class="d-flex align-items-center justify-content-between">
  5. <div class="title--section_name d-flex align-items-center justify-content-between">
  6. <i class="ico--ui title_section utenti me-2"></i>
  7. <h2 class="primary">Reports</h2>
  8. </div>
  9. </header>
  10. <section id="subheader" class="d-flex align-items-center">
  11. </section>
  12. <section id="reports-section">
  13. <div class="row">
  14. <div class="col-md-12">
  15. <canvas id="monthly-in-out-chart"></canvas>
  16. </div>
  17. </div>
  18. <div class="col-md-12 chart-container">
  19. <canvas id="causals-chart" style="height: 300px; max-height: 300px;"></canvas>
  20. </div>
  21. <div class="row mt-5">
  22. <div class="col-md-12">
  23. <canvas id="tesserati-chart"></canvas>
  24. </div>
  25. </div>
  26. <div class="row mt-5">
  27. <div class="col-md-6">
  28. <select wire:model="selectedCourse" wire:change="updateCourseChart">
  29. <option value="">Seleziona un Corso</option>
  30. @foreach($courses as $course)
  31. <option value="{{ $course['id'] }}">{{ $course['full_name'] }}</option>
  32. @endforeach
  33. </select>
  34. </div>
  35. <div class="col-md-12 mt-3">
  36. <canvas id="courses-chart" style="height: 250px; max-height: 250px;"></canvas>
  37. </div>
  38. </div>
  39. </section>
  40. </div>
  41. @push('scripts')
  42. <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  43. <script>
  44. document.addEventListener('DOMContentLoaded', function () {
  45. initializeChartSizes();
  46. window.livewire.on('dataUpdated', () => {
  47. updateCharts();
  48. updateCausalsChart();
  49. });
  50. window.livewire.on('courseDataUpdated', async (courseId) => {
  51. console.log('Course data update event received for course ID:', courseId);
  52. await updateCoursesChart(courseId);
  53. updateCausalsChart();
  54. Object.keys(window.chartSizes).forEach(chartId => {
  55. restoreChartSize(chartId);
  56. });
  57. });
  58. updateCharts();
  59. updateCausalsChart();
  60. updateTesseratiChart();
  61. async function updateCharts() {
  62. try {
  63. const monthlyData = await @this.getMonthlyTotals();
  64. if (window.monthlyInOutChart) {
  65. window.monthlyInOutChart.destroy();
  66. }
  67. const monthlyInOutCtx = document.getElementById('monthly-in-out-chart').getContext('2d');
  68. window.monthlyInOutChart = new Chart(monthlyInOutCtx, {
  69. type: 'bar',
  70. data: monthlyData,
  71. options: {
  72. responsive: true,
  73. scales: {
  74. x: {
  75. title: {
  76. display: false,
  77. text: 'Mese'
  78. },
  79. grid: {
  80. display: false
  81. }
  82. },
  83. y: {
  84. display: false,
  85. title: {
  86. display: false,
  87. text: 'Importo (€)'
  88. },
  89. beginAtZero: true,
  90. ticks: {
  91. display: false
  92. },
  93. grid: {
  94. display: false
  95. }
  96. }
  97. },
  98. plugins: {
  99. legend: {
  100. display: true
  101. },
  102. title: {
  103. display: true,
  104. text: 'Entrate/Uscite Mensili',
  105. font: {
  106. size: 16
  107. }
  108. },
  109. tooltip: {
  110. callbacks: {
  111. label: function (context) {
  112. let label = context.dataset.label || '';
  113. if (label) {
  114. label += ': ';
  115. }
  116. if (context.parsed.y !== null) {
  117. label += new Intl.NumberFormat('it-IT', {
  118. style: 'currency',
  119. currency: 'EUR'
  120. }).format(context.parsed.y);
  121. }
  122. return label;
  123. }
  124. }
  125. }
  126. }
  127. }
  128. });
  129. const summaryData = await @this.getYearlySummary();
  130. } catch (error) {
  131. console.error('Error updating charts:', error);
  132. document.getElementById('monthly-in-out-chart').insertAdjacentHTML(
  133. 'afterend',
  134. '<div class="alert alert-danger">Errore nel caricamento dei dati finanziari</div>'
  135. );
  136. }
  137. }
  138. async function updateCausalsChart() {
  139. try {
  140. const causalsData = await @this.getTopCausalsByAmount(10, 'IN');
  141. if (window.causalsChart) {
  142. window.causalsChart.destroy();
  143. }
  144. const causalsCtx = document.getElementById('causals-chart').getContext('2d');
  145. const existingTabs = document.querySelector('.causals-tabs');
  146. if (existingTabs) {
  147. existingTabs.remove();
  148. }
  149. const existingTitle = document.querySelector('.causals-title');
  150. if (existingTitle) {
  151. existingTitle.remove();
  152. }
  153. const chartTitle = document.createElement('h4');
  154. chartTitle.className = 'text-center mt-2 mb-3 causals-title';
  155. const chartCanvas = document.getElementById('causals-chart');
  156. chartCanvas.parentNode.insertBefore(chartTitle, chartCanvas);
  157. const inData = causalsData.inData;
  158. const colors = [
  159. 'rgba(54, 162, 235, 0.8)', // Blue
  160. 'rgba(75, 192, 192, 0.8)', // Teal
  161. 'rgba(153, 102, 255, 0.8)', // Purple
  162. 'rgba(255, 159, 64, 0.8)', // Orange
  163. 'rgba(39, 174, 96, 0.8)', // Green
  164. 'rgba(41, 128, 185, 0.8)', // Dark blue
  165. 'rgba(142, 68, 173, 0.8)', // Dark purple
  166. 'rgba(230, 126, 34, 0.8)', // Dark orange
  167. 'rgba(46, 204, 113, 0.8)', // Light green
  168. 'rgba(52, 152, 219, 0.8)' // Light blue
  169. ];
  170. const commonOptions = {
  171. responsive: true,
  172. maintainAspectRatio: false,
  173. plugins: {
  174. title: {
  175. display: true,
  176. text: 'Causali performanti',
  177. font: {
  178. size: 16
  179. }
  180. },
  181. tooltip: {
  182. callbacks: {
  183. label: function (context) {
  184. const fullName = inData[context.dataIndex]?.fullName || context.label;
  185. const value = context.raw;
  186. return fullName + ': ' + new Intl.NumberFormat('it-IT', {
  187. style: 'currency',
  188. currency: 'EUR'
  189. }).format(value);
  190. }
  191. }
  192. },
  193. legend: {
  194. display: true,
  195. position: 'right',
  196. labels: {
  197. boxWidth: 15,
  198. padding: 10,
  199. generateLabels: function (chart) {
  200. const data = chart.data;
  201. if (data.labels.length && data.datasets.length) {
  202. return data.labels.map(function (label, i) {
  203. const meta = chart.getDatasetMeta(0);
  204. const style = meta.controller.getStyle(i);
  205. let shortenedLabel = label;
  206. if (label.length > 20) {
  207. shortenedLabel = label.substring(0, 17) + '...';
  208. }
  209. return {
  210. text: shortenedLabel,
  211. fillStyle: style.backgroundColor,
  212. hidden: !chart.getDataVisibility(i),
  213. index: i,
  214. datasetIndex: 0
  215. };
  216. });
  217. }
  218. return [];
  219. }
  220. }
  221. }
  222. }
  223. };
  224. let chartData = {
  225. labels: inData.map(item => item.label),
  226. datasets: [{
  227. label: 'Importo',
  228. data: inData.map(item => item.value),
  229. backgroundColor: inData.map((item, index) => colors[index % colors.length]),
  230. borderWidth: 1,
  231. borderColor: '#fff'
  232. }]
  233. };
  234. window.causalsChart = new Chart(causalsCtx, {
  235. type: 'doughnut',
  236. data: chartData,
  237. options: commonOptions
  238. });
  239. } catch (error) {
  240. console.error('Error updating causals chart:', error);
  241. document.getElementById('causals-chart').insertAdjacentHTML(
  242. 'afterend',
  243. '<div class="alert alert-danger">Errore nel caricamento dei dati delle causali</div>'
  244. );
  245. }
  246. }
  247. });
  248. async function updateCoursesChart() {
  249. try {
  250. const courseData = await @this.getCourseMonthlyEarnings();
  251. console.log('Course data received:', courseData);
  252. if (window.coursesChart) {
  253. window.coursesChart.destroy();
  254. }
  255. const coursesCtx = document.getElementById('courses-chart').getContext('2d');
  256. const dashedLinesPlugin = {
  257. // Plugin definition unchanged
  258. id: 'dashedLines',
  259. beforeDatasetsDraw: (chart) => {
  260. const ctx = chart.ctx;
  261. const lineDataset = chart.data.datasets.find(d => d.type === 'line' && d.label === 'Pagamenti Attesi');
  262. const barDataset = chart.data.datasets.find(d => d.type === 'bar' && d.label === 'Pagamenti Effettuati');
  263. if (!lineDataset || !barDataset) return;
  264. const lineMeta = chart.getDatasetMeta(chart.data.datasets.indexOf(lineDataset));
  265. const barMeta = chart.getDatasetMeta(chart.data.datasets.indexOf(barDataset));
  266. if (!lineMeta.data.length || !barMeta.data.length) return;
  267. const missingData = lineDataset.missing || [];
  268. ctx.save();
  269. ctx.lineWidth = 2;
  270. ctx.setLineDash([8, 4]);
  271. ctx.strokeStyle = 'rgba(48, 51, 107, 0.3)';
  272. for (let i = 0; i < lineMeta.data.length; i++) {
  273. const linePoint = lineMeta.data[i];
  274. const barPoint = barMeta.data[i];
  275. if (!linePoint || !barPoint) continue;
  276. ctx.beginPath();
  277. ctx.moveTo(linePoint.x, linePoint.y);
  278. ctx.lineTo(linePoint.x, barPoint.y);
  279. ctx.stroke();
  280. if (missingData[i] && missingData[i] > 0) {
  281. const midY = (linePoint.y + barPoint.y) / 2;
  282. ctx.textAlign = 'center';
  283. ctx.font = '10px Arial';
  284. ctx.fillStyle = 'rgba(48, 51, 107, 0.8)';
  285. }
  286. }
  287. ctx.restore();
  288. }
  289. };
  290. window.coursesChart = new Chart(coursesCtx, {
  291. type: 'bar',
  292. plugins: [dashedLinesPlugin],
  293. data: courseData,
  294. options: {
  295. responsive: true,
  296. maintainAspectRatio: false,
  297. layout: {
  298. padding: {
  299. top: 20
  300. }
  301. },
  302. // Rest of options unchanged
  303. scales: {
  304. x: {
  305. grid: {
  306. display: false
  307. }
  308. },
  309. y: {
  310. display: true,
  311. beginAtZero: true,
  312. grid: {
  313. color: 'rgba(0, 0, 0, 0.1)',
  314. borderDash: [5, 5]
  315. },
  316. ticks: {
  317. callback: function (value) {
  318. return new Intl.NumberFormat('it-IT', {
  319. style: 'currency',
  320. currency: 'EUR',
  321. maximumFractionDigits: 0
  322. }).format(value);
  323. }
  324. }
  325. }
  326. },
  327. plugins: {
  328. legend: {
  329. display: true,
  330. position: 'top',
  331. align: 'center',
  332. labels: {
  333. usePointStyle: true,
  334. padding: 20
  335. }
  336. },
  337. title: {
  338. display: true,
  339. text: 'Pagamenti per corso',
  340. font: {
  341. size: 16
  342. }
  343. },
  344. tooltip: {
  345. callbacks: {
  346. label: function (context) {
  347. if (context.dataset.label === 'Pagamenti Attesi') {
  348. let parts = [];
  349. const participants = context.dataset.participants ? context.dataset.participants[context.dataIndex] : 0;
  350. parts.push('N° iscritti: ' + participants);
  351. const expectedAmount = context.parsed.y;
  352. parts.push('Pagamenti attesi: ' + new Intl.NumberFormat('it-IT', {
  353. style: 'currency',
  354. currency: 'EUR'
  355. }).format(expectedAmount));
  356. const missingAmount = context.dataset.missing ? context.dataset.missing[context.dataIndex] : 0;
  357. parts.push('Ancora da pagare: ' + new Intl.NumberFormat('it-IT', {
  358. style: 'currency',
  359. currency: 'EUR'
  360. }).format(missingAmount));
  361. return parts.join(' | ');
  362. }
  363. let label = context.dataset.label || '';
  364. if (label) {
  365. label += ': ';
  366. }
  367. if (context.parsed.y !== null) {
  368. label += new Intl.NumberFormat('it-IT', {
  369. style: 'currency',
  370. currency: 'EUR'
  371. }).format(context.parsed.y);
  372. }
  373. return label;
  374. }
  375. }
  376. }
  377. },
  378. elements: {
  379. point: {
  380. radius: 4,
  381. hoverRadius: 6
  382. },
  383. line: {
  384. tension: 0.4
  385. }
  386. }
  387. }
  388. });
  389. // Maintain chart dimensions
  390. coursesCtx.canvas.style.height = '250px';
  391. coursesCtx.canvas.style.maxHeight = '250px';
  392. } catch (error) {
  393. console.error('Error updating courses chart:', error);
  394. document.getElementById('courses-chart').insertAdjacentHTML(
  395. 'afterend',
  396. '<div class="alert alert-danger">Errore nel caricamento dei dati del corso</div>'
  397. );
  398. }
  399. }
  400. async function updateTesseratiChart() {
  401. try {
  402. const tesseratiData = await @this.getTesseratiData();
  403. if (window.tesseratiChart) {
  404. window.tesseratiChart.destroy();
  405. }
  406. const tesseratiCtx = document.getElementById('tesserati-chart').getContext('2d');
  407. window.tesseratiChart = new Chart(tesseratiCtx, {
  408. type: 'line',
  409. data: tesseratiData,
  410. options: {
  411. responsive: true,
  412. scales: {
  413. x: {
  414. title: {
  415. display: true,
  416. text: 'Anno Tesseramento'
  417. },
  418. grid: {
  419. display: false
  420. }
  421. },
  422. y: {
  423. title: {
  424. display: true,
  425. text: 'Numero di Tesserati'
  426. },
  427. beginAtZero: true,
  428. ticks: {
  429. precision: 0
  430. }
  431. }
  432. },
  433. plugins: {
  434. legend: {
  435. display: true,
  436. position: 'top'
  437. },
  438. title: {
  439. display: true,
  440. text: 'Andamento Tesserati per Anno',
  441. font: {
  442. size: 16
  443. }
  444. },
  445. tooltip: {
  446. callbacks: {
  447. label: function (context) {
  448. let label = context.dataset.label || '';
  449. if (label) {
  450. label += ': ';
  451. }
  452. if (context.parsed.y !== null) {
  453. label += context.parsed.y + ' tesserati';
  454. }
  455. return label;
  456. }
  457. }
  458. }
  459. }
  460. }
  461. });
  462. } catch (error) {
  463. console.error('Error updating tesserati chart:', error);
  464. document.getElementById('tesserati-chart').insertAdjacentHTML(
  465. 'afterend',
  466. '<div class="alert alert-danger">Errore nel caricamento dei dati tesserati</div>'
  467. );
  468. }
  469. }
  470. document.addEventListener('DOMContentLoaded', function () {
  471. const selectElement = document.querySelector('select[name="selectedCourse"]');
  472. if (selectElement) {
  473. selectElement.addEventListener('change', function () {
  474. const selectedValue = this.value;
  475. console.log('Selected course ID:', selectedValue);
  476. @this.set('selectedCourse', selectedValue);
  477. });
  478. }
  479. });
  480. function initializeChartSizes() {
  481. window.chartSizes = {
  482. 'monthly-in-out-chart': {
  483. height: document.getElementById('monthly-in-out-chart').style.height || 'auto',
  484. maxHeight: document.getElementById('monthly-in-out-chart').style.maxHeight || 'none'
  485. },
  486. 'causals-chart': {
  487. height: '300px',
  488. maxHeight: '300px'
  489. },
  490. 'tesserati-chart': {
  491. height: document.getElementById('tesserati-chart').style.height || 'auto',
  492. maxHeight: document.getElementById('tesserati-chart').style.maxHeight || 'none'
  493. },
  494. 'courses-chart': {
  495. height: '250px',
  496. maxHeight: '250px'
  497. }
  498. };
  499. }
  500. function restoreChartSize(chartId) {
  501. if (!window.chartSizes || !window.chartSizes[chartId]) return;
  502. const canvas = document.getElementById(chartId);
  503. if (canvas) {
  504. canvas.style.height = window.chartSizes[chartId].height;
  505. canvas.style.maxHeight = window.chartSizes[chartId].maxHeight;
  506. }
  507. }
  508. </script>
  509. @endpush