- Integra ASP.NET Core Identity: login/password, lockout brute-force, cookie sicuri, password policy forte - Seed automatico utente admin da variabili ambiente (fallback password temporanea forte) - Tutte le pagine principali ora protette con [Authorize] e redirect automatico a /login - Nuovo layout login/logout pulito senza sidebar, spinner durante redirect - NavMenu mostra utente autenticato e logout - Rimosse credenziali Bidoo da env/Docker: ora solo cookie sessione da UI - Aggiornata documentazione: sicurezza, deploy, backup, troubleshooting - Fix NavigationException, SectionRegistry, errori header read-only - Versione incrementata a 1.2.0, pronto per deploy production Tailscale/Unraid
267 lines
11 KiB
Plaintext
267 lines
11 KiB
Plaintext
@page "/statistics"
|
|
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
|
@inject StatsService StatsService
|
|
@inject IJSRuntime JSRuntime
|
|
|
|
<PageTitle>Statistiche - AutoBidder</PageTitle>
|
|
|
|
<div class="statistics-container animate-fade-in p-4">
|
|
<div class="d-flex align-items-center justify-content-between mb-4 animate-fade-in-down">
|
|
<div class="d-flex align-items-center">
|
|
<i class="bi bi-bar-chart-fill text-primary me-3" style="font-size: 2.5rem;"></i>
|
|
<h2 class="mb-0 fw-bold">Statistiche</h2>
|
|
</div>
|
|
<button class="btn btn-primary hover-lift" @onclick="RefreshStats" disabled="@isLoading">
|
|
@if (isLoading)
|
|
{
|
|
<span class="spinner-border spinner-border-sm me-2"></span>
|
|
}
|
|
else
|
|
{
|
|
<i class="bi bi-arrow-clockwise me-1"></i>
|
|
}
|
|
Aggiorna
|
|
</button>
|
|
</div>
|
|
|
|
@if (errorMessage != null)
|
|
{
|
|
<div class="alert alert-danger border-0 shadow-sm animate-shake mb-4">
|
|
<i class="bi bi-exclamation-triangle-fill me-2"></i> @errorMessage
|
|
</div>
|
|
}
|
|
|
|
@if (isLoading)
|
|
{
|
|
<div class="text-center py-5">
|
|
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem;"></div>
|
|
<p class="mt-3 text-muted">Caricamento statistiche...</p>
|
|
</div>
|
|
}
|
|
else if (totalStats != null)
|
|
{
|
|
<!-- CARD TOTALI -->
|
|
<div class="row g-3 mb-4 animate-fade-in-up">
|
|
<div class="col-md-3">
|
|
<div class="stats-card card border-0 shadow-sm hover-lift">
|
|
<div class="card-body text-center">
|
|
<i class="bi bi-hand-index-fill text-primary" style="font-size: 2.5rem;"></i>
|
|
<h3 class="mt-3 mb-1 fw-bold">@totalStats.TotalBidsUsed</h3>
|
|
<p class="text-muted mb-0">Puntate Usate</p>
|
|
<small class="text-muted">€@((totalStats.TotalBidsUsed * 0.20).ToString("F2"))</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="stats-card card border-0 shadow-sm hover-lift">
|
|
<div class="card-body text-center">
|
|
<i class="bi bi-trophy-fill text-warning" style="font-size: 2.5rem;"></i>
|
|
<h3 class="mt-3 mb-1 fw-bold">@totalStats.TotalAuctionsWon</h3>
|
|
<p class="text-muted mb-0">Aste Vinte</p>
|
|
<small class="text-muted">Win Rate: @totalStats.WinRate.ToString("F1")%</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="stats-card card border-0 shadow-sm hover-lift">
|
|
<div class="card-body text-center">
|
|
<i class="bi bi-piggy-bank-fill text-success" style="font-size: 2.5rem;"></i>
|
|
<h3 class="mt-3 mb-1 fw-bold">€@totalStats.TotalSavings.ToString("F2")</h3>
|
|
<p class="text-muted mb-0">Risparmio Totale</p>
|
|
<small class="text-muted">ROI: @roi.ToString("F1")%</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="stats-card card border-0 shadow-sm hover-lift">
|
|
<div class="card-body text-center">
|
|
<i class="bi bi-speedometer text-info" style="font-size: 2.5rem;"></i>
|
|
<h3 class="mt-3 mb-1 fw-bold">@totalStats.AverageBidsPerAuction.ToString("F1")</h3>
|
|
<p class="text-muted mb-0">Puntate/Asta Media</p>
|
|
<small class="text-muted">Latency: @totalStats.AverageLatency.ToString("F0")ms</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- GRAFICI -->
|
|
<div class="row g-4 mb-4 animate-fade-in-up delay-100">
|
|
<div class="col-lg-8">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-primary text-white">
|
|
<h5 class="mb-0"><i class="bi bi-graph-up me-2"></i>Spesa Giornaliera (Ultimi 30 Giorni)</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<canvas id="moneyChart" height="80"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-4">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-success text-white">
|
|
<h5 class="mb-0"><i class="bi bi-pie-chart me-2"></i>Aste Vinte vs Perse</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<canvas id="winsChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ASTE RECENTI -->
|
|
@if (recentResults != null && recentResults.Any())
|
|
{
|
|
<div class="card border-0 shadow-sm animate-fade-in-up delay-200">
|
|
<div class="card-header bg-info text-white">
|
|
<h5 class="mb-0"><i class="bi bi-clock-history me-2"></i>Aste Recenti</h5>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-striped table-hover mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>Asta</th>
|
|
<th>Prezzo Finale</th>
|
|
<th>Puntate</th>
|
|
<th>Risultato</th>
|
|
<th>Risparmio</th>
|
|
<th>Data</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach (var result in recentResults.Take(10))
|
|
{
|
|
<tr class="@(result.Won ? "table-success" : "")">
|
|
<td class="fw-semibold">@result.AuctionName</td>
|
|
<td>€@result.FinalPrice.ToString("F2")</td>
|
|
<td><span class="badge bg-info">@result.BidsUsed</span></td>
|
|
<td>
|
|
@if (result.Won)
|
|
{
|
|
<span class="badge bg-success"><i class="bi bi-trophy-fill"></i> Vinta</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="badge bg-secondary"><i class="bi bi-x-circle"></i> Persa</span>
|
|
}
|
|
</td>
|
|
<td class="@(result.Savings > 0 ? "text-success fw-bold" : "text-danger")">
|
|
@if (result.Savings.HasValue)
|
|
{
|
|
@((result.Savings.Value > 0 ? "+" : "") + "€" + result.Savings.Value.ToString("F2"))
|
|
}
|
|
else
|
|
{
|
|
<span class="text-muted">-</span>
|
|
}
|
|
</td>
|
|
<td class="text-muted small">@DateTime.Parse(result.Timestamp).ToString("dd/MM HH:mm")</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
else
|
|
{
|
|
<div class="alert alert-info border-0 shadow-sm animate-scale-in">
|
|
<i class="bi bi-info-circle-fill me-2"></i>
|
|
Nessuna statistica disponibile. Completa alcune aste per vedere le statistiche.
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<style>
|
|
.statistics-container {
|
|
max-width: 1600px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.stats-card {
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.stats-card:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2) !important;
|
|
}
|
|
</style>
|
|
|
|
@code {
|
|
private TotalStats? totalStats;
|
|
private List<AuctionResult>? recentResults;
|
|
private string? errorMessage;
|
|
private bool isLoading = false;
|
|
private double roi = 0;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await RefreshStats();
|
|
}
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender && totalStats != null)
|
|
{
|
|
await RenderCharts();
|
|
}
|
|
}
|
|
|
|
private async Task RefreshStats()
|
|
{
|
|
try
|
|
{
|
|
isLoading = true;
|
|
errorMessage = null;
|
|
StateHasChanged();
|
|
|
|
// Carica statistiche
|
|
totalStats = await StatsService.GetTotalStatsAsync();
|
|
roi = await StatsService.CalculateROIAsync();
|
|
recentResults = await StatsService.GetRecentAuctionResultsAsync(20);
|
|
|
|
// Render grafici dopo il caricamento
|
|
if (totalStats != null)
|
|
{
|
|
await RenderCharts();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errorMessage = $"Errore caricamento statistiche: {ex.Message}";
|
|
Console.WriteLine($"[ERROR] Statistics: {ex}");
|
|
}
|
|
finally
|
|
{
|
|
isLoading = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private async Task RenderCharts()
|
|
{
|
|
try
|
|
{
|
|
var chartData = await StatsService.GetChartDataAsync(30);
|
|
|
|
// Render grafico spesa
|
|
await JSRuntime.InvokeVoidAsync("renderMoneyChart",
|
|
chartData.Labels,
|
|
chartData.MoneySpent,
|
|
chartData.Savings);
|
|
|
|
// Render grafico wins
|
|
await JSRuntime.InvokeVoidAsync("renderWinsChart",
|
|
totalStats!.TotalAuctionsWon,
|
|
totalStats!.TotalAuctionsLost);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[ERROR] Render charts: {ex.Message}");
|
|
}
|
|
}
|
|
}
|