|
|
@@ -1,413 +1,9 @@
|
|
|
{{-- 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);
|
|
|
- }
|
|
|
-
|
|
|
- .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;
|
|
|
- }
|
|
|
-
|
|
|
- .dashboard-header h1 {
|
|
|
- font-size: 2.5rem;
|
|
|
- font-weight: 700;
|
|
|
- margin-bottom: 0.5rem;
|
|
|
- }
|
|
|
-
|
|
|
- .dashboard-header p {
|
|
|
- font-size: 1.1rem;
|
|
|
- opacity: 0.9;
|
|
|
- }
|
|
|
-
|
|
|
- .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;
|
|
|
- }
|
|
|
-
|
|
|
- .control-group {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 0.5rem;
|
|
|
- }
|
|
|
-
|
|
|
- .control-group label {
|
|
|
- font-weight: 600;
|
|
|
- font-size: 0.875rem;
|
|
|
- color: var(--secondary-color);
|
|
|
- }
|
|
|
-
|
|
|
- .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;
|
|
|
- }
|
|
|
-
|
|
|
- .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);
|
|
|
- }
|
|
|
-
|
|
|
- .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;
|
|
|
- }
|
|
|
-
|
|
|
- .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;
|
|
|
- }
|
|
|
-
|
|
|
- .monthly-table-container {
|
|
|
- order: 2;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- .loading {
|
|
|
- text-align: center;
|
|
|
- padding: 2rem;
|
|
|
- color: var(--secondary-color);
|
|
|
- }
|
|
|
-
|
|
|
- .error-message {
|
|
|
- background: #fff5f5;
|
|
|
- border: 1px solid #fed7d7;
|
|
|
- color: #c53030;
|
|
|
- padding: 1rem;
|
|
|
- border-radius: 8px;
|
|
|
- margin: 1rem 0;
|
|
|
- }
|
|
|
-
|
|
|
- @media (max-width: 768px) {
|
|
|
- .dashboard-container {
|
|
|
- padding: 0.5rem;
|
|
|
- }
|
|
|
-
|
|
|
- .dashboard-header h1 {
|
|
|
- font-size: 2rem;
|
|
|
- }
|
|
|
-
|
|
|
- .controls-section {
|
|
|
- flex-direction: column;
|
|
|
- align-items: stretch;
|
|
|
- gap: 1rem;
|
|
|
- }
|
|
|
-
|
|
|
- .chart-body {
|
|
|
- padding: 1rem;
|
|
|
- }
|
|
|
-
|
|
|
- .chart-container {
|
|
|
- height: 300px !important;
|
|
|
- }
|
|
|
-
|
|
|
- .chart-container canvas {
|
|
|
- max-height: 300px !important;
|
|
|
- height: 300px !important;
|
|
|
- }
|
|
|
-
|
|
|
- .legend-container {
|
|
|
- gap: 1rem;
|
|
|
- }
|
|
|
- }
|
|
|
- </style>
|
|
|
+ <link rel="stylesheet" href="{{ asset('css/chart-reports.css') }}">
|
|
|
|
|
|
<div class="dashboard-container">
|
|
|
-
|
|
|
<div class="controls-section">
|
|
|
<div class="control-group">
|
|
|
<label for="season-filter">Stagione di Riferimento:</label>
|
|
|
@@ -437,49 +33,20 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- Charts Section - Force complete re-render with wire:key -->
|
|
|
- <div wire:key="season-{{ $seasonFilter }}-course-{{ $selectedCourse ?? 'none' }}">
|
|
|
+ <!-- Main Charts Section - Protected with wire:ignore -->
|
|
|
+ <div wire:ignore>
|
|
|
<div class="chart-row">
|
|
|
<div class="chart-card">
|
|
|
<div class="chart-header">
|
|
|
- <h3 class="chart-title">Entrate e Uscite Mensili - {{ $seasonFilter }}</h3>
|
|
|
+ <h3 class="chart-title">Entrate e Uscite Mensili - <span
|
|
|
+ id="monthly-season-title">{{ $seasonFilter }}</span></h3>
|
|
|
</div>
|
|
|
<div class="chart-body">
|
|
|
- <div style="display: grid; grid-template-columns: 1fr 300px; gap: 2rem; align-items: start;">
|
|
|
+ <div style="display: grid; grid-template-columns: 1fr 300px; 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 class="monthly-table-container" id="monthly-table">
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -489,7 +56,8 @@
|
|
|
<div class="chart-row">
|
|
|
<div class="chart-card">
|
|
|
<div class="chart-header">
|
|
|
- <h3 class="chart-title">Causali Performanti - {{ $seasonFilter }}</h3>
|
|
|
+ <h3 class="chart-title">Causali Performanti - <span
|
|
|
+ id="causals-season-title">{{ $seasonFilter }}</span></h3>
|
|
|
</div>
|
|
|
<div class="chart-body">
|
|
|
<div class="chart-container">
|
|
|
@@ -505,409 +73,590 @@
|
|
|
<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 style="display: grid; grid-template-columns: 1fr 300px; gap: 1rem; 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 class="members-table-container" id="members-table">
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+ </div>
|
|
|
|
|
|
- <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 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 ({{ $seasonFilter }}):</label>
|
|
|
+ <select class="form-select" wire:model.live="selectedCourse">
|
|
|
+ <option value="">Seleziona un Corso</option>
|
|
|
+ @foreach($this->getCoursesForSelect() 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-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 class="legend-item">
|
|
|
+ <div class="legend-color" style="background: rgba(48, 51, 107, 1);"></div>
|
|
|
+ <span>Pagamenti Attesi</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="chart-container">
|
|
|
- <canvas id="courses-chart-{{ str_replace('-', '', $seasonFilter) }}-{{ $selectedCourse ?? 'none' }}"></canvas>
|
|
|
- </div>
|
|
|
</div>
|
|
|
+
|
|
|
+ @if($selectedCourse)
|
|
|
+ <div wire:ignore wire:key="course-chart-{{ $seasonFilter }}-{{ $selectedCourse }}">
|
|
|
+ <div class="chart-container">
|
|
|
+ <canvas
|
|
|
+ id="courses-chart-{{ str_replace('-', '', $seasonFilter) }}-{{ $selectedCourse }}"></canvas>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ @else
|
|
|
+ <div class="chart-container"
|
|
|
+ style="display: flex; align-items: center; justify-content: center; min-height: 400px; color: var(--secondary-color);">
|
|
|
+ <p style="font-size: 1.1rem;">Seleziona un corso per visualizzare il grafico</p>
|
|
|
+ </div>
|
|
|
+ @endif
|
|
|
</div>
|
|
|
</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- <script>
|
|
|
- (function() {
|
|
|
- const seasonFilter = '{{ $seasonFilter }}';
|
|
|
- const selectedCourse = '{{ $selectedCourse ?? 'none' }}';
|
|
|
- const seasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
|
|
|
-
|
|
|
- console.log('Creating charts for season:', seasonFilter);
|
|
|
-
|
|
|
- // 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]);
|
|
|
-
|
|
|
- // Wait for DOM to be ready
|
|
|
- if (document.readyState === 'loading') {
|
|
|
- document.addEventListener('DOMContentLoaded', initCharts);
|
|
|
- } else {
|
|
|
- setTimeout(initCharts, 100);
|
|
|
- }
|
|
|
-
|
|
|
- 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,
|
|
|
- }
|
|
|
- ]
|
|
|
+ <!-- Single JavaScript section -->
|
|
|
+ <script>
|
|
|
+ // Global chart manager
|
|
|
+ window.ReportsChartManager = window.ReportsChartManager || {
|
|
|
+ charts: {},
|
|
|
+ currentSeason: null,
|
|
|
+
|
|
|
+ destroyChart: function (chartId) {
|
|
|
+ if (this.charts[chartId]) {
|
|
|
+ this.charts[chartId].destroy();
|
|
|
+ delete this.charts[chartId];
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ destroyAllCharts: function () {
|
|
|
+ Object.keys(this.charts).forEach(chartId => {
|
|
|
+ this.destroyChart(chartId);
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ updateMainCharts: function () {
|
|
|
+ const seasonFilter = '{{ $seasonFilter }}';
|
|
|
+ const seasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
|
|
|
+
|
|
|
+ // Only update if season changed
|
|
|
+ if (this.currentSeason === seasonFilter) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.currentSeason = seasonFilter;
|
|
|
+
|
|
|
+ // Get fresh data
|
|
|
+ const monthlyData = @json($this->getMonthlyTotals());
|
|
|
+ const causalsData = @json($this->getTopCausalsByAmount());
|
|
|
+ const membersData = @json($this->getTesseratiData());
|
|
|
+
|
|
|
+ // Update titles
|
|
|
+ document.getElementById('monthly-season-title').textContent = seasonFilter;
|
|
|
+ document.getElementById('causals-season-title').textContent = seasonFilter;
|
|
|
+
|
|
|
+ // Create/update charts
|
|
|
+ this.createMonthlyChart(seasonKey, monthlyData);
|
|
|
+ this.createCausalsChart(seasonKey, causalsData);
|
|
|
+ this.createMembersChart(seasonKey, membersData);
|
|
|
+
|
|
|
+ // Update tables
|
|
|
+ this.updateMonthlyTable(monthlyData);
|
|
|
+ this.updateMembersTable(membersData);
|
|
|
+ },
|
|
|
+
|
|
|
+ createMonthlyChart: function (seasonKey, monthlyData) {
|
|
|
+ const chartId = `monthly-chart-${seasonKey}`;
|
|
|
+ const canvas = document.getElementById(chartId);
|
|
|
+ if (!canvas) return;
|
|
|
+
|
|
|
+ this.destroyChart(chartId);
|
|
|
+
|
|
|
+ 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)');
|
|
|
+
|
|
|
+ this.charts[chartId] = new Chart(ctx, {
|
|
|
+ type: 'bar',
|
|
|
+ data: {
|
|
|
+ labels: monthlyData.labels,
|
|
|
+ datasets: [
|
|
|
+ {
|
|
|
+ label: 'Entrate',
|
|
|
+ data: monthlyData.datasets[0].data,
|
|
|
+ backgroundColor: incomeGradient,
|
|
|
+ borderColor: '#00b894',
|
|
|
+ borderWidth: 2,
|
|
|
},
|
|
|
- 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);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
- 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);
|
|
|
- }
|
|
|
- }
|
|
|
+ {
|
|
|
+ label: 'Uscite',
|
|
|
+ data: monthlyData.datasets[1].data,
|
|
|
+ backgroundColor: expenseGradient,
|
|
|
+ borderColor: '#ff6b6b',
|
|
|
+ borderWidth: 2,
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ },
|
|
|
+ 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);
|
|
|
}
|
|
|
- },
|
|
|
- animation: {
|
|
|
- duration: 1000,
|
|
|
- easing: 'easeOutQuart'
|
|
|
}
|
|
|
}
|
|
|
- });
|
|
|
-
|
|
|
- console.log('Monthly chart created for', seasonFilter);
|
|
|
- }
|
|
|
-
|
|
|
- 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
|
|
|
- }]
|
|
|
+ },
|
|
|
+ scales: {
|
|
|
+ x: {
|
|
|
+ grid: { display: false },
|
|
|
+ ticks: { font: { weight: '500' } }
|
|
|
},
|
|
|
- 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}%)`;
|
|
|
- }
|
|
|
- }
|
|
|
+ y: {
|
|
|
+ beginAtZero: true,
|
|
|
+ grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
|
|
+ ticks: {
|
|
|
+ callback: function (value) {
|
|
|
+ return '€' + new Intl.NumberFormat('it-IT').format(value);
|
|
|
}
|
|
|
- },
|
|
|
- animation: {
|
|
|
- animateRotate: true,
|
|
|
- duration: 1000
|
|
|
}
|
|
|
}
|
|
|
- });
|
|
|
+ },
|
|
|
+ animation: {
|
|
|
+ duration: 1000,
|
|
|
+ easing: 'easeOutQuart'
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
- 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
|
|
|
- }]
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ createCausalsChart: function (seasonKey, causalsData) {
|
|
|
+ const chartId = `causals-chart-${seasonKey}`;
|
|
|
+ const canvas = document.getElementById(chartId);
|
|
|
+ if (!canvas) return;
|
|
|
+
|
|
|
+ this.destroyChart(chartId);
|
|
|
+
|
|
|
+ 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)'
|
|
|
+ ];
|
|
|
+
|
|
|
+ this.charts[chartId] = 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' }
|
|
|
+ }
|
|
|
},
|
|
|
- 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;
|
|
|
- }
|
|
|
- }
|
|
|
+ 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}%)`;
|
|
|
}
|
|
|
- },
|
|
|
- scales: {
|
|
|
- x: { grid: { display: false } },
|
|
|
- y: {
|
|
|
- beginAtZero: true,
|
|
|
- grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
|
|
- ticks: { precision: 0 }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ animation: {
|
|
|
+ animateRotate: true,
|
|
|
+ duration: 1000
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ createMembersChart: function (seasonKey, membersData) {
|
|
|
+ const chartId = `members-chart-${seasonKey}`;
|
|
|
+ const canvas = document.getElementById(chartId);
|
|
|
+ if (!canvas) return;
|
|
|
+
|
|
|
+ this.destroyChart(chartId);
|
|
|
+
|
|
|
+ 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)');
|
|
|
+
|
|
|
+ this.charts[chartId] = 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
|
|
|
+ }]
|
|
|
+ },
|
|
|
+ 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;
|
|
|
}
|
|
|
- },
|
|
|
- animation: {
|
|
|
- duration: 1000,
|
|
|
- easing: 'easeOutQuart'
|
|
|
}
|
|
|
}
|
|
|
- });
|
|
|
+ },
|
|
|
+ scales: {
|
|
|
+ x: { grid: { display: false } },
|
|
|
+ y: {
|
|
|
+ beginAtZero: true,
|
|
|
+ grid: { color: 'rgba(0, 0, 0, 0.05)' },
|
|
|
+ ticks: { precision: 0 }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ animation: {
|
|
|
+ duration: 1000,
|
|
|
+ easing: 'easeOutQuart'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ updateMonthlyTable: function (monthlyData) {
|
|
|
+ const container = document.getElementById('monthly-table');
|
|
|
+ if (!container) return;
|
|
|
+
|
|
|
+ const incomeData = monthlyData.datasets[0].data;
|
|
|
+ const expenseData = monthlyData.datasets[1].data;
|
|
|
+ const monthNames = monthlyData.labels;
|
|
|
+
|
|
|
+ let tableHtml = `
|
|
|
+ <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">Delta</div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ monthNames.forEach((month, index) => {
|
|
|
+ const income = parseFloat(incomeData[index] || 0);
|
|
|
+ const expense = parseFloat(expenseData[index] || 0);
|
|
|
+ const net = income - expense;
|
|
|
+ const rowClass = net < 0 ? 'negative' : (net > 0 ? 'positive' : 'neutral');
|
|
|
+
|
|
|
+ tableHtml += `
|
|
|
+ <div class="table-row ${rowClass}">
|
|
|
+ <div class="table-cell month-name">${month}</div>
|
|
|
+ <div class="table-cell income">€${new Intl.NumberFormat('it-IT').format(income)}</div>
|
|
|
+ <div class="table-cell expense">€${new Intl.NumberFormat('it-IT').format(expense)}</div>
|
|
|
+ <div class="table-cell net">€${new Intl.NumberFormat('it-IT').format(net)}</div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ });
|
|
|
+
|
|
|
+ tableHtml += '</div>';
|
|
|
+ container.innerHTML = tableHtml;
|
|
|
+ },
|
|
|
+
|
|
|
+ updateMembersTable: function (membersData) {
|
|
|
+ const container = document.getElementById('members-table');
|
|
|
+ if (!container) return;
|
|
|
+
|
|
|
+ const seasonLabels = membersData.labels;
|
|
|
+ const memberCounts = membersData.datasets[0].data;
|
|
|
+
|
|
|
+ let tableHtml = `
|
|
|
+ <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>
|
|
|
+ `;
|
|
|
+
|
|
|
+ seasonLabels.forEach((season, index) => {
|
|
|
+ const current = parseInt(memberCounts[index] || 0);
|
|
|
+ const previous = index > 0 ? parseInt(memberCounts[index - 1] || 0) : 0;
|
|
|
+ const variation = index > 0 ? current - previous : 0;
|
|
|
+ const variationPercent = previous > 0 ? Math.round((variation / previous) * 100 * 10) / 10 : 0;
|
|
|
+ const rowClass = variation > 0 ? 'positive' : (variation < 0 ? 'negative' : 'neutral');
|
|
|
+
|
|
|
+ let variationText = '—';
|
|
|
+ if (index > 0) {
|
|
|
+ if (variation > 0) {
|
|
|
+ variationText = `<span class="variation-positive">+${variation} (+${variationPercent}%)</span>`;
|
|
|
+ } else if (variation < 0) {
|
|
|
+ variationText = `<span class="variation-negative">${variation} (${variationPercent}%)</span>`;
|
|
|
+ } else {
|
|
|
+ variationText = `<span class="variation-neutral">${variation}</span>`;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- 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
|
|
|
+ tableHtml += `
|
|
|
+ <div class="table-row ${rowClass}">
|
|
|
+ <div class="table-cell season-name">${season}</div>
|
|
|
+ <div class="table-cell members-count">${new Intl.NumberFormat('it-IT').format(current)}</div>
|
|
|
+ <div class="table-cell variation">${variationText}</div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ });
|
|
|
+
|
|
|
+ tableHtml += '</div>';
|
|
|
+ container.innerHTML = tableHtml;
|
|
|
+ },
|
|
|
+
|
|
|
+ createCourseChart: function () {
|
|
|
+ console.log('Creating course chart...');
|
|
|
+ const seasonFilter = '{{ $seasonFilter }}';
|
|
|
+ const selectedCourse = '{{ $selectedCourse ?? '' }}';
|
|
|
+ const seasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
|
|
|
+ console.log('Selected course:', selectedCourse, 'for season:', seasonFilter);
|
|
|
+
|
|
|
+ // Add this check at the beginning
|
|
|
+ if (!selectedCourse || selectedCourse.trim() === '') {
|
|
|
+ console.log('No course selected, skipping chart creation');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const chartId = `courses-chart-${seasonKey}-${selectedCourse}`;
|
|
|
+ const canvas = document.getElementById(chartId);
|
|
|
+ if (!canvas) return;
|
|
|
+
|
|
|
+ this.destroyChart(chartId);
|
|
|
+
|
|
|
+ const courseData = @json($this->getCourseMonthlyEarnings());
|
|
|
+ const ctx = canvas.getContext('2d');
|
|
|
+ this.charts[chartId] = new Chart(ctx, {
|
|
|
+ type: 'bar',
|
|
|
+ data: {
|
|
|
+ labels: courseData.labels,
|
|
|
+ datasets: courseData.datasets.map(dataset => {
|
|
|
+ if (dataset.type === 'line') {
|
|
|
+ return {
|
|
|
+ ...dataset,
|
|
|
+ type: 'line',
|
|
|
+ fill: false,
|
|
|
+ backgroundColor: 'transparent'
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return dataset;
|
|
|
+ })
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ interaction: {
|
|
|
+ mode: 'index',
|
|
|
+ intersect: false,
|
|
|
+ },
|
|
|
+ scales: {
|
|
|
+ x: {
|
|
|
+ grid: { display: false },
|
|
|
+ ticks: { font: { weight: '500' } }
|
|
|
},
|
|
|
- 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);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ y: {
|
|
|
+ beginAtZero: true,
|
|
|
+ grid: {
|
|
|
+ color: 'rgba(0, 0, 0, 0.1)',
|
|
|
+ borderDash: [5, 5]
|
|
|
},
|
|
|
- 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);
|
|
|
- }
|
|
|
- }
|
|
|
+ ticks: {
|
|
|
+ callback: function (value) {
|
|
|
+ return '€' + new Intl.NumberFormat('it-IT').format(value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ plugins: {
|
|
|
+ legend: {
|
|
|
+ display: true,
|
|
|
+ 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);
|
|
|
}
|
|
|
- },
|
|
|
- animation: {
|
|
|
- duration: 1000,
|
|
|
- easing: 'easeOutQuart'
|
|
|
}
|
|
|
}
|
|
|
- });
|
|
|
+ },
|
|
|
+ animation: {
|
|
|
+ duration: 1000,
|
|
|
+ easing: 'easeOutQuart'
|
|
|
+ }
|
|
|
}
|
|
|
- })();
|
|
|
- </script>
|
|
|
- </div>
|
|
|
-
|
|
|
- </div>
|
|
|
+ });
|
|
|
+ },
|
|
|
+ createCourseChartWithValue: function (selectedCourseValue) {
|
|
|
+ console.log('Creating course chart with value:', selectedCourseValue);
|
|
|
+ const seasonFilter = '{{ $seasonFilter }}';
|
|
|
+ const seasonKey = '{{ str_replace('-', '', $seasonFilter) }}';
|
|
|
+
|
|
|
+ const chartId = `courses-chart-${seasonKey}-${selectedCourseValue}`;
|
|
|
+ const canvas = document.getElementById(chartId);
|
|
|
+ if (!canvas) {
|
|
|
+ console.log('Canvas not found for chart ID:', chartId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.destroyChart(chartId);
|
|
|
+
|
|
|
+ // Call Livewire method to get fresh data
|
|
|
+ @this.call('getCourseData', selectedCourseValue).then(courseData => {
|
|
|
+ console.log('Received course data:', courseData);
|
|
|
+
|
|
|
+ if (!courseData || !courseData.labels || courseData.labels.length === 0) {
|
|
|
+ console.log('No data available for chart');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const ctx = canvas.getContext('2d');
|
|
|
+ this.charts[chartId] = new Chart(ctx, {
|
|
|
+ type: 'bar',
|
|
|
+ data: {
|
|
|
+ labels: courseData.labels,
|
|
|
+ datasets: courseData.datasets
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ responsive: true,
|
|
|
+ maintainAspectRatio: false,
|
|
|
+ scales: {
|
|
|
+ y: {
|
|
|
+ beginAtZero: true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }).catch(error => {
|
|
|
+ console.error('Error calling getCourseData:', error);
|
|
|
+ });
|
|
|
+},
|
|
|
+ };
|
|
|
+
|
|
|
+ document.addEventListener('DOMContentLoaded', function () {
|
|
|
+ setTimeout(() => {
|
|
|
+ window.ReportsChartManager.updateMainCharts();
|
|
|
+ }, 100);
|
|
|
+ });
|
|
|
+
|
|
|
+ document.addEventListener('livewire:navigated', function () {
|
|
|
+ setTimeout(() => {
|
|
|
+ window.ReportsChartManager.updateMainCharts();
|
|
|
+ }, 100);
|
|
|
+ });
|
|
|
+ document.addEventListener('livewire:load', function () {
|
|
|
+ Livewire.on('courseSelected', (courseId) => {
|
|
|
+ console.log('Course selected event received:', courseId);
|
|
|
+ setTimeout(() => {
|
|
|
+ window.ReportsChartManager.createCourseChartWithValue(courseId);
|
|
|
+ }, 200);
|
|
|
+ });
|
|
|
+ });
|
|
|
+ </script>
|
|
|
</div>
|