reports.blade.php 48 KB

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