reports.blade.php 52 KB

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