reports.blade.php 59 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292
  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="/css/chart-reports.css">
  5. <div class="dashboard-container">
  6. <div class="chart-row">
  7. <div class="chart-card">
  8. <div class="chart-header">
  9. <h3 class="chart-title">Tesserati per Stagione</h3>
  10. </div>
  11. <div class="chart-body">
  12. <div style="display: grid; grid-template-columns: 1fr 500px; gap: 1rem; align-items: start;">
  13. <div class="chart-container">
  14. <canvas id="members-chart"></canvas>
  15. </div>
  16. <div class="yearly-table-container" id="yearly-table">
  17. </div>
  18. </div>
  19. </div>
  20. </div>
  21. </div>
  22. <div class="controls-section">
  23. <div class="control-group">
  24. <label for="season-filter">Stagione di Riferimento:</label>
  25. <select class="form-select" wire:model="seasonFilter" wire:change="updateCharts">
  26. @foreach($this->getAvailableSeasons() as $season)
  27. <option value="{{ $season }}">{{ $season }}</option>
  28. @endforeach
  29. </select>
  30. </div>
  31. </div>
  32. @php
  33. $summary = $this->getYearlySummary();
  34. @endphp
  35. <div class="summary-cards">
  36. <div class="summary-card income">
  37. <h3>Entrate Totali</h3>
  38. <div class="value">€{{ number_format($summary['totalIncome'], 2, ',', '.') }}</div>
  39. </div>
  40. <div class="summary-card expense">
  41. <h3>Uscite Totali</h3>
  42. <div class="value">€{{ number_format($summary['totalExpenses'], 2, ',', '.') }}</div>
  43. </div>
  44. <div class="summary-card delta {{ $summary['delta'] < 0 ? 'negative' : '' }}">
  45. <h3>Bilancio Netto</h3>
  46. <div class="value">€{{ number_format($summary['delta'], 2, ',', '.') }}</div>
  47. </div>
  48. </div>
  49. <div wire:ignore>
  50. <div class="chart-row">
  51. <div class="chart-card">
  52. <div class="chart-header">
  53. <h3 class="chart-title">Entrate e Uscite Mensili - <span id="monthly-season-title">{{ $seasonFilter }}</span></h3>
  54. </div>
  55. <div class="chart-body">
  56. <div style="display: grid; grid-template-columns: 1fr 300px; align-items: start;">
  57. <div class="chart-container">
  58. <canvas id="monthly-chart-{{ str_replace('-', '', $seasonFilter) }}"></canvas>
  59. </div>
  60. <div class="monthly-table-container" id="monthly-table">
  61. </div>
  62. </div>
  63. </div>
  64. </div>
  65. </div>
  66. <div class="chart-row">
  67. <div class="chart-card">
  68. <div class="chart-header">
  69. <h3 class="chart-title">Causali Performanti - <span id="causals-season-title">{{ $seasonFilter }}</span></h3>
  70. </div>
  71. <div class="chart-body">
  72. <div style="display: grid; grid-template-columns: 1fr 700px; gap: 1rem; align-items: start;">
  73. <div class="causals-table-container" id="causals-table">
  74. </div>
  75. <div class="chart-container">
  76. <canvas id="causals-chart-{{ str_replace('-', '', $seasonFilter) }}"></canvas>
  77. </div>
  78. </div>
  79. </div>
  80. </div>
  81. </div>
  82. <div class="chart-row">
  83. <div class="chart-card">
  84. <div class="chart-header">
  85. <h3 class="chart-title">Tesserati per Stagione</h3>
  86. </div>
  87. <div class="chart-body">
  88. <div style="display: grid; grid-template-columns: 1fr 500px; gap: 1rem; align-items: start;">
  89. <div class="chart-container">
  90. <canvas id="members-chart2"></canvas>
  91. </div>
  92. <div class="members-table-container" id="members-table">
  93. </div>
  94. </div>
  95. </div>
  96. </div>
  97. </div>
  98. </div>
  99. @if(false)
  100. <div class="chart-row">
  101. <div class="chart-card modern-course-card">
  102. <div class="chart-header">
  103. <h3 class="chart-title">Analisi Corsi</h3>
  104. </div>
  105. <div class="chart-body">
  106. <div class="course-controls">
  107. <div class="control-group">
  108. <label>Seleziona Corso ({{ $seasonFilter }}):</label>
  109. <select class="form-select modern-select" wire:model.live="selectedCourse">
  110. <option value="">Seleziona un Corso</option>
  111. @foreach($this->getCoursesForSelect() as $course)
  112. <option value="{{ $course['id'] }}">{{ $course['full_name'] }}</option>
  113. @endforeach
  114. </select>
  115. </div>
  116. </div>
  117. @if($selectedCourse)
  118. <div wire:ignore wire:key="course-chart-{{ $seasonFilter }}-{{ $selectedCourse }}">
  119. <div class="modern-chart-layout">
  120. <div class="course-delta-table" id="course-delta-table-{{ str_replace('-', '', $seasonFilter) }}-{{ $selectedCourse }}">
  121. </div>
  122. <div class="modern-chart-container">
  123. <canvas id="courses-chart-{{ str_replace('-', '', $seasonFilter) }}-{{ $selectedCourse }}"></canvas>
  124. </div>
  125. </div>
  126. </div>
  127. @else
  128. <div class="chart-placeholder">
  129. <div style="text-align: center;">
  130. <div style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.3;">📊</div>
  131. <p style="font-size: 1.25rem; font-weight: 600; margin: 0;">Seleziona un corso per
  132. visualizzare il grafico</p>
  133. <p style="font-size: 1rem; opacity: 0.7; margin-top: 0.5rem;">Usa il menu a tendina sopra
  134. per scegliere un corso</p>
  135. </div>
  136. </div>
  137. @endif
  138. </div>
  139. </div>
  140. </div>
  141. @endif
  142. </div>
  143. <script>
  144. window.ReportsChartManager = window.ReportsChartManager || {
  145. charts: {},
  146. currentSeason: null,
  147. destroyChart: function (chartId) {
  148. if (this.charts[chartId]) {
  149. this.charts[chartId].destroy();
  150. delete this.charts[chartId];
  151. }
  152. },
  153. destroyAllCharts: function () {
  154. Object.keys(this.charts).forEach(chartId => {
  155. this.destroyChart(chartId);
  156. });
  157. },
  158. destroySeasonCharts: function (oldSeasonKey) {
  159. const chartsToDestroy = Object.keys(this.charts).filter(chartId =>
  160. chartId.includes(oldSeasonKey)
  161. );
  162. chartsToDestroy.forEach(chartId => this.destroyChart(chartId));
  163. },
  164. updateMainCharts: function () {
  165. console.log('=== updateMainCharts called ===');
  166. const seasonFilter = @this.get('seasonFilter');
  167. const monthlyTitle = document.getElementById('monthly-season-title');
  168. const causalsTitle = document.getElementById('causals-season-title');
  169. if (monthlyTitle) {
  170. monthlyTitle.textContent = seasonFilter;
  171. }
  172. if (causalsTitle) {
  173. causalsTitle.textContent = seasonFilter;
  174. }
  175. const originalSeasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
  176. console.log('Using original season key for canvas IDs:', originalSeasonKey);
  177. @this.call('getMonthlyTotals').then(monthlyData => {
  178. console.log('Got monthly data:', monthlyData);
  179. this.createMonthlyChart(originalSeasonKey, monthlyData);
  180. this.updateMonthlyTable(monthlyData);
  181. });
  182. @this.call('getYearlyTotals').then(yearlyData => {
  183. console.log('Got yearly data:', yearlyData);
  184. this.updateYearlyTable(yearlyData);
  185. });
  186. @this.call('getTopCausalsByAmount').then(causalsData => {
  187. console.log('Got causals data:', causalsData);
  188. this.createCausalsChart(originalSeasonKey, causalsData);
  189. });
  190. @this.call('getTesseratiData').then(membersData => {
  191. console.log('Got members data:', membersData);
  192. this.createMembersChart(originalSeasonKey, membersData);
  193. this.updateMembersTable(membersData);
  194. });
  195. },
  196. forceUpdateCharts: function () {
  197. console.log('Force updating charts...');
  198. this.currentSeason = null;
  199. this.updateMainCharts();
  200. },
  201. createMonthlyChart: function (seasonKey, monthlyData) {
  202. const chartId = `monthly-chart-${seasonKey}`;
  203. const canvas = document.getElementById(chartId);
  204. if (!canvas) {
  205. console.error('Canvas not found for ID:', chartId);
  206. return;
  207. }
  208. this.destroyChart(chartId);
  209. const ctx = canvas.getContext('2d');
  210. const incomeGradient = ctx.createLinearGradient(0, 0, 0, 400);
  211. incomeGradient.addColorStop(0, 'rgba(0, 184, 148, 1)');
  212. incomeGradient.addColorStop(1, 'rgba(0, 184, 148, 1)');
  213. const expenseGradient = ctx.createLinearGradient(0, 0, 0, 400);
  214. expenseGradient.addColorStop(0, 'rgba(255, 107, 107, 1)');
  215. expenseGradient.addColorStop(1, 'rgba(255, 107, 107, 1)');
  216. this.charts[chartId] = new Chart(ctx, {
  217. type: 'bar',
  218. data: {
  219. labels: monthlyData.labels,
  220. datasets: [
  221. {
  222. label: 'Entrate',
  223. data: monthlyData.datasets[0].data,
  224. backgroundColor: incomeGradient,
  225. borderColor: '#00b894',
  226. borderWidth: 0,
  227. borderRadius: 0,
  228. borderSkipped: false,
  229. barThickness: "flex",
  230. barPercentage: 0.65,
  231. categoryPercentage: 0.4,
  232. },
  233. {
  234. label: 'Uscite',
  235. data: monthlyData.datasets[1].data,
  236. backgroundColor: expenseGradient,
  237. borderColor: '#ff6b6b',
  238. borderWidth: 0,
  239. borderRadius: 0,
  240. borderSkipped: false,
  241. barThickness: "flex",
  242. barPercentage: 0.65,
  243. categoryPercentage: 0.4,
  244. }
  245. ]
  246. },
  247. options: {
  248. responsive: true,
  249. maintainAspectRatio: false,
  250. interaction: {
  251. mode: 'index',
  252. intersect: false,
  253. },
  254. plugins: {
  255. legend: {
  256. position: 'bottom',
  257. labels: {
  258. usePointStyle: true,
  259. padding: 20,
  260. pointStyle: "rect",
  261. font: { weight: '500' }
  262. }
  263. },
  264. tooltip: {
  265. backgroundColor: 'rgba(255, 255, 255, 1)',
  266. titleColor: '#212529',
  267. bodyColor: '#495057',
  268. borderColor: '#e9ecef',
  269. borderWidth: 2,
  270. cornerRadius: 0,
  271. callbacks: {
  272. label: function (context) {
  273. return context.dataset.label + ': €' +
  274. new Intl.NumberFormat('it-IT').format(context.parsed.y);
  275. }
  276. }
  277. },
  278. },
  279. scales: {
  280. x: {
  281. grid: { display: false },
  282. ticks: { font: { weight: '500' } }
  283. },
  284. y: {
  285. beginAtZero: true,
  286. grid: { color: 'rgba(0, 0, 0, 0.05)' },
  287. ticks: {
  288. callback: function (value) {
  289. return '€' + new Intl.NumberFormat('it-IT').format(value);
  290. }
  291. }
  292. }
  293. },
  294. elements: {
  295. bar: {
  296. borderRadius: {
  297. topLeft: 12,
  298. topRight: 12,
  299. bottomLeft: 0,
  300. bottomRight: 0
  301. }
  302. }
  303. },
  304. animation: {
  305. duration: 1000,
  306. easing: 'easeOutQuart'
  307. }
  308. }
  309. });
  310. },
  311. createCausalsChart: function (seasonKey, causalsData) {
  312. const chartId = `causals-chart-${seasonKey}`;
  313. const canvas = document.getElementById(chartId);
  314. if (!canvas) return;
  315. this.destroyChart(chartId);
  316. const ctx = canvas.getContext('2d');
  317. const colors = [
  318. 'rgba(59, 91, 219, 0.8)',
  319. 'rgba(0, 184, 148, 0.8)',
  320. 'rgba(34, 184, 207, 0.8)',
  321. 'rgba(255, 212, 59, 0.8)',
  322. 'rgba(255, 107, 107, 0.8)',
  323. 'rgba(142, 68, 173, 0.8)',
  324. 'rgba(230, 126, 34, 0.8)',
  325. 'rgba(149, 165, 166, 0.8)',
  326. 'rgba(241, 196, 15, 0.8)',
  327. 'rgba(231, 76, 60, 0.8)'
  328. ];
  329. const dataValues = causalsData.inData.map(item => parseFloat(item.value));
  330. const total = dataValues.reduce((sum, value) => sum + value, 0);
  331. // alwaysTooltip
  332. const alwaysShowTooltip = {
  333. id: "alwaysShowTooltip",
  334. afterDraw(chart) {
  335. const { ctx } = chart;
  336. ctx.save();
  337. const linePad = 12; // distanza dal bordo della ciambella
  338. const textOffset = 20; // distanza fissa del testo dal bordo
  339. const lineGap = 8; // margine tra linea e testo
  340. const lineWidth = 1;
  341. chart.data.datasets.forEach((dataset, di) => {
  342. const meta = chart.getDatasetMeta(di);
  343. const total = dataset.data.reduce((s, v) => s + v, 0);
  344. meta.data.forEach((datapoint, index) => {
  345. const value = dataset.data[index];
  346. const perc = total > 0 ? (value / total) * 100 : 0;
  347. const text = `${perc.toFixed(1)}%`;
  348. // calcolo angolo e anchor
  349. const { x, y } = datapoint.tooltipPosition();
  350. const coords = outsideLabelPoint({
  351. cx: datapoint.x,
  352. cy: datapoint.y,
  353. px: x,
  354. py: y,
  355. radius: datapoint.outerRadius,
  356. pad: linePad,
  357. });
  358. const ux = Math.cos(coords.theta);
  359. const uy = Math.sin(coords.theta);
  360. const ax = coords.anchor.x;
  361. const ay = coords.anchor.y;
  362. // centro testo
  363. const tx = ax + ux * textOffset;
  364. const ty = ay + uy * textOffset;
  365. // fine della linea = poco prima del testo
  366. const lx = tx - ux * lineGap;
  367. const ly = ty - uy * lineGap;
  368. // linea: anchor -> vicino al testo
  369. ctx.lineWidth = lineWidth;
  370. ctx.strokeStyle = datapoint.options.borderColor;
  371. ctx.beginPath();
  372. ctx.moveTo(ax, ay);
  373. ctx.lineTo(lx, ly);
  374. ctx.stroke();
  375. // testo
  376. ctx.font = "12px greycliff-cf, sans-serif";
  377. ctx.fillStyle = datapoint.options.borderColor;
  378. ctx.textAlign = "center";
  379. ctx.textBaseline = "middle";
  380. ctx.fillText(text, tx, ty);
  381. });
  382. });
  383. ctx.restore();
  384. },
  385. };
  386. function outsideLabelPoint({ cx, cy, px, py, radius, pad = 20 }) {
  387. const theta = Math.atan2(py - cy, px - cx); // angolo rad
  388. const anchor = { // punto sul bordo del pie
  389. x: cx + radius * Math.cos(theta),
  390. y: cy + radius * Math.sin(theta)
  391. };
  392. const label = { // punto esterno per il tooltip/etichetta
  393. x: cx + (radius + pad) * Math.cos(theta),
  394. y: cy + (radius + pad) * Math.sin(theta)
  395. };
  396. const align = {
  397. textAlign: Math.cos(theta) >= 0 ? 'left' : 'right',
  398. textBaseline: Math.sin(theta) > 0 ? 'top' : 'bottom'
  399. };
  400. return { theta, anchor, label, align };
  401. }
  402. this.charts[chartId] = new Chart(ctx, {
  403. type: 'doughnut',
  404. data: {
  405. labels: causalsData.inLabels,
  406. datasets: [{
  407. label: 'Importo',
  408. data: dataValues,
  409. backgroundColor: colors,
  410. borderColor: colors.map(color => color.replace('0.8', '1')),
  411. borderWidth: 2,
  412. hoverOffset: 8
  413. }]
  414. },
  415. options: {
  416. responsive: true,
  417. maintainAspectRatio: false,
  418. cutout: '30%',
  419. layout: {
  420. padding: {
  421. top: 30,
  422. right: 30,
  423. bottom: 30,
  424. left: 30
  425. }
  426. },
  427. plugins: {
  428. legend: {
  429. display: false
  430. },
  431. tooltip: {
  432. backgroundColor: 'rgba(255, 255, 255, 1)',
  433. titleColor: '#212529',
  434. bodyColor: '#495057',
  435. borderColor: '#e9ecef',
  436. borderWidth: 2,
  437. cornerRadius: 0,
  438. titleFont: {
  439. size: 13,
  440. weight: 'bold'
  441. },
  442. bodyFont: {
  443. size: 12,
  444. weight: '500'
  445. },
  446. padding: 12,
  447. callbacks: {
  448. title: function (context) {
  449. return context[0].label;
  450. },
  451. label: function (context) {
  452. const value = context.raw;
  453. const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
  454. return [
  455. `Importo: €${new Intl.NumberFormat('it-IT', {
  456. minimumFractionDigits: 2,
  457. maximumFractionDigits: 2
  458. }).format(value)}`,
  459. `Percentuale: ${percentage}%`
  460. ];
  461. }
  462. }
  463. },
  464. },
  465. animation: {
  466. animateRotate: true,
  467. duration: 1000
  468. }
  469. },
  470. plugins: [alwaysShowTooltip]
  471. });
  472. this.updateCausalsTable(causalsData, dataValues, total);
  473. },
  474. updateCausalsTable: function (causalsData, dataValues, total) {
  475. const container = document.getElementById('causals-table');
  476. if (!container) return;
  477. const colors = [
  478. '#3b5bdb', '#00b894', '#22b8cf', '#ffd43b', '#ff6b6b',
  479. '#8e44ad', '#e67e22', '#95a5a6', '#f1c40f', '#e74c3c'
  480. ];
  481. let tableHtml = `
  482. <div class="causals-table compact">
  483. <div class="table-header">
  484. <div class="table-cell causale">Causale</div>
  485. <div class="table-cell euro">Importo</div>
  486. <div class="table-cell percent">%</div>
  487. </div>
  488. `;
  489. causalsData.inLabels.forEach((label, index) => {
  490. const value = dataValues[index] || 0;
  491. const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
  492. const color = colors[index % colors.length];
  493. tableHtml += `
  494. <div class="table-row">
  495. <div class="table-cell causale">
  496. <span class="causale-indicator" style="background-color: ${color}"></span>
  497. ${label}
  498. </div>
  499. <div class="table-cell euro">€${new Intl.NumberFormat('it-IT', {
  500. minimumFractionDigits: 2,
  501. maximumFractionDigits: 2
  502. }).format(value)}</div>
  503. <div class="table-cell percent">${percentage}%</div>
  504. </div>
  505. `;
  506. });
  507. tableHtml += '</div>';
  508. container.innerHTML = tableHtml;
  509. },
  510. createMembersChart: function (seasonKey, membersData) {
  511. const chartId = `members-chart`;
  512. const chartId2 = `members-chart2`;
  513. const canvas = document.getElementById(chartId);
  514. const canvas2 = document.getElementById(chartId2);
  515. if (!canvas) return;
  516. this.destroyChart(chartId);
  517. this.destroyChart(chartId2);
  518. const ctx = canvas.getContext('2d');
  519. const ctx2 = canvas2.getContext('2d');
  520. const gradient = ctx.createLinearGradient(0, 0, 0, 400);
  521. gradient.addColorStop(0, 'rgba(59, 91, 219, 0.3)');
  522. gradient.addColorStop(1, 'rgba(59, 91, 219, 0.05)');
  523. const processedDatasets = membersData.datasets.map((dataset, index) => {
  524. if (dataset.label === 'Totale Membri Tesserati') {
  525. return {
  526. ...dataset,
  527. // backgroundColor: gradient,
  528. backgroundColor: dataset.borderColor,
  529. borderColor: dataset.borderColor,
  530. borderWidth: 0,
  531. type: 'bar',
  532. order: 1,
  533. fill: true,
  534. barThickness: "flex",
  535. barPercentage: 0.65,
  536. categoryPercentage: 0.4,
  537. };
  538. } else {
  539. return {
  540. ...dataset,
  541. backgroundColor: dataset.borderColor,
  542. borderColor: dataset.borderColor,
  543. borderWidth: 0,
  544. type: 'bar',
  545. order: 2,
  546. fill: true,
  547. barThickness: "flex",
  548. barPercentage: 0.65,
  549. categoryPercentage: 0.4,
  550. };
  551. }
  552. });
  553. let options = {
  554. type: 'bar',
  555. data: {
  556. labels: membersData.labels,
  557. datasets: processedDatasets
  558. },
  559. options: {
  560. responsive: true,
  561. maintainAspectRatio: false,
  562. interaction: {
  563. mode: 'index',
  564. intersect: false,
  565. },
  566. plugins: {
  567. legend: {
  568. position: 'bottom',
  569. labels: {
  570. usePointStyle: true,
  571. padding: 20,
  572. pointStyle: "rect",
  573. font: { weight: '500' }
  574. }
  575. },
  576. tooltip: {
  577. backgroundColor: 'rgba(255, 255, 255, 1)',
  578. titleColor: '#212529',
  579. bodyColor: '#495057',
  580. borderColor: '#e9ecef',
  581. borderWidth: 2,
  582. cornerRadius: 0,
  583. callbacks: {
  584. title: function (context) {
  585. return 'Stagione: ' + context[0].label;
  586. },
  587. label: function (context) {
  588. return context.dataset.label + ': ' + context.parsed.y;
  589. }
  590. }
  591. }
  592. },
  593. scales: {
  594. x: {
  595. grid: { display: false },
  596. ticks: {
  597. font: { weight: '500' }
  598. }
  599. },
  600. y: {
  601. beginAtZero: true,
  602. grid: { color: 'rgba(0, 0, 0, 0.05)' },
  603. ticks: {
  604. precision: 0,
  605. callback: function (value) {
  606. return Math.floor(value); // Ensure integer values
  607. }
  608. }
  609. }
  610. },
  611. animation: {
  612. duration: 1000,
  613. easing: 'easeOutQuart'
  614. }
  615. }
  616. };
  617. this.charts[chartId] = new Chart(ctx, options);
  618. this.charts[chartId2] = new Chart(ctx2, options);
  619. },
  620. updateMonthlyTable: function (monthlyData) {
  621. const container = document.getElementById('monthly-table');
  622. if (!container) return;
  623. const incomeData = monthlyData.datasets[0].data;
  624. const expenseData = monthlyData.datasets[1].data;
  625. const monthNames = monthlyData.labels;
  626. let tableHtml = `
  627. <div class="monthly-table">
  628. <div class="table-header">
  629. <div class="table-cell month">Mese</div>
  630. <div class="table-cell">Entrate</div>
  631. <div class="table-cell">Uscite</div>
  632. <div class="table-cell">Delta</div>
  633. </div>
  634. `;
  635. monthNames.forEach((month, index) => {
  636. const income = parseFloat(incomeData[index] || 0);
  637. const expense = parseFloat(expenseData[index] || 0);
  638. const net = income - expense;
  639. const rowClass = net < 0 ? 'negative' : (net > 0 ? 'positive' : 'neutral');
  640. tableHtml += `
  641. <div class="table-row ${rowClass}">
  642. <div class="table-cell month-name">${month}</div>
  643. <div class="table-cell income">€${new Intl.NumberFormat('it-IT').format(income)}</div>
  644. <div class="table-cell expense">€${new Intl.NumberFormat('it-IT').format(expense)}</div>
  645. <div class="table-cell net">€${new Intl.NumberFormat('it-IT').format(net)}</div>
  646. </div>
  647. `;
  648. });
  649. tableHtml += '</div>';
  650. container.innerHTML = tableHtml;
  651. },
  652. updateYearlyTable: function (yearlyData) {
  653. const container = document.getElementById('yearly-table');
  654. if (!container) return;
  655. const incomeData = yearlyData.datasets[0].data;
  656. const expenseData = yearlyData.datasets[1].data;
  657. const years = yearlyData.labels;
  658. let tableHtml = `
  659. <div class="monthly-table">
  660. <div class="table-header">
  661. <div class="table-cell month">Anno</div>
  662. <div class="table-cell">Entrate</div>
  663. <div class="table-cell">Uscite</div>
  664. <div class="table-cell">Delta</div>
  665. </div>
  666. `;
  667. years.forEach((year, index) => {
  668. const income = parseFloat(incomeData[index] || 0);
  669. const expense = parseFloat(expenseData[index] || 0);
  670. const net = income - expense;
  671. const rowClass = net < 0 ? 'negative' : (net > 0 ? 'positive' : 'neutral');
  672. tableHtml += `
  673. <div class="table-row ${rowClass}">
  674. <div class="table-cell month-name">${year}</div>
  675. <div class="table-cell income">€${new Intl.NumberFormat('it-IT').format(income)}</div>
  676. <div class="table-cell expense">€${new Intl.NumberFormat('it-IT').format(expense)}</div>
  677. <div class="table-cell net">€${new Intl.NumberFormat('it-IT').format(net)}</div>
  678. </div>
  679. `;
  680. });
  681. tableHtml += '</div>';
  682. container.innerHTML = tableHtml;
  683. },
  684. updateMembersTable: function (membersData) {
  685. const container = document.getElementById('members-table');
  686. if (!container) return;
  687. const seasonLabels = membersData.labels;
  688. const totalDataset = membersData.datasets.find(d => d.label === 'Totale Membri Tesserati');
  689. const cardTypeDatasets = membersData.datasets.filter(d => d.label !== 'Totale Membri Tesserati');
  690. const memberCounts = totalDataset ? totalDataset.data : [];
  691. let tableHtml = `
  692. <div class="members-table">
  693. <div class="table-header">
  694. <div class="table-cell">Stagione</div>
  695. <div class="table-cell">Totale</div>
  696. <div class="table-cell">Variazione</div>
  697. <div class="table-cell">Ente</div>
  698. </div>
  699. `;
  700. seasonLabels.forEach((season, index) => {
  701. const current = parseInt(memberCounts[index] || 0);
  702. const previous = index > 0 ? parseInt(memberCounts[index - 1] || 0) : 0;
  703. const variation = index > 0 ? current - previous : 0;
  704. const variationPercent = previous > 0 ? Math.round((variation / previous) * 100 * 10) / 10 : 0;
  705. const rowClass = variation > 0 ? 'positive' : (variation < 0 ? 'negative' : 'neutral');
  706. let variationText = '—';
  707. if (index > 0) {
  708. if (variation > 0) {
  709. variationText = `<span class="variation-positive">+${variation} (+${variationPercent}%)</span>`;
  710. } else if (variation < 0) {
  711. variationText = `<span class="variation-negative">${variation} (${variationPercent}%)</span>`;
  712. } else {
  713. variationText = `<span class="variation-neutral">${variation}</span>`;
  714. }
  715. }
  716. // Build card type breakdown
  717. let cardTypeBreakdown = '';
  718. cardTypeDatasets.forEach((dataset, datasetIndex) => {
  719. const count = dataset.data[index] || 0;
  720. if (count > 0) {
  721. const color = dataset.borderColor || '#6b7280';
  722. cardTypeBreakdown += `
  723. <div class="card-type-item">
  724. <span class="card-type-indicator" style="background-color: ${color}"></span>
  725. <span class="card-type-name">${dataset.label}</span>
  726. <span class="card-type-count">${count}</span>
  727. </div>
  728. `;
  729. }
  730. });
  731. if (!cardTypeBreakdown) {
  732. cardTypeBreakdown = '<div class="no-card-types">Nessun dettaglio</div>';
  733. }
  734. tableHtml += `
  735. <div class="table-row ${rowClass}">
  736. <div class="table-cell season-name">${season}</div>
  737. <div class="table-cell members-count">${new Intl.NumberFormat('it-IT').format(current)}</div>
  738. <div class="table-cell variation">${variationText}</div>
  739. <div class="table-cell card-types">
  740. <div class="card-types-container">
  741. ${cardTypeBreakdown}
  742. </div>
  743. </div>
  744. </div>
  745. `;
  746. });
  747. tableHtml += '</div>';
  748. container.innerHTML = tableHtml;
  749. },
  750. createCourseChart: function () {
  751. console.log('Creating course chart...');
  752. const seasonFilter = '{{ $seasonFilter }}';
  753. const selectedCourse = '{{ $selectedCourse ?? '' }}';
  754. const seasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
  755. console.log('Selected course:', selectedCourse, 'for season:', seasonFilter);
  756. if (!selectedCourse || selectedCourse.trim() === '') {
  757. console.log('No course selected, skipping chart creation');
  758. return;
  759. }
  760. const chartId = `courses-chart-${seasonKey}-${selectedCourse}`;
  761. const canvas = document.getElementById(chartId);
  762. if (!canvas) return;
  763. this.destroyChart(chartId);
  764. const courseData = @json($this->getCourseMonthlyEarnings());
  765. const ctx = canvas.getContext('2d');
  766. this.charts[chartId] = new Chart(ctx, {
  767. type: 'bar',
  768. data: {
  769. labels: courseData.labels,
  770. datasets: courseData.datasets.map(dataset => {
  771. if (dataset.type === 'line') {
  772. return {
  773. ...dataset,
  774. type: 'line',
  775. fill: false,
  776. backgroundColor: 'transparent'
  777. };
  778. }
  779. return dataset;
  780. })
  781. },
  782. options: {
  783. responsive: true,
  784. maintainAspectRatio: false,
  785. interaction: {
  786. mode: 'index',
  787. intersect: false,
  788. },
  789. scales: {
  790. x: {
  791. grid: { display: false },
  792. ticks: { font: { weight: '500' } }
  793. },
  794. y: {
  795. beginAtZero: true,
  796. grid: {
  797. color: 'rgba(0, 0, 0, 1)',
  798. borderDash: [5, 5]
  799. },
  800. ticks: {
  801. callback: function (value) {
  802. return '€' + new Intl.NumberFormat('it-IT').format(value);
  803. }
  804. }
  805. }
  806. },
  807. plugins: {
  808. legend: {
  809. position: 'bottom',
  810. labels: {
  811. usePointStyle: true,
  812. padding: 20,
  813. pointStyle: "rect",
  814. font: { weight: '500' }
  815. }
  816. },
  817. tooltip: {
  818. backgroundColor: 'rgba(255, 255, 255, 1)',
  819. titleColor: '#212529',
  820. bodyColor: '#495057',
  821. borderColor: '#e9ecef',
  822. borderWidth: 2,
  823. cornerRadius: 0,
  824. callbacks: {
  825. label: function (context) {
  826. return context.dataset.label + ': €' +
  827. new Intl.NumberFormat('it-IT').format(context.parsed.y);
  828. }
  829. }
  830. }
  831. },
  832. animation: {
  833. duration: 1000,
  834. easing: 'easeOutQuart'
  835. }
  836. }
  837. });
  838. },
  839. createCourseChartWithValue: function (selectedCourseValue) {
  840. console.log('Creating modern course chart with value:', selectedCourseValue);
  841. const seasonFilter = '{{ $seasonFilter }}';
  842. const seasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
  843. const chartId = `courses-chart-${seasonKey}-${selectedCourseValue}`;
  844. const tableId = `course-delta-table-${seasonKey}-${selectedCourseValue}`;
  845. let canvas = document.getElementById(chartId);
  846. const tableContainer = document.getElementById(tableId);
  847. if (!canvas) {
  848. console.log('Canvas not found for chart ID:', chartId);
  849. const chartContainer = document.querySelector('.modern-chart-container');
  850. if (chartContainer) {
  851. chartContainer.innerHTML = `
  852. <div class="chart-empty-state">
  853. <div style="text-align: center; padding: 4rem 2rem;">
  854. <div style="font-size: 4rem; margin-bottom: 1.5rem; opacity: 0.3;">📊</div>
  855. <h3 style="font-size: 1.5rem; font-weight: 600; margin-bottom: 1rem; color: #374151;">
  856. Grafico non disponibile
  857. </h3>
  858. <p style="font-size: 1rem; opacity: 0.7; margin: 0; max-width: 400px; margin-left: auto; margin-right: auto; line-height: 1.5;">
  859. Il grafico per questo corso non può essere visualizzato nella stagione selezionata.
  860. </p>
  861. </div>
  862. </div>
  863. `;
  864. }
  865. if (tableContainer) {
  866. tableContainer.innerHTML = '';
  867. }
  868. return;
  869. }
  870. this.destroyChart(chartId);
  871. @this.call('getCourseData', selectedCourseValue).then(courseData => {
  872. console.log('Received course data:', courseData);
  873. if (courseData.isEmpty) {
  874. console.log('No data available for course, showing message');
  875. if (tableContainer) {
  876. tableContainer.innerHTML = '';
  877. }
  878. const chartContainer = canvas.parentElement;
  879. chartContainer.innerHTML = `
  880. <div class="chart-empty-state">
  881. <div style="text-align: center; padding: 4rem 2rem;">
  882. <div style="font-size: 4rem; margin-bottom: 1.5rem; opacity: 0.3;">📊</div>
  883. <h3 style="font-size: 1.5rem; font-weight: 600; margin-bottom: 1rem; color: #374151;">
  884. ${courseData.message}
  885. </h3>
  886. <p style="font-size: 1rem; opacity: 0.7; margin: 0; max-width: 400px; margin-left: auto; margin-right: auto; line-height: 1.5;">
  887. Questo corso non ha pagamenti registrati per la stagione selezionata.
  888. </p>
  889. </div>
  890. </div>
  891. `;
  892. return;
  893. }
  894. if (!courseData || !courseData.labels || courseData.labels.length === 0) {
  895. console.log('No data available for chart');
  896. return;
  897. }
  898. let canvasElement = document.getElementById(chartId);
  899. if (!canvasElement) {
  900. const chartContainer = canvas.parentElement;
  901. chartContainer.innerHTML = `<canvas id="${chartId}"></canvas>`;
  902. canvasElement = document.getElementById(chartId);
  903. }
  904. this.updateCourseTable(tableContainer, courseData.tableData);
  905. const participantData = courseData.datasets.find(d => d.participantData)?.participantData || [];
  906. const ctx = canvasElement.getContext('2d');
  907. const earnedGradient = ctx.createLinearGradient(0, 0, 0, 400);
  908. earnedGradient.addColorStop(0, 'rgba(16, 185, 129, 1)');
  909. earnedGradient.addColorStop(1, 'rgba(16, 185, 129, 1)');
  910. const totalData = courseData.datasets.find(d => d.label === 'Pagamenti Attesi')?.data || [];
  911. const earnedData = courseData.datasets.find(d => d.label === 'Pagamenti Effettuati')?.data || [];
  912. this.charts[chartId] = new Chart(ctx, {
  913. type: 'bar',
  914. data: {
  915. labels: courseData.labels,
  916. datasets: [
  917. {
  918. label: 'Pagamenti Effettuati',
  919. backgroundColor: earnedGradient,
  920. borderColor: 'rgba(16, 185, 129, 1)',
  921. borderWidth: 0,
  922. borderRadius: 8,
  923. borderSkipped: false,
  924. data: earnedData,
  925. type: 'bar',
  926. order: 2
  927. },
  928. {
  929. label: 'Pagamenti Attesi',
  930. backgroundColor: 'transparent',
  931. borderColor: 'rgba(59, 130, 246, 1)',
  932. borderWidth: 3,
  933. pointBackgroundColor: 'rgba(59, 130, 246, 1)',
  934. pointBorderColor: '#ffffff',
  935. pointBorderWidth: 3,
  936. pointRadius: 7,
  937. pointHoverRadius: 9,
  938. data: totalData,
  939. type: 'line',
  940. tension: 0.3,
  941. order: 1,
  942. participantData: participantData
  943. }
  944. ]
  945. },
  946. options: {
  947. responsive: true,
  948. maintainAspectRatio: false,
  949. interaction: {
  950. mode: 'index',
  951. intersect: false,
  952. },
  953. layout: {
  954. padding: {
  955. top: 20,
  956. right: 20,
  957. bottom: 20,
  958. left: 10
  959. }
  960. },
  961. scales: {
  962. x: {
  963. grid: {
  964. display: false
  965. },
  966. ticks: {
  967. font: {
  968. weight: '600',
  969. size: 13
  970. },
  971. color: '#6b7280'
  972. },
  973. border: {
  974. display: false
  975. }
  976. },
  977. y: {
  978. beginAtZero: true,
  979. grid: {
  980. color: 'rgba(156, 163, 175, 0.15)',
  981. borderDash: [3, 3]
  982. },
  983. border: {
  984. display: false
  985. },
  986. ticks: {
  987. font: {
  988. size: 12,
  989. weight: '500'
  990. },
  991. color: '#6b7280',
  992. callback: function (value) {
  993. return '€' + new Intl.NumberFormat('it-IT', {
  994. minimumFractionDigits: 0,
  995. maximumFractionDigits: 0
  996. }).format(value);
  997. }
  998. }
  999. }
  1000. },
  1001. plugins: {
  1002. legend: {
  1003. position: 'bottom',
  1004. labels: {
  1005. usePointStyle: true,
  1006. padding: 20,
  1007. pointStyle: "rect",
  1008. font: { weight: '500' }
  1009. }
  1010. },
  1011. tooltip: {
  1012. backgroundColor: 'rgba(255, 255, 255, 1)',
  1013. titleColor: '#111827',
  1014. bodyColor: '#374151',
  1015. borderColor: 'rgba(229, 231, 235, 0.8)',
  1016. borderWidth: 2,
  1017. cornerRadius: 0,
  1018. titleFont: {
  1019. weight: 'bold',
  1020. size: 15
  1021. },
  1022. bodyFont: {
  1023. size: 14,
  1024. weight: '500'
  1025. },
  1026. padding: 16,
  1027. boxPadding: 8,
  1028. usePointStyle: true,
  1029. displayColors: true,
  1030. callbacks: {
  1031. title: function (context) {
  1032. return context[0].label;
  1033. },
  1034. label: function (context) {
  1035. let label = context.dataset.label + ': €' +
  1036. new Intl.NumberFormat('it-IT').format(context.parsed.y);
  1037. if (context.dataset.label === 'Pagamenti Effettuati') {
  1038. const earnedValue = parseFloat(context.parsed.y) || 0;
  1039. const totalValue = parseFloat(totalData[context.dataIndex]) || 0;
  1040. const missingValue = Math.max(0, totalValue - earnedValue);
  1041. if (participantData[context.dataIndex]) {
  1042. label += '\n👥 Partecipanti: ' + participantData[context.dataIndex];
  1043. }
  1044. if (missingValue > 0) {
  1045. label += '\n🔴 Mancanti: €' + new Intl.NumberFormat('it-IT').format(missingValue);
  1046. }
  1047. }
  1048. if (context.dataset.label === 'Pagamenti Attesi' && participantData[context.dataIndex]) {
  1049. label += '\n👥 Partecipanti: ' + participantData[context.dataIndex];
  1050. }
  1051. return label;
  1052. }
  1053. }
  1054. }
  1055. },
  1056. animation: {
  1057. duration: 1500,
  1058. easing: 'easeOutCubic'
  1059. },
  1060. elements: {
  1061. bar: {
  1062. borderRadius: {
  1063. topLeft: 8,
  1064. topRight: 8,
  1065. bottomLeft: 0,
  1066. bottomRight: 0
  1067. }
  1068. },
  1069. line: {
  1070. borderCapStyle: 'round',
  1071. borderJoinStyle: 'round'
  1072. },
  1073. point: {
  1074. hoverBorderWidth: 4,
  1075. borderWidth: 3
  1076. }
  1077. }
  1078. }
  1079. });
  1080. }).catch(error => {
  1081. console.error('Error calling getCourseData:', error);
  1082. });
  1083. },
  1084. updateCourseTable: function (container, tableData) {
  1085. if (!container || !tableData) return;
  1086. let tableHtml = `
  1087. <div class="course-table">
  1088. <div class="table-header">
  1089. <div class="table-cell month">Mese</div>
  1090. <div class="table-cell participants">👥</div>
  1091. <div class="table-cell delta">Mancanti</div>
  1092. <div class="table-cell percentage">%</div>
  1093. </div>
  1094. `;
  1095. tableData.forEach(row => {
  1096. const earned = parseFloat(row.earned) || 0;
  1097. const total = parseFloat(row.total) || 0;
  1098. const delta = Math.max(0, total - earned);
  1099. let percentage = 0;
  1100. let percentageDisplay = '—';
  1101. let percentageClass = 'neutral';
  1102. if (total > 0) {
  1103. percentage = Math.round((earned / total) * 100);
  1104. percentageDisplay = percentage + '%';
  1105. // Color based on completion
  1106. if (percentage >= 100) {
  1107. percentageClass = 'good';
  1108. } else if (percentage >= 80) {
  1109. percentageClass = 'warning';
  1110. } else {
  1111. percentageClass = 'bad';
  1112. }
  1113. }
  1114. // Delta styling: positive when delta is 0 (fully paid), negative when there's missing amount
  1115. const deltaClass = (total > 0 && delta === 0) ? 'positive' :
  1116. (delta > 0) ? 'negative' : 'neutral';
  1117. tableHtml += `
  1118. <div class="table-row">
  1119. <div class="table-cell month">${row.month}</div>
  1120. <div class="table-cell participants">${row.participants}</div>
  1121. <div class="table-cell delta ${deltaClass}">€${new Intl.NumberFormat('it-IT').format(delta)}</div>
  1122. <div class="table-cell percentage ${percentageClass}">${percentageDisplay}</div>
  1123. </div>
  1124. `;
  1125. });
  1126. tableHtml += '</div>';
  1127. container.innerHTML = tableHtml;
  1128. },
  1129. updateCourseChart: function () {
  1130. if (this.selectedCourse) {
  1131. const seasonFilter = @json($seasonFilter);
  1132. const seasonKey = seasonFilter.replace('-', '');
  1133. this.createCourseChartWithValue(this.selectedCourse);
  1134. }
  1135. }
  1136. };
  1137. document.addEventListener('DOMContentLoaded', function () {
  1138. setTimeout(() => {
  1139. window.ReportsChartManager.updateMainCharts();
  1140. }, 100);
  1141. });
  1142. document.addEventListener('livewire:navigated', function () {
  1143. setTimeout(() => {
  1144. window.ReportsChartManager.updateMainCharts();
  1145. }, 100);
  1146. });
  1147. document.addEventListener('livewire:updated', function (event) {
  1148. console.log('Livewire updated, waiting for component to fully update...');
  1149. setTimeout(() => {
  1150. console.log('Now updating charts after delay');
  1151. window.ReportsChartManager.forceUpdateCharts();
  1152. }, 800);
  1153. });
  1154. document.addEventListener('livewire:load', function () {
  1155. Livewire.on('courseSelected', (courseId) => {
  1156. console.log('Course selected event received:', courseId);
  1157. setTimeout(() => {
  1158. window.ReportsChartManager.createCourseChartWithValue(courseId);
  1159. }, 200);
  1160. });
  1161. Livewire.on('chartsUpdated', () => {
  1162. console.log('Charts updated event received from Livewire');
  1163. setTimeout(() => {
  1164. window.ReportsChartManager.forceUpdateCharts();
  1165. }, 200);
  1166. });
  1167. });
  1168. </script>
  1169. </div>