|
|
@@ -1,579 +1,913 @@
|
|
|
-<!-- filepath: /Users/fabiofratini/Desktop/Projects/iao_team/resources/views/livewire/reports.blade.php -->
|
|
|
-<div class="col card--ui" id="card--dashboard">
|
|
|
-
|
|
|
- <header id="title--section" style="display:none !important"
|
|
|
- class="d-flex align-items-center justify-content-between">
|
|
|
- <div class="title--section_name d-flex align-items-center justify-content-between">
|
|
|
- <i class="ico--ui title_section utenti me-2"></i>
|
|
|
- <h2 class="primary">Reports</h2>
|
|
|
- </div>
|
|
|
+{{-- resources/views/livewire/reports.blade.php --}}
|
|
|
+<div>
|
|
|
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
|
+ <style>
|
|
|
+ :root {
|
|
|
+ --primary-color: #3b5bdb;
|
|
|
+ --secondary-color: #495057;
|
|
|
+ --success-color: #00b894;
|
|
|
+ --info-color: #22b8cf;
|
|
|
+ --warning-color: #ffd43b;
|
|
|
+ --danger-color: #ff6b6b;
|
|
|
+ --dark-color: #212529;
|
|
|
+ --border-color: #e9ecef;
|
|
|
+ --shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
|
|
+ --shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
|
|
+ }
|
|
|
|
|
|
- </header>
|
|
|
+ .dashboard-container {
|
|
|
+ padding: 1rem;
|
|
|
+ max-width: 1400px;
|
|
|
+ margin: 0 auto;
|
|
|
+ }
|
|
|
|
|
|
+ .dashboard-header {
|
|
|
+ text-align: center;
|
|
|
+ margin-bottom: 2rem;
|
|
|
+ padding: 2rem;
|
|
|
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
+ border-radius: 16px;
|
|
|
+ color: white;
|
|
|
+ }
|
|
|
|
|
|
- <section id="subheader" class="d-flex align-items-center">
|
|
|
- </section>
|
|
|
+ .dashboard-header h1 {
|
|
|
+ font-size: 2.5rem;
|
|
|
+ font-weight: 700;
|
|
|
+ margin-bottom: 0.5rem;
|
|
|
+ }
|
|
|
|
|
|
- <section id="reports-section">
|
|
|
+ .dashboard-header p {
|
|
|
+ font-size: 1.1rem;
|
|
|
+ opacity: 0.9;
|
|
|
+ }
|
|
|
|
|
|
- <div class="row">
|
|
|
- <div class="col-md-12">
|
|
|
- <canvas id="monthly-in-out-chart"></canvas>
|
|
|
- </div>
|
|
|
+ .controls-section {
|
|
|
+ background: white;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 1.5rem;
|
|
|
+ box-shadow: var(--shadow-sm);
|
|
|
+ border: 1px solid var(--border-color);
|
|
|
+ margin-bottom: 2rem;
|
|
|
+ display: flex;
|
|
|
+ gap: 2rem;
|
|
|
+ align-items: center;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
|
|
|
- </div>
|
|
|
- <div class="col-md-12 chart-container">
|
|
|
- <canvas id="causals-chart" style="height: 300px; max-height: 300px;"></canvas>
|
|
|
- </div>
|
|
|
- <div class="row mt-5">
|
|
|
- <div class="col-md-12">
|
|
|
- <canvas id="tesserati-chart"></canvas>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="row mt-5">
|
|
|
- <div class="col-md-6">
|
|
|
- <select wire:model="selectedCourse" wire:change="updateCourseChart">
|
|
|
- <option value="">Seleziona un Corso</option>
|
|
|
- @foreach($courses as $course)
|
|
|
- <option value="{{ $course['id'] }}">{{ $course['full_name'] }}</option>
|
|
|
- @endforeach
|
|
|
- </select>
|
|
|
- </div>
|
|
|
- <div class="col-md-12 mt-3">
|
|
|
- <canvas id="courses-chart" style="height: 250px; max-height: 250px;"></canvas>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </section>
|
|
|
+ .control-group {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 0.5rem;
|
|
|
+ }
|
|
|
|
|
|
-</div>
|
|
|
+ .control-group label {
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 0.875rem;
|
|
|
+ color: var(--secondary-color);
|
|
|
+ }
|
|
|
|
|
|
-@push('scripts')
|
|
|
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
|
- <script>
|
|
|
- document.addEventListener('DOMContentLoaded', function () {
|
|
|
- initializeChartSizes();
|
|
|
-
|
|
|
- window.livewire.on('dataUpdated', () => {
|
|
|
- updateCharts();
|
|
|
- updateCausalsChart();
|
|
|
- });
|
|
|
-
|
|
|
- window.livewire.on('courseDataUpdated', async (courseId) => {
|
|
|
- console.log('Course data update event received for course ID:', courseId);
|
|
|
- await updateCoursesChart(courseId);
|
|
|
- updateCausalsChart();
|
|
|
- Object.keys(window.chartSizes).forEach(chartId => {
|
|
|
- restoreChartSize(chartId);
|
|
|
- });
|
|
|
- });
|
|
|
-
|
|
|
- updateCharts();
|
|
|
- updateCausalsChart();
|
|
|
- updateTesseratiChart();
|
|
|
-
|
|
|
-
|
|
|
- async function updateCharts() {
|
|
|
- try {
|
|
|
- const monthlyData = await @this.getMonthlyTotals();
|
|
|
-
|
|
|
- if (window.monthlyInOutChart) {
|
|
|
- window.monthlyInOutChart.destroy();
|
|
|
- }
|
|
|
+ .form-select {
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 2px solid var(--border-color);
|
|
|
+ padding: 0.75rem 1rem;
|
|
|
+ font-size: 1rem;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ min-width: 200px;
|
|
|
+ }
|
|
|
|
|
|
- const monthlyInOutCtx = document.getElementById('monthly-in-out-chart').getContext('2d');
|
|
|
-
|
|
|
- window.monthlyInOutChart = new Chart(monthlyInOutCtx, {
|
|
|
- type: 'bar',
|
|
|
- data: monthlyData,
|
|
|
- options: {
|
|
|
- responsive: true,
|
|
|
- scales: {
|
|
|
- x: {
|
|
|
- title: {
|
|
|
- display: false,
|
|
|
- text: 'Mese'
|
|
|
- },
|
|
|
- grid: {
|
|
|
- display: false
|
|
|
- }
|
|
|
- },
|
|
|
- y: {
|
|
|
- display: false,
|
|
|
- title: {
|
|
|
- display: false,
|
|
|
- text: 'Importo (€)'
|
|
|
- },
|
|
|
- beginAtZero: true,
|
|
|
- ticks: {
|
|
|
- display: false
|
|
|
- },
|
|
|
- grid: {
|
|
|
- display: false
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
- plugins: {
|
|
|
- legend: {
|
|
|
- display: true
|
|
|
- },
|
|
|
- title: {
|
|
|
- display: true,
|
|
|
- text: 'Entrate/Uscite Mensili',
|
|
|
- font: {
|
|
|
- size: 16
|
|
|
- }
|
|
|
- },
|
|
|
- tooltip: {
|
|
|
- callbacks: {
|
|
|
- label: function (context) {
|
|
|
- let label = context.dataset.label || '';
|
|
|
- if (label) {
|
|
|
- label += ': ';
|
|
|
- }
|
|
|
- if (context.parsed.y !== null) {
|
|
|
- label += new Intl.NumberFormat('it-IT', {
|
|
|
- style: 'currency',
|
|
|
- currency: 'EUR'
|
|
|
- }).format(context.parsed.y);
|
|
|
- }
|
|
|
- return label;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- });
|
|
|
+ .form-select:focus {
|
|
|
+ border-color: var(--primary-color);
|
|
|
+ box-shadow: 0 0 0 0.2rem rgba(59, 91, 219, 0.25);
|
|
|
+ outline: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ .summary-cards {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
|
+ gap: 1rem;
|
|
|
+ margin-bottom: 2rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .summary-card {
|
|
|
+ background: white;
|
|
|
+ border-radius: 16px;
|
|
|
+ padding: 1.5rem;
|
|
|
+ box-shadow: var(--shadow-sm);
|
|
|
+ border: 1px solid var(--border-color);
|
|
|
+ text-align: center;
|
|
|
+ transition: transform 0.3s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ .summary-card:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+ }
|
|
|
+
|
|
|
+ .summary-card h3 {
|
|
|
+ font-size: 0.875rem;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--secondary-color);
|
|
|
+ text-transform: uppercase;
|
|
|
+ letter-spacing: 0.5px;
|
|
|
+ margin-bottom: 0.5rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .summary-card .value {
|
|
|
+ font-size: 2rem;
|
|
|
+ font-weight: 700;
|
|
|
+ margin-bottom: 0.25rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .summary-card.income .value { color: var(--success-color); }
|
|
|
+ .summary-card.expense .value { color: var(--danger-color); }
|
|
|
+ .summary-card.delta .value { color: var(--primary-color); }
|
|
|
+ .summary-card.delta.negative .value { color: var(--danger-color); }
|
|
|
+
|
|
|
+ .chart-row {
|
|
|
+ width: 100%;
|
|
|
+ margin-bottom: 2rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-card {
|
|
|
+ background: white;
|
|
|
+ border-radius: 16px;
|
|
|
+ box-shadow: var(--shadow-sm);
|
|
|
+ border: 1px solid var(--border-color);
|
|
|
+ overflow: hidden;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-card:hover {
|
|
|
+ box-shadow: var(--shadow);
|
|
|
+ }
|
|
|
|
|
|
- const summaryData = await @this.getYearlySummary();
|
|
|
+ .chart-header {
|
|
|
+ padding: 1.5rem 2rem 1rem;
|
|
|
+ border-bottom: 1px solid var(--border-color);
|
|
|
+ background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-title {
|
|
|
+ font-size: 1.25rem;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--dark-color);
|
|
|
+ margin: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-body {
|
|
|
+ padding: 2rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-container {
|
|
|
+ position: relative;
|
|
|
+ height: 400px !important;
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .chart-container canvas {
|
|
|
+ max-height: 400px !important;
|
|
|
+ height: 400px !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .course-controls {
|
|
|
+ background: #f8f9fa;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 1.5rem;
|
|
|
+ margin-bottom: 1.5rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .legend-container {
|
|
|
+ display: flex;
|
|
|
+ gap: 2rem;
|
|
|
+ margin-top: 1rem;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .legend-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 0.5rem;
|
|
|
+ font-size: 0.875rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .legend-color {
|
|
|
+ width: 12px;
|
|
|
+ height: 12px;
|
|
|
+ border-radius: 50%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .monthly-table-container {
|
|
|
+ background: #f8f9fa;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 1.5rem;
|
|
|
+ box-shadow: var(--shadow-sm);
|
|
|
+ }
|
|
|
+
|
|
|
+ .members-table-container {
|
|
|
+ background: #f8f9fa;
|
|
|
+ border-radius: 12px;
|
|
|
+ padding: 1.5rem;
|
|
|
+ box-shadow: var(--shadow-sm);
|
|
|
+ }
|
|
|
+
|
|
|
+ .monthly-table, .members-table {
|
|
|
+ font-size: 0.875rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .members-table .table-header {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1.2fr 0.8fr 1fr;
|
|
|
+ gap: 0.5rem;
|
|
|
+ margin-bottom: 0.5rem;
|
|
|
+ padding-bottom: 0.5rem;
|
|
|
+ border-bottom: 2px solid var(--border-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ .members-table .table-row {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1.2fr 0.8fr 1fr;
|
|
|
+ gap: 0.5rem;
|
|
|
+ padding: 0.5rem 0;
|
|
|
+ border-bottom: 1px solid #e9ecef;
|
|
|
+ transition: background-color 0.2s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-cell.season-name {
|
|
|
+ text-align: left;
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 0.8rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-cell.members-count {
|
|
|
+ text-align: center;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--primary-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-cell.variation {
|
|
|
+ text-align: right;
|
|
|
+ font-size: 0.8rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .variation-positive {
|
|
|
+ color: var(--success-color);
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ .variation-negative {
|
|
|
+ color: var(--danger-color);
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ .variation-neutral {
|
|
|
+ color: var(--secondary-color);
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .monthly-table {
|
|
|
+ font-size: 0.875rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-header {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr 1fr 1fr;
|
|
|
+ gap: 0.5rem;
|
|
|
+ margin-bottom: 0.5rem;
|
|
|
+ padding-bottom: 0.5rem;
|
|
|
+ border-bottom: 2px solid var(--border-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-row {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr 1fr 1fr;
|
|
|
+ gap: 0.5rem;
|
|
|
+ padding: 0.5rem 0;
|
|
|
+ border-bottom: 1px solid #e9ecef;
|
|
|
+ transition: background-color 0.2s ease;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-row:hover {
|
|
|
+ background-color: rgba(0, 0, 0, 0.02);
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-row.positive {
|
|
|
+ background-color: rgba(0, 184, 148, 0.05);
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-row.negative {
|
|
|
+ background-color: rgba(255, 107, 107, 0.05);
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-row.neutral {
|
|
|
+ background-color: rgba(73, 80, 87, 0.02);
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-cell {
|
|
|
+ padding: 0.25rem 0.5rem;
|
|
|
+ text-align: right;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-header .table-cell {
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--secondary-color);
|
|
|
+ text-align: center;
|
|
|
+ text-transform: uppercase;
|
|
|
+ font-size: 0.75rem;
|
|
|
+ letter-spacing: 0.5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-cell.month-name {
|
|
|
+ text-align: left;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
|
|
|
- } catch (error) {
|
|
|
- console.error('Error updating charts:', error);
|
|
|
- document.getElementById('monthly-in-out-chart').insertAdjacentHTML(
|
|
|
- 'afterend',
|
|
|
- '<div class="alert alert-danger">Errore nel caricamento dei dati finanziari</div>'
|
|
|
- );
|
|
|
- }
|
|
|
+ .table-cell.income {
|
|
|
+ color: var(--success-color);
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-cell.expense {
|
|
|
+ color: var(--danger-color);
|
|
|
+ font-weight: 500;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-cell.net {
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-row.positive .table-cell.net {
|
|
|
+ color: var(--success-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-row.negative .table-cell.net {
|
|
|
+ color: var(--danger-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ .table-row.neutral .table-cell.net {
|
|
|
+ color: var(--secondary-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ @media (max-width: 1024px) {
|
|
|
+ .chart-body > div[style*="grid-template-columns"] {
|
|
|
+ grid-template-columns: 1fr !important;
|
|
|
+ gap: 1rem !important;
|
|
|
}
|
|
|
|
|
|
- async function updateCausalsChart() {
|
|
|
- try {
|
|
|
- const causalsData = await @this.getTopCausalsByAmount(10, 'IN');
|
|
|
+ .monthly-table-container {
|
|
|
+ order: 2;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- if (window.causalsChart) {
|
|
|
- window.causalsChart.destroy();
|
|
|
- }
|
|
|
+ .loading {
|
|
|
+ text-align: center;
|
|
|
+ padding: 2rem;
|
|
|
+ color: var(--secondary-color);
|
|
|
+ }
|
|
|
|
|
|
- const causalsCtx = document.getElementById('causals-chart').getContext('2d');
|
|
|
+ .error-message {
|
|
|
+ background: #fff5f5;
|
|
|
+ border: 1px solid #fed7d7;
|
|
|
+ color: #c53030;
|
|
|
+ padding: 1rem;
|
|
|
+ border-radius: 8px;
|
|
|
+ margin: 1rem 0;
|
|
|
+ }
|
|
|
|
|
|
- const existingTabs = document.querySelector('.causals-tabs');
|
|
|
- if (existingTabs) {
|
|
|
- existingTabs.remove();
|
|
|
- }
|
|
|
+ @media (max-width: 768px) {
|
|
|
+ .dashboard-container {
|
|
|
+ padding: 0.5rem;
|
|
|
+ }
|
|
|
|
|
|
- const existingTitle = document.querySelector('.causals-title');
|
|
|
- if (existingTitle) {
|
|
|
- existingTitle.remove();
|
|
|
- }
|
|
|
+ .dashboard-header h1 {
|
|
|
+ font-size: 2rem;
|
|
|
+ }
|
|
|
|
|
|
- const chartTitle = document.createElement('h4');
|
|
|
- chartTitle.className = 'text-center mt-2 mb-3 causals-title';
|
|
|
-
|
|
|
- const chartCanvas = document.getElementById('causals-chart');
|
|
|
- chartCanvas.parentNode.insertBefore(chartTitle, chartCanvas);
|
|
|
-
|
|
|
- const inData = causalsData.inData;
|
|
|
-
|
|
|
- const colors = [
|
|
|
- 'rgba(54, 162, 235, 0.8)', // Blue
|
|
|
- 'rgba(75, 192, 192, 0.8)', // Teal
|
|
|
- 'rgba(153, 102, 255, 0.8)', // Purple
|
|
|
- 'rgba(255, 159, 64, 0.8)', // Orange
|
|
|
- 'rgba(39, 174, 96, 0.8)', // Green
|
|
|
- 'rgba(41, 128, 185, 0.8)', // Dark blue
|
|
|
- 'rgba(142, 68, 173, 0.8)', // Dark purple
|
|
|
- 'rgba(230, 126, 34, 0.8)', // Dark orange
|
|
|
- 'rgba(46, 204, 113, 0.8)', // Light green
|
|
|
- 'rgba(52, 152, 219, 0.8)' // Light blue
|
|
|
- ];
|
|
|
-
|
|
|
- const commonOptions = {
|
|
|
- responsive: true,
|
|
|
- maintainAspectRatio: false,
|
|
|
- plugins: {
|
|
|
- title: {
|
|
|
- display: true,
|
|
|
- text: 'Causali performanti',
|
|
|
- font: {
|
|
|
- size: 16
|
|
|
- }
|
|
|
- },
|
|
|
- tooltip: {
|
|
|
- callbacks: {
|
|
|
- label: function (context) {
|
|
|
- const fullName = inData[context.dataIndex]?.fullName || context.label;
|
|
|
- const value = context.raw;
|
|
|
-
|
|
|
- return fullName + ': ' + new Intl.NumberFormat('it-IT', {
|
|
|
- style: 'currency',
|
|
|
- currency: 'EUR'
|
|
|
- }).format(value);
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
- legend: {
|
|
|
- display: true,
|
|
|
- position: 'right',
|
|
|
- labels: {
|
|
|
- boxWidth: 15,
|
|
|
- padding: 10,
|
|
|
- generateLabels: function (chart) {
|
|
|
- const data = chart.data;
|
|
|
- if (data.labels.length && data.datasets.length) {
|
|
|
- return data.labels.map(function (label, i) {
|
|
|
- const meta = chart.getDatasetMeta(0);
|
|
|
- const style = meta.controller.getStyle(i);
|
|
|
-
|
|
|
- let shortenedLabel = label;
|
|
|
- if (label.length > 20) {
|
|
|
- shortenedLabel = label.substring(0, 17) + '...';
|
|
|
- }
|
|
|
-
|
|
|
- return {
|
|
|
- text: shortenedLabel,
|
|
|
- fillStyle: style.backgroundColor,
|
|
|
- hidden: !chart.getDataVisibility(i),
|
|
|
- index: i,
|
|
|
- datasetIndex: 0
|
|
|
- };
|
|
|
- });
|
|
|
- }
|
|
|
- return [];
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- let chartData = {
|
|
|
- labels: inData.map(item => item.label),
|
|
|
- datasets: [{
|
|
|
- label: 'Importo',
|
|
|
- data: inData.map(item => item.value),
|
|
|
- backgroundColor: inData.map((item, index) => colors[index % colors.length]),
|
|
|
- borderWidth: 1,
|
|
|
- borderColor: '#fff'
|
|
|
- }]
|
|
|
- };
|
|
|
-
|
|
|
- window.causalsChart = new Chart(causalsCtx, {
|
|
|
- type: 'doughnut',
|
|
|
- data: chartData,
|
|
|
- options: commonOptions
|
|
|
- });
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('Error updating causals chart:', error);
|
|
|
- document.getElementById('causals-chart').insertAdjacentHTML(
|
|
|
- 'afterend',
|
|
|
- '<div class="alert alert-danger">Errore nel caricamento dei dati delle causali</div>'
|
|
|
- );
|
|
|
- }
|
|
|
+ .controls-section {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: stretch;
|
|
|
+ gap: 1rem;
|
|
|
}
|
|
|
- });
|
|
|
|
|
|
+ .chart-body {
|
|
|
+ padding: 1rem;
|
|
|
+ }
|
|
|
|
|
|
- async function updateCoursesChart() {
|
|
|
- try {
|
|
|
- const courseData = await @this.getCourseMonthlyEarnings();
|
|
|
- console.log('Course data received:', courseData);
|
|
|
+ .chart-container {
|
|
|
+ height: 300px !important;
|
|
|
+ }
|
|
|
|
|
|
- if (window.coursesChart) {
|
|
|
- window.coursesChart.destroy();
|
|
|
- }
|
|
|
+ .chart-container canvas {
|
|
|
+ max-height: 300px !important;
|
|
|
+ height: 300px !important;
|
|
|
+ }
|
|
|
|
|
|
- const coursesCtx = document.getElementById('courses-chart').getContext('2d');
|
|
|
+ .legend-container {
|
|
|
+ gap: 1rem;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ </style>
|
|
|
|
|
|
- const dashedLinesPlugin = {
|
|
|
- // Plugin definition unchanged
|
|
|
- id: 'dashedLines',
|
|
|
- beforeDatasetsDraw: (chart) => {
|
|
|
- const ctx = chart.ctx;
|
|
|
- const lineDataset = chart.data.datasets.find(d => d.type === 'line' && d.label === 'Pagamenti Attesi');
|
|
|
- const barDataset = chart.data.datasets.find(d => d.type === 'bar' && d.label === 'Pagamenti Effettuati');
|
|
|
+ <div class="dashboard-container">
|
|
|
|
|
|
- if (!lineDataset || !barDataset) return;
|
|
|
+ <div class="controls-section">
|
|
|
+ <div class="control-group">
|
|
|
+ <label for="season-filter">Stagione di Riferimento:</label>
|
|
|
+ <select class="form-select" wire:model="seasonFilter" wire:change="updateCharts">
|
|
|
+ @foreach($this->getAvailableSeasons() as $season)
|
|
|
+ <option value="{{ $season }}">{{ $season }}</option>
|
|
|
+ @endforeach
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- const lineMeta = chart.getDatasetMeta(chart.data.datasets.indexOf(lineDataset));
|
|
|
- const barMeta = chart.getDatasetMeta(chart.data.datasets.indexOf(barDataset));
|
|
|
+ @php
|
|
|
+ $summary = $this->getYearlySummary();
|
|
|
+ @endphp
|
|
|
+ <div class="summary-cards">
|
|
|
+ <div class="summary-card income">
|
|
|
+ <h3>Entrate Totali</h3>
|
|
|
+ <div class="value">€{{ number_format($summary['totalIncome'], 2, ',', '.') }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="summary-card expense">
|
|
|
+ <h3>Uscite Totali</h3>
|
|
|
+ <div class="value">€{{ number_format($summary['totalExpenses'], 2, ',', '.') }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="summary-card delta {{ $summary['delta'] < 0 ? 'negative' : '' }}">
|
|
|
+ <h3>Bilancio Netto</h3>
|
|
|
+ <div class="value">€{{ number_format($summary['delta'], 2, ',', '.') }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- if (!lineMeta.data.length || !barMeta.data.length) return;
|
|
|
+ <!-- Charts Section - Force complete re-render with wire:key -->
|
|
|
+ <div wire:key="season-{{ $seasonFilter }}-course-{{ $selectedCourse ?? 'none' }}">
|
|
|
+ <div class="chart-row">
|
|
|
+ <div class="chart-card">
|
|
|
+ <div class="chart-header">
|
|
|
+ <h3 class="chart-title">Entrate e Uscite Mensili - {{ $seasonFilter }}</h3>
|
|
|
+ </div>
|
|
|
+ <div class="chart-body">
|
|
|
+ <div style="display: grid; grid-template-columns: 1fr 300px; gap: 2rem; align-items: start;">
|
|
|
+ <div class="chart-container">
|
|
|
+ <canvas id="monthly-chart-{{ str_replace('-', '', $seasonFilter) }}"></canvas>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ @php
|
|
|
+ $monthlyTotals = $this->getMonthlyTotals();
|
|
|
+ $incomeData = $monthlyTotals['datasets'][0]['data'];
|
|
|
+ $expenseData = $monthlyTotals['datasets'][1]['data'];
|
|
|
+ $monthNames = $monthlyTotals['labels'];
|
|
|
+ @endphp
|
|
|
+
|
|
|
+ <div class="monthly-table-container">
|
|
|
+ <h4 style="margin-bottom: 1rem; font-size: 1rem; font-weight: 600; color: var(--dark-color);">Riepilogo Mensile</h4>
|
|
|
+ <div class="monthly-table">
|
|
|
+ <div class="table-header">
|
|
|
+ <div class="table-cell">Mese</div>
|
|
|
+ <div class="table-cell">Entrate</div>
|
|
|
+ <div class="table-cell">Uscite</div>
|
|
|
+ <div class="table-cell">Netto</div>
|
|
|
+ </div>
|
|
|
+ @foreach($monthNames as $index => $month)
|
|
|
+ @php
|
|
|
+ $income = floatval($incomeData[$index] ?? 0);
|
|
|
+ $expense = floatval($expenseData[$index] ?? 0);
|
|
|
+ $net = $income - $expense;
|
|
|
+ @endphp
|
|
|
+ <div class="table-row {{ $net < 0 ? 'negative' : ($net > 0 ? 'positive' : 'neutral') }}">
|
|
|
+ <div class="table-cell month-name">{{ $month }}</div>
|
|
|
+ <div class="table-cell income">€{{ number_format($income, 0, ',', '.') }}</div>
|
|
|
+ <div class="table-cell expense">€{{ number_format($expense, 0, ',', '.') }}</div>
|
|
|
+ <div class="table-cell net">€{{ number_format($net, 0, ',', '.') }}</div>
|
|
|
+ </div>
|
|
|
+ @endforeach
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- const missingData = lineDataset.missing || [];
|
|
|
+ <div class="chart-row">
|
|
|
+ <div class="chart-card">
|
|
|
+ <div class="chart-header">
|
|
|
+ <h3 class="chart-title">Causali Performanti - {{ $seasonFilter }}</h3>
|
|
|
+ </div>
|
|
|
+ <div class="chart-body">
|
|
|
+ <div class="chart-container">
|
|
|
+ <canvas id="causals-chart-{{ str_replace('-', '', $seasonFilter) }}"></canvas>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- ctx.save();
|
|
|
- ctx.lineWidth = 2;
|
|
|
- ctx.setLineDash([8, 4]);
|
|
|
- ctx.strokeStyle = 'rgba(48, 51, 107, 0.3)';
|
|
|
+ <div class="chart-row">
|
|
|
+ <div class="chart-card">
|
|
|
+ <div class="chart-header">
|
|
|
+ <h3 class="chart-title">Tesserati per Stagione</h3>
|
|
|
+ </div>
|
|
|
+ <div class="chart-body">
|
|
|
+ <div style="display: grid; grid-template-columns: 1fr 300px; gap: 2rem; align-items: start;">
|
|
|
+ <div class="chart-container">
|
|
|
+ <canvas id="members-chart-{{ str_replace('-', '', $seasonFilter) }}"></canvas>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ @php
|
|
|
+ $membersData = $this->getTesseratiData();
|
|
|
+ $seasonLabels = $membersData['labels'];
|
|
|
+ $memberCounts = $membersData['datasets'][0]['data'];
|
|
|
+ @endphp
|
|
|
+
|
|
|
+ <div class="members-table-container">
|
|
|
+ <h4 style="margin-bottom: 1rem; font-size: 1rem; font-weight: 600; color: var(--dark-color);">Riepilogo Tesserati</h4>
|
|
|
+ <div class="members-table">
|
|
|
+ <div class="table-header">
|
|
|
+ <div class="table-cell">Stagione</div>
|
|
|
+ <div class="table-cell">Tesserati</div>
|
|
|
+ <div class="table-cell">Variazione</div>
|
|
|
+ </div>
|
|
|
+ @foreach($seasonLabels as $index => $season)
|
|
|
+ @php
|
|
|
+ $current = intval($memberCounts[$index] ?? 0);
|
|
|
+ $previous = $index > 0 ? intval($memberCounts[$index - 1] ?? 0) : 0;
|
|
|
+ $variation = $index > 0 ? $current - $previous : 0;
|
|
|
+ $variationPercent = $previous > 0 ? round(($variation / $previous) * 100, 1) : 0;
|
|
|
+ @endphp
|
|
|
+ <div class="table-row {{ $variation > 0 ? 'positive' : ($variation < 0 ? 'negative' : 'neutral') }}">
|
|
|
+ <div class="table-cell season-name">{{ $season }}</div>
|
|
|
+ <div class="table-cell members-count">{{ number_format($current, 0, ',', '.') }}</div>
|
|
|
+ <div class="table-cell variation">
|
|
|
+ @if($index > 0)
|
|
|
+ @if($variation > 0)
|
|
|
+ <span class="variation-positive">+{{ $variation }} (+{{ $variationPercent }}%)</span>
|
|
|
+ @elseif($variation < 0)
|
|
|
+ <span class="variation-negative">{{ $variation }} ({{ $variationPercent }}%)</span>
|
|
|
+ @else
|
|
|
+ <span class="variation-neutral">{{ $variation }}</span>
|
|
|
+ @endif
|
|
|
+ @else
|
|
|
+ <span class="variation-neutral">—</span>
|
|
|
+ @endif
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ @endforeach
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- for (let i = 0; i < lineMeta.data.length; i++) {
|
|
|
- const linePoint = lineMeta.data[i];
|
|
|
- const barPoint = barMeta.data[i];
|
|
|
+ <div class="chart-row">
|
|
|
+ <div class="chart-card">
|
|
|
+ <div class="chart-header">
|
|
|
+ <h3 class="chart-title">Analisi Corsi</h3>
|
|
|
+ </div>
|
|
|
+ <div class="chart-body">
|
|
|
+ <div class="course-controls">
|
|
|
+ <div class="control-group">
|
|
|
+ <label>Seleziona Corso:</label>
|
|
|
+ <select class="form-select" wire:model="selectedCourse" wire:change="updateCourseChart">
|
|
|
+ <option value="">Seleziona un Corso</option>
|
|
|
+ @foreach($courses as $course)
|
|
|
+ <option value="{{ $course['id'] }}">{{ $course['full_name'] }}</option>
|
|
|
+ @endforeach
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div class="legend-container">
|
|
|
+ <div class="legend-item">
|
|
|
+ <div class="legend-color" style="background: rgba(0, 184, 148, 1);"></div>
|
|
|
+ <span>Pagamenti Effettuati</span>
|
|
|
+ </div>
|
|
|
+ <div class="legend-item">
|
|
|
+ <div class="legend-color" style="background: rgba(48, 51, 107, 1);"></div>
|
|
|
+ <span>Pagamenti Attesi</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="chart-container">
|
|
|
+ <canvas id="courses-chart-{{ str_replace('-', '', $seasonFilter) }}-{{ $selectedCourse ?? 'none' }}"></canvas>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- if (!linePoint || !barPoint) continue;
|
|
|
+ <script>
|
|
|
+ (function() {
|
|
|
+ const seasonFilter = '{{ $seasonFilter }}';
|
|
|
+ const selectedCourse = '{{ $selectedCourse ?? 'none' }}';
|
|
|
+ const seasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
|
|
|
|
|
|
- ctx.beginPath();
|
|
|
- ctx.moveTo(linePoint.x, linePoint.y);
|
|
|
- ctx.lineTo(linePoint.x, barPoint.y);
|
|
|
- ctx.stroke();
|
|
|
+ console.log('Creating charts for season:', seasonFilter);
|
|
|
|
|
|
- if (missingData[i] && missingData[i] > 0) {
|
|
|
- const midY = (linePoint.y + barPoint.y) / 2;
|
|
|
- ctx.textAlign = 'center';
|
|
|
- ctx.font = '10px Arial';
|
|
|
- ctx.fillStyle = 'rgba(48, 51, 107, 0.8)';
|
|
|
- }
|
|
|
- }
|
|
|
+ // Get the data fresh from PHP
|
|
|
+ const monthlyData = @json($this->getMonthlyTotals());
|
|
|
+ const causalsData = @json($this->getTopCausalsByAmount());
|
|
|
+ const membersData = @json($this->getTesseratiData());
|
|
|
+ const courseData = @json($this->getCourseMonthlyEarnings());
|
|
|
+
|
|
|
+ console.log('Monthly data for', seasonFilter, ':', monthlyData);
|
|
|
+ console.log('May value should be:', monthlyData.datasets[0].data[8]);
|
|
|
|
|
|
- ctx.restore();
|
|
|
+ // Wait for DOM to be ready
|
|
|
+ if (document.readyState === 'loading') {
|
|
|
+ document.addEventListener('DOMContentLoaded', initCharts);
|
|
|
+ } else {
|
|
|
+ setTimeout(initCharts, 100);
|
|
|
}
|
|
|
- };
|
|
|
-
|
|
|
- window.coursesChart = new Chart(coursesCtx, {
|
|
|
- type: 'bar',
|
|
|
- plugins: [dashedLinesPlugin],
|
|
|
- data: courseData,
|
|
|
- options: {
|
|
|
- responsive: true,
|
|
|
- maintainAspectRatio: false,
|
|
|
- layout: {
|
|
|
- padding: {
|
|
|
- top: 20
|
|
|
- }
|
|
|
- },
|
|
|
- // Rest of options unchanged
|
|
|
- scales: {
|
|
|
- x: {
|
|
|
- grid: {
|
|
|
- display: false
|
|
|
- }
|
|
|
+
|
|
|
+ function initCharts() {
|
|
|
+ createMonthlyChart();
|
|
|
+ createCausalsChart();
|
|
|
+ createMembersChart();
|
|
|
+ createCoursesChart();
|
|
|
+ }
|
|
|
+
|
|
|
+ function createMonthlyChart() {
|
|
|
+ const canvasId = `monthly-chart-${seasonKey}`;
|
|
|
+ const canvas = document.getElementById(canvasId);
|
|
|
+ if (!canvas) {
|
|
|
+ console.log('Canvas not found:', canvasId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const ctx = canvas.getContext('2d');
|
|
|
+
|
|
|
+ const incomeGradient = ctx.createLinearGradient(0, 0, 0, 400);
|
|
|
+ incomeGradient.addColorStop(0, 'rgba(0, 184, 148, 0.8)');
|
|
|
+ incomeGradient.addColorStop(1, 'rgba(0, 184, 148, 0.2)');
|
|
|
+
|
|
|
+ const expenseGradient = ctx.createLinearGradient(0, 0, 0, 400);
|
|
|
+ expenseGradient.addColorStop(0, 'rgba(255, 107, 107, 0.8)');
|
|
|
+ expenseGradient.addColorStop(1, 'rgba(255, 107, 107, 0.2)');
|
|
|
+
|
|
|
+ new Chart(ctx, {
|
|
|
+ type: 'bar',
|
|
|
+ data: {
|
|
|
+ labels: monthlyData.labels,
|
|
|
+ datasets: [
|
|
|
+ {
|
|
|
+ label: 'Entrate',
|
|
|
+ data: monthlyData.datasets[0].data,
|
|
|
+ backgroundColor: incomeGradient,
|
|
|
+ borderColor: '#00b894',
|
|
|
+ borderWidth: 2,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: 'Uscite',
|
|
|
+ data: monthlyData.datasets[1].data,
|
|
|
+ backgroundColor: expenseGradient,
|
|
|
+ borderColor: '#ff6b6b',
|
|
|
+ borderWidth: 2,
|
|
|
+ }
|
|
|
+ ]
|
|
|
},
|
|
|
- y: {
|
|
|
- display: true,
|
|
|
- beginAtZero: true,
|
|
|
- grid: {
|
|
|
- color: 'rgba(0, 0, 0, 0.1)',
|
|
|
- borderDash: [5, 5]
|
|
|
+ options: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ plugins: {
|
|
|
+ legend: {
|
|
|
+ position: 'top',
|
|
|
+ labels: {
|
|
|
+ usePointStyle: true,
|
|
|
+ padding: 20,
|
|
|
+ font: { weight: '500' }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
|
+ titleColor: '#212529',
|
|
|
+ bodyColor: '#495057',
|
|
|
+ borderColor: '#e9ecef',
|
|
|
+ borderWidth: 1,
|
|
|
+ cornerRadius: 8,
|
|
|
+ callbacks: {
|
|
|
+ label: function(context) {
|
|
|
+ return context.dataset.label + ': €' +
|
|
|
+ new Intl.NumberFormat('it-IT').format(context.parsed.y);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
},
|
|
|
- ticks: {
|
|
|
- callback: function (value) {
|
|
|
- return new Intl.NumberFormat('it-IT', {
|
|
|
- style: 'currency',
|
|
|
- currency: 'EUR',
|
|
|
- maximumFractionDigits: 0
|
|
|
- }).format(value);
|
|
|
+ scales: {
|
|
|
+ x: {
|
|
|
+ grid: { display: false },
|
|
|
+ ticks: { font: { weight: '500' } }
|
|
|
+ },
|
|
|
+ y: {
|
|
|
+ beginAtZero: true,
|
|
|
+ grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
|
|
+ ticks: {
|
|
|
+ callback: function(value) {
|
|
|
+ return '€' + new Intl.NumberFormat('it-IT').format(value);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
+ },
|
|
|
+ animation: {
|
|
|
+ duration: 1000,
|
|
|
+ easing: 'easeOutQuart'
|
|
|
}
|
|
|
}
|
|
|
- },
|
|
|
- plugins: {
|
|
|
- legend: {
|
|
|
- display: true,
|
|
|
- position: 'top',
|
|
|
- align: 'center',
|
|
|
- labels: {
|
|
|
- usePointStyle: true,
|
|
|
- padding: 20
|
|
|
- }
|
|
|
- },
|
|
|
- title: {
|
|
|
- display: true,
|
|
|
- text: 'Pagamenti per corso',
|
|
|
- font: {
|
|
|
- size: 16
|
|
|
- }
|
|
|
- },
|
|
|
- tooltip: {
|
|
|
- callbacks: {
|
|
|
- label: function (context) {
|
|
|
- if (context.dataset.label === 'Pagamenti Attesi') {
|
|
|
- let parts = [];
|
|
|
-
|
|
|
- const participants = context.dataset.participants ? context.dataset.participants[context.dataIndex] : 0;
|
|
|
- parts.push('N° iscritti: ' + participants);
|
|
|
-
|
|
|
- const expectedAmount = context.parsed.y;
|
|
|
- parts.push('Pagamenti attesi: ' + new Intl.NumberFormat('it-IT', {
|
|
|
- style: 'currency',
|
|
|
- currency: 'EUR'
|
|
|
- }).format(expectedAmount));
|
|
|
-
|
|
|
- const missingAmount = context.dataset.missing ? context.dataset.missing[context.dataIndex] : 0;
|
|
|
- parts.push('Ancora da pagare: ' + new Intl.NumberFormat('it-IT', {
|
|
|
- style: 'currency',
|
|
|
- currency: 'EUR'
|
|
|
- }).format(missingAmount));
|
|
|
-
|
|
|
- return parts.join(' | ');
|
|
|
- }
|
|
|
+ });
|
|
|
|
|
|
- let label = context.dataset.label || '';
|
|
|
- if (label) {
|
|
|
- label += ': ';
|
|
|
- }
|
|
|
+ console.log('Monthly chart created for', seasonFilter);
|
|
|
+ }
|
|
|
|
|
|
- if (context.parsed.y !== null) {
|
|
|
- label += new Intl.NumberFormat('it-IT', {
|
|
|
- style: 'currency',
|
|
|
- currency: 'EUR'
|
|
|
- }).format(context.parsed.y);
|
|
|
+ function createCausalsChart() {
|
|
|
+ const canvasId = `causals-chart-${seasonKey}`;
|
|
|
+ const canvas = document.getElementById(canvasId);
|
|
|
+ if (!canvas) return;
|
|
|
+
|
|
|
+ const ctx = canvas.getContext('2d');
|
|
|
+
|
|
|
+ const colors = [
|
|
|
+ 'rgba(59, 91, 219, 0.8)',
|
|
|
+ 'rgba(0, 184, 148, 0.8)',
|
|
|
+ 'rgba(34, 184, 207, 0.8)',
|
|
|
+ 'rgba(255, 212, 59, 0.8)',
|
|
|
+ 'rgba(255, 107, 107, 0.8)',
|
|
|
+ 'rgba(142, 68, 173, 0.8)',
|
|
|
+ 'rgba(230, 126, 34, 0.8)',
|
|
|
+ 'rgba(149, 165, 166, 0.8)',
|
|
|
+ 'rgba(241, 196, 15, 0.8)',
|
|
|
+ 'rgba(231, 76, 60, 0.8)'
|
|
|
+ ];
|
|
|
+
|
|
|
+ new Chart(ctx, {
|
|
|
+ type: 'doughnut',
|
|
|
+ data: {
|
|
|
+ labels: causalsData.inLabels,
|
|
|
+ datasets: [{
|
|
|
+ label: 'Importo',
|
|
|
+ data: causalsData.inData.map(item => item.value),
|
|
|
+ backgroundColor: colors,
|
|
|
+ borderColor: colors.map(color => color.replace('0.8', '1')),
|
|
|
+ borderWidth: 2,
|
|
|
+ hoverOffset: 8
|
|
|
+ }]
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ cutout: '60%',
|
|
|
+ plugins: {
|
|
|
+ legend: {
|
|
|
+ position: 'left',
|
|
|
+ labels: {
|
|
|
+ usePointStyle: true,
|
|
|
+ padding: 15,
|
|
|
+ font: { size: 11, weight: '500' }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ tooltip: {
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
|
+ titleColor: '#212529',
|
|
|
+ bodyColor: '#495057',
|
|
|
+ borderColor: '#e9ecef',
|
|
|
+ borderWidth: 1,
|
|
|
+ cornerRadius: 8,
|
|
|
+ callbacks: {
|
|
|
+ label: function(context) {
|
|
|
+ const value = context.raw;
|
|
|
+ const total = context.dataset.data.reduce((a, b) => a + b, 0);
|
|
|
+ const percentage = Math.round((value / total) * 100);
|
|
|
+ return context.label + ': €' +
|
|
|
+ new Intl.NumberFormat('it-IT').format(value) +
|
|
|
+ ` (${percentage}%)`;
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
- return label;
|
|
|
}
|
|
|
+ },
|
|
|
+ animation: {
|
|
|
+ animateRotate: true,
|
|
|
+ duration: 1000
|
|
|
}
|
|
|
}
|
|
|
- },
|
|
|
- elements: {
|
|
|
- point: {
|
|
|
- radius: 4,
|
|
|
- hoverRadius: 6
|
|
|
- },
|
|
|
- line: {
|
|
|
- tension: 0.4
|
|
|
- }
|
|
|
- }
|
|
|
+ });
|
|
|
}
|
|
|
- });
|
|
|
-
|
|
|
- // Maintain chart dimensions
|
|
|
- coursesCtx.canvas.style.height = '250px';
|
|
|
- coursesCtx.canvas.style.maxHeight = '250px';
|
|
|
-
|
|
|
- } catch (error) {
|
|
|
- console.error('Error updating courses chart:', error);
|
|
|
- document.getElementById('courses-chart').insertAdjacentHTML(
|
|
|
- 'afterend',
|
|
|
- '<div class="alert alert-danger">Errore nel caricamento dei dati del corso</div>'
|
|
|
- );
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- async function updateTesseratiChart() {
|
|
|
- try {
|
|
|
- const tesseratiData = await @this.getTesseratiData();
|
|
|
-
|
|
|
- if (window.tesseratiChart) {
|
|
|
- window.tesseratiChart.destroy();
|
|
|
- }
|
|
|
|
|
|
- const tesseratiCtx = document.getElementById('tesserati-chart').getContext('2d');
|
|
|
-
|
|
|
- window.tesseratiChart = new Chart(tesseratiCtx, {
|
|
|
- type: 'line',
|
|
|
- data: tesseratiData,
|
|
|
- options: {
|
|
|
- responsive: true,
|
|
|
- scales: {
|
|
|
- x: {
|
|
|
- title: {
|
|
|
- display: true,
|
|
|
- text: 'Anno Tesseramento'
|
|
|
- },
|
|
|
- grid: {
|
|
|
- display: false
|
|
|
- }
|
|
|
+ function createMembersChart() {
|
|
|
+ const canvasId = `members-chart-${seasonKey}`;
|
|
|
+ const canvas = document.getElementById(canvasId);
|
|
|
+ if (!canvas) return;
|
|
|
+
|
|
|
+ const ctx = canvas.getContext('2d');
|
|
|
+
|
|
|
+ const gradient = ctx.createLinearGradient(0, 0, 0, 400);
|
|
|
+ gradient.addColorStop(0, 'rgba(59, 91, 219, 0.3)');
|
|
|
+ gradient.addColorStop(1, 'rgba(59, 91, 219, 0.05)');
|
|
|
+
|
|
|
+ new Chart(ctx, {
|
|
|
+ type: 'line',
|
|
|
+ data: {
|
|
|
+ labels: membersData.labels,
|
|
|
+ datasets: [{
|
|
|
+ label: 'Membri Tesserati',
|
|
|
+ data: membersData.datasets[0].data,
|
|
|
+ borderColor: '#3b5bdb',
|
|
|
+ backgroundColor: gradient,
|
|
|
+ borderWidth: 3,
|
|
|
+ fill: true,
|
|
|
+ tension: 0.4,
|
|
|
+ pointBackgroundColor: '#3b5bdb',
|
|
|
+ pointBorderColor: '#ffffff',
|
|
|
+ pointBorderWidth: 2,
|
|
|
+ pointRadius: 6,
|
|
|
+ pointHoverRadius: 8
|
|
|
+ }]
|
|
|
},
|
|
|
- y: {
|
|
|
- title: {
|
|
|
- display: true,
|
|
|
- text: 'Numero di Tesserati'
|
|
|
+ options: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ plugins: {
|
|
|
+ legend: { display: false },
|
|
|
+ tooltip: {
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
|
+ titleColor: '#212529',
|
|
|
+ bodyColor: '#495057',
|
|
|
+ borderColor: '#e9ecef',
|
|
|
+ borderWidth: 1,
|
|
|
+ cornerRadius: 8,
|
|
|
+ callbacks: {
|
|
|
+ label: function(context) {
|
|
|
+ return 'Tesserati: ' + context.parsed.y;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ scales: {
|
|
|
+ x: { grid: { display: false } },
|
|
|
+ y: {
|
|
|
+ beginAtZero: true,
|
|
|
+ grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
|
|
+ ticks: { precision: 0 }
|
|
|
+ }
|
|
|
},
|
|
|
- beginAtZero: true,
|
|
|
- ticks: {
|
|
|
- precision: 0
|
|
|
+ animation: {
|
|
|
+ duration: 1000,
|
|
|
+ easing: 'easeOutQuart'
|
|
|
}
|
|
|
}
|
|
|
- },
|
|
|
- plugins: {
|
|
|
- legend: {
|
|
|
- display: true,
|
|
|
- position: 'top'
|
|
|
- },
|
|
|
- title: {
|
|
|
- display: true,
|
|
|
- text: 'Andamento Tesserati per Anno',
|
|
|
- font: {
|
|
|
- size: 16
|
|
|
- }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function createCoursesChart() {
|
|
|
+ const canvasId = `courses-chart-${seasonKey}-${selectedCourse}`;
|
|
|
+ const canvas = document.getElementById(canvasId);
|
|
|
+ if (!canvas) return;
|
|
|
+
|
|
|
+ const ctx = canvas.getContext('2d');
|
|
|
+
|
|
|
+ new Chart(ctx, {
|
|
|
+ type: 'bar',
|
|
|
+ data: {
|
|
|
+ labels: courseData.labels,
|
|
|
+ datasets: courseData.datasets
|
|
|
},
|
|
|
- tooltip: {
|
|
|
- callbacks: {
|
|
|
- label: function (context) {
|
|
|
- let label = context.dataset.label || '';
|
|
|
- if (label) {
|
|
|
- label += ': ';
|
|
|
+ options: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ scales: {
|
|
|
+ x: { grid: { display: false } },
|
|
|
+ y: {
|
|
|
+ beginAtZero: true,
|
|
|
+ grid: {
|
|
|
+ color: 'rgba(0, 0, 0, 0.1)',
|
|
|
+ borderDash: [5, 5]
|
|
|
+ },
|
|
|
+ ticks: {
|
|
|
+ callback: function(value) {
|
|
|
+ return '€' + new Intl.NumberFormat('it-IT').format(value);
|
|
|
+ }
|
|
|
}
|
|
|
- if (context.parsed.y !== null) {
|
|
|
- label += context.parsed.y + ' tesserati';
|
|
|
+ }
|
|
|
+ },
|
|
|
+ plugins: {
|
|
|
+ legend: { display: false },
|
|
|
+ tooltip: {
|
|
|
+ backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
|
|
+ titleColor: '#212529',
|
|
|
+ bodyColor: '#495057',
|
|
|
+ borderColor: '#e9ecef',
|
|
|
+ borderWidth: 1,
|
|
|
+ cornerRadius: 8,
|
|
|
+ callbacks: {
|
|
|
+ label: function(context) {
|
|
|
+ return context.dataset.label + ': €' +
|
|
|
+ new Intl.NumberFormat('it-IT').format(context.parsed.y);
|
|
|
+ }
|
|
|
}
|
|
|
- return label;
|
|
|
}
|
|
|
+ },
|
|
|
+ animation: {
|
|
|
+ duration: 1000,
|
|
|
+ easing: 'easeOutQuart'
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
+ });
|
|
|
}
|
|
|
- });
|
|
|
- } catch (error) {
|
|
|
- console.error('Error updating tesserati chart:', error);
|
|
|
- document.getElementById('tesserati-chart').insertAdjacentHTML(
|
|
|
- 'afterend',
|
|
|
- '<div class="alert alert-danger">Errore nel caricamento dei dati tesserati</div>'
|
|
|
- );
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- document.addEventListener('DOMContentLoaded', function () {
|
|
|
- const selectElement = document.querySelector('select[name="selectedCourse"]');
|
|
|
- if (selectElement) {
|
|
|
- selectElement.addEventListener('change', function () {
|
|
|
- const selectedValue = this.value;
|
|
|
- console.log('Selected course ID:', selectedValue);
|
|
|
- @this.set('selectedCourse', selectedValue);
|
|
|
- });
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- function initializeChartSizes() {
|
|
|
- window.chartSizes = {
|
|
|
- 'monthly-in-out-chart': {
|
|
|
- height: document.getElementById('monthly-in-out-chart').style.height || 'auto',
|
|
|
- maxHeight: document.getElementById('monthly-in-out-chart').style.maxHeight || 'none'
|
|
|
- },
|
|
|
- 'causals-chart': {
|
|
|
- height: '300px',
|
|
|
- maxHeight: '300px'
|
|
|
- },
|
|
|
- 'tesserati-chart': {
|
|
|
- height: document.getElementById('tesserati-chart').style.height || 'auto',
|
|
|
- maxHeight: document.getElementById('tesserati-chart').style.maxHeight || 'none'
|
|
|
- },
|
|
|
- 'courses-chart': {
|
|
|
- height: '250px',
|
|
|
- maxHeight: '250px'
|
|
|
- }
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- function restoreChartSize(chartId) {
|
|
|
- if (!window.chartSizes || !window.chartSizes[chartId]) return;
|
|
|
-
|
|
|
- const canvas = document.getElementById(chartId);
|
|
|
- if (canvas) {
|
|
|
- canvas.style.height = window.chartSizes[chartId].height;
|
|
|
- canvas.style.maxHeight = window.chartSizes[chartId].maxHeight;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
+ })();
|
|
|
+ </script>
|
|
|
+ </div>
|
|
|
|
|
|
- </script>
|
|
|
-@endpush
|
|
|
+ </div>
|
|
|
+</div>
|