reports.blade.php 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067
  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. afterDatasetsDraw: function (chart) {
  639. const ctx = chart.ctx;
  640. const meta0 = chart.getDatasetMeta(0);
  641. const meta1 = chart.getDatasetMeta(1);
  642. ctx.save();
  643. ctx.strokeStyle = 'rgba(239, 68, 68, 0.8)';
  644. ctx.lineWidth = 2;
  645. ctx.setLineDash([6, 4]);
  646. totalData.forEach((totalValue, index) => {
  647. const earnedValue = parseFloat(earnedData[index]) || 0;
  648. const totalVal = parseFloat(totalValue) || 0;
  649. if (totalVal > 0 && totalVal > earnedValue) {
  650. let barX;
  651. if (meta0.data[index] && earnedValue > 0) {
  652. barX = meta0.data[index].x;
  653. } else {
  654. const chartArea = chart.chartArea;
  655. const xScale = chart.scales.x;
  656. barX = xScale.getPixelForValue(index);
  657. }
  658. let startY;
  659. if (meta0.data[index] && earnedValue > 0) {
  660. startY = meta0.data[index].y;
  661. } else {
  662. const yScale = chart.scales.y;
  663. startY = yScale.getPixelForValue(0);
  664. }
  665. const linePoint = meta1.data[index];
  666. if (linePoint) {
  667. const endY = linePoint.y;
  668. ctx.beginPath();
  669. ctx.moveTo(barX, startY);
  670. ctx.lineTo(barX, endY);
  671. ctx.stroke();
  672. ctx.setLineDash([]);
  673. ctx.lineWidth = 1;
  674. ctx.beginPath();
  675. ctx.moveTo(barX - 3, endY);
  676. ctx.lineTo(barX + 3, endY);
  677. ctx.stroke();
  678. ctx.beginPath();
  679. ctx.moveTo(barX - 3, startY);
  680. ctx.lineTo(barX + 3, startY);
  681. ctx.stroke();
  682. ctx.setLineDash([6, 4]);
  683. ctx.lineWidth = 2;
  684. }
  685. }
  686. });
  687. ctx.restore();
  688. }
  689. };
  690. Chart.register(verticalMissingLinesPlugin);
  691. this.charts[chartId] = new Chart(ctx, {
  692. type: 'bar',
  693. data: {
  694. labels: courseData.labels,
  695. datasets: [
  696. {
  697. label: 'Pagamenti Effettuati',
  698. backgroundColor: earnedGradient,
  699. borderColor: 'rgba(16, 185, 129, 1)',
  700. borderWidth: 0,
  701. borderRadius: 8,
  702. borderSkipped: false,
  703. data: earnedData,
  704. type: 'bar',
  705. order: 2
  706. },
  707. {
  708. label: 'Pagamenti Attesi',
  709. backgroundColor: 'transparent',
  710. borderColor: 'rgba(59, 130, 246, 1)',
  711. borderWidth: 3,
  712. pointBackgroundColor: 'rgba(59, 130, 246, 1)',
  713. pointBorderColor: '#ffffff',
  714. pointBorderWidth: 3,
  715. pointRadius: 7,
  716. pointHoverRadius: 9,
  717. data: totalData,
  718. type: 'line',
  719. tension: 0.3,
  720. order: 1,
  721. participantData: participantData
  722. }
  723. ]
  724. },
  725. options: {
  726. responsive: true,
  727. maintainAspectRatio: false,
  728. interaction: {
  729. mode: 'index',
  730. intersect: false,
  731. },
  732. layout: {
  733. padding: {
  734. top: 20,
  735. right: 20,
  736. bottom: 20,
  737. left: 10
  738. }
  739. },
  740. scales: {
  741. x: {
  742. grid: {
  743. display: false
  744. },
  745. ticks: {
  746. font: {
  747. weight: '600',
  748. size: 13
  749. },
  750. color: '#6b7280'
  751. },
  752. border: {
  753. display: false
  754. }
  755. },
  756. y: {
  757. beginAtZero: true,
  758. grid: {
  759. color: 'rgba(156, 163, 175, 0.15)',
  760. borderDash: [3, 3]
  761. },
  762. border: {
  763. display: false
  764. },
  765. ticks: {
  766. font: {
  767. size: 12,
  768. weight: '500'
  769. },
  770. color: '#6b7280',
  771. callback: function (value) {
  772. return '€' + new Intl.NumberFormat('it-IT', {
  773. minimumFractionDigits: 0,
  774. maximumFractionDigits: 0
  775. }).format(value);
  776. }
  777. }
  778. }
  779. },
  780. plugins: {
  781. legend: {
  782. display: true,
  783. position: 'top',
  784. labels: {
  785. usePointStyle: true,
  786. padding: 15,
  787. font: { weight: '500', size: 12 },
  788. generateLabels: function (chart) {
  789. const original = Chart.defaults.plugins.legend.labels.generateLabels(chart);
  790. original.push({
  791. text: 'Pagamenti Mancanti',
  792. fillStyle: 'transparent',
  793. strokeStyle: 'rgba(239, 68, 68, 0.8)',
  794. lineDash: [6, 4],
  795. lineWidth: 2,
  796. pointStyle: 'line'
  797. });
  798. return original;
  799. }
  800. }
  801. },
  802. tooltip: {
  803. backgroundColor: 'rgba(255, 255, 255, 0.98)',
  804. titleColor: '#111827',
  805. bodyColor: '#374151',
  806. borderColor: 'rgba(229, 231, 235, 0.8)',
  807. borderWidth: 1,
  808. cornerRadius: 12,
  809. titleFont: {
  810. weight: 'bold',
  811. size: 15
  812. },
  813. bodyFont: {
  814. size: 14,
  815. weight: '500'
  816. },
  817. padding: 16,
  818. boxPadding: 8,
  819. usePointStyle: true,
  820. displayColors: true,
  821. callbacks: {
  822. title: function (context) {
  823. return context[0].label;
  824. },
  825. label: function (context) {
  826. let label = context.dataset.label + ': €' +
  827. new Intl.NumberFormat('it-IT').format(context.parsed.y);
  828. if (context.dataset.label === 'Pagamenti Effettuati') {
  829. const earnedValue = parseFloat(context.parsed.y) || 0;
  830. const totalValue = parseFloat(totalData[context.dataIndex]) || 0;
  831. const missingValue = Math.max(0, totalValue - earnedValue);
  832. if (participantData[context.dataIndex]) {
  833. label += '\n👥 Partecipanti: ' + participantData[context.dataIndex];
  834. }
  835. if (missingValue > 0) {
  836. label += '\n🔴 Mancanti: €' + new Intl.NumberFormat('it-IT').format(missingValue);
  837. }
  838. }
  839. if (context.dataset.label === 'Pagamenti Attesi' && participantData[context.dataIndex]) {
  840. label += '\n👥 Partecipanti: ' + participantData[context.dataIndex];
  841. }
  842. return label;
  843. }
  844. }
  845. }
  846. },
  847. animation: {
  848. duration: 1500,
  849. easing: 'easeOutCubic'
  850. },
  851. elements: {
  852. bar: {
  853. borderRadius: {
  854. topLeft: 8,
  855. topRight: 8,
  856. bottomLeft: 0,
  857. bottomRight: 0
  858. }
  859. },
  860. line: {
  861. borderCapStyle: 'round',
  862. borderJoinStyle: 'round'
  863. },
  864. point: {
  865. hoverBorderWidth: 4,
  866. borderWidth: 3
  867. }
  868. }
  869. }
  870. });
  871. }).catch(error => {
  872. console.error('Error calling getCourseData:', error);
  873. });
  874. },
  875. updateCourseTable: function (container, tableData) {
  876. if (!container || !tableData) return;
  877. let tableHtml = `
  878. <div class="course-table">
  879. <div class="table-header">
  880. <div class="table-cell month">Mese</div>
  881. <div class="table-cell participants">👥</div>
  882. <div class="table-cell delta">Mancanti</div>
  883. <div class="table-cell percentage">%</div>
  884. </div>
  885. `;
  886. tableData.forEach(row => {
  887. const earned = parseFloat(row.earned) || 0;
  888. const total = parseFloat(row.total) || 0;
  889. const delta = Math.max(0, total - earned);
  890. // Fix percentage calculation and display logic
  891. let percentage = 0;
  892. let percentageDisplay = '—';
  893. let percentageClass = 'neutral';
  894. if (total > 0) {
  895. percentage = Math.round((earned / total) * 100);
  896. percentageDisplay = percentage + '%';
  897. // Color based on completion
  898. if (percentage >= 100) {
  899. percentageClass = 'good';
  900. } else if (percentage >= 80) {
  901. percentageClass = 'warning';
  902. } else {
  903. percentageClass = 'bad';
  904. }
  905. }
  906. // Delta styling: positive when delta is 0 (fully paid), negative when there's missing amount
  907. const deltaClass = (total > 0 && delta === 0) ? 'positive' :
  908. (delta > 0) ? 'negative' : 'neutral';
  909. tableHtml += `
  910. <div class="table-row">
  911. <div class="table-cell month">${row.month}</div>
  912. <div class="table-cell participants">${row.participants}</div>
  913. <div class="table-cell delta ${deltaClass}">€${new Intl.NumberFormat('it-IT').format(delta)}</div>
  914. <div class="table-cell percentage ${percentageClass}">${percentageDisplay}</div>
  915. </div>
  916. `;
  917. });
  918. tableHtml += '</div>';
  919. container.innerHTML = tableHtml;
  920. },
  921. updateCourseChart: function () {
  922. if (this.selectedCourse) {
  923. const seasonFilter = @json($seasonFilter);
  924. const seasonKey = seasonFilter.replace('-', '');
  925. this.createCourseChartWithValue(this.selectedCourse);
  926. }
  927. }
  928. };
  929. document.addEventListener('DOMContentLoaded', function () {
  930. setTimeout(() => {
  931. window.ReportsChartManager.updateMainCharts();
  932. }, 100);
  933. });
  934. document.addEventListener('livewire:navigated', function () {
  935. setTimeout(() => {
  936. window.ReportsChartManager.updateMainCharts();
  937. }, 100);
  938. });
  939. document.addEventListener('livewire:updated', function (event) {
  940. console.log('Livewire updated, waiting for component to fully update...');
  941. setTimeout(() => {
  942. console.log('Now updating charts after delay');
  943. window.ReportsChartManager.forceUpdateCharts();
  944. }, 800);
  945. });
  946. document.addEventListener('livewire:load', function () {
  947. Livewire.on('courseSelected', (courseId) => {
  948. console.log('Course selected event received:', courseId);
  949. setTimeout(() => {
  950. window.ReportsChartManager.createCourseChartWithValue(courseId);
  951. }, 200);
  952. });
  953. Livewire.on('chartsUpdated', () => {
  954. console.log('Charts updated event received from Livewire');
  955. setTimeout(() => {
  956. window.ReportsChartManager.forceUpdateCharts();
  957. }, 200);
  958. });
  959. });
  960. </script>
  961. </div>