reports.blade.php 48 KB

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