reports.blade.php 62 KB

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