Restyling monitor aste: toolbar compatta, split panel, UX

- Nuova toolbar compatta con azioni rapide e indicatori stato aste
- Layout a pannelli ridimensionabili con splitter drag&drop
- Tabella aste compatta, ping colorato, azioni XS
- Pulsanti per rimozione aste per stato (attive, vinte, ecc.)
- Dettagli asta sempre visibili in pannello inferiore
- Statistiche prodotti: filtro, ordinamento, editing limiti default
- Limiti default prodotto salvati in DB, applicabili a tutte le aste
- Migliorata sidebar utente con info sessione sempre visibili
- Log motivi blocco puntata sempre visibili, suggerimenti timing
- Miglioramenti filtri, UX responsive, fix minori e feedback visivi
This commit is contained in:
2026-02-06 15:35:53 +01:00
parent 45dd205270
commit 5b95f18889
11 changed files with 2289 additions and 585 deletions

View File

@@ -99,6 +99,13 @@ namespace AutoBidder.Models
[JsonIgnore]
public bool BidScheduled { get; set; }
/// <summary>
/// Timer per cui è stata schedulata l'ultima puntata.
/// Usato per evitare doppie puntate sullo stesso ciclo.
/// </summary>
[JsonIgnore]
public double LastScheduledTimerMs { get; set; }
// Storico
public List<BidHistory> BidHistory { get; set; } = new List<BidHistory>();
public Dictionary<string, BidderInfo> BidderStats { get; set; } = new(StringComparer.OrdinalIgnoreCase);

View File

@@ -35,6 +35,14 @@ namespace AutoBidder.Models
public int? RecommendedMaxResets { get; set; }
public int? RecommendedMaxBids { get; set; }
// Valori di default definiti dall'utente (editabili)
public double? UserDefaultMinPrice { get; set; }
public double? UserDefaultMaxPrice { get; set; }
public int? UserDefaultMinResets { get; set; }
public int? UserDefaultMaxResets { get; set; }
public int? UserDefaultMaxBids { get; set; }
public int? UserDefaultBidBeforeDeadlineMs { get; set; }
// JSON con statistiche per fascia oraria
public string? HourlyStatsJson { get; set; }

View File

@@ -7,148 +7,155 @@
<PageTitle>Monitor Aste - AutoBidder</PageTitle>
<div class="auction-monitor animate-fade-in">
<!-- Toolbar Superiore -->
<div class="toolbar animate-fade-in-down">
<!-- Box Sessione Utente - Compatto in linea -->
<div class="toolbar-user-info">
@if (!string.IsNullOrEmpty(sessionUsername))
{
<div class="user-card connected">
<i class="bi bi-person-circle user-icon"></i>
<span class="user-name">@sessionUsername</span>
<div class="divider"></div>
<div class="stat-compact">
<i class="bi bi-hand-index-thumb-fill"></i>
<span class="stat-value @GetBidsClass()">@sessionRemainingBids</span>
<div class="auction-monitor-container">
<!-- Toolbar Compatta -->
<div class="toolbar-compact">
<!-- Pulsanti Azioni Massiva (senza conteggi) -->
<div class="btn-group-actions">
<button class="action-btn success" @onclick="StartAll" title="Avvia tutte le aste">
<i class="bi bi-play-fill"></i>
</button>
<button class="action-btn warning" @onclick="PauseAll" title="Metti in pausa tutte le aste">
<i class="bi bi-pause-fill"></i>
</button>
<button class="action-btn secondary" @onclick="StopAll" title="Ferma tutte le aste">
<i class="bi bi-stop-fill"></i>
</button>
</div>
<div class="divider"></div>
<div class="stat-compact">
<i class="bi bi-wallet2"></i>
<span class="stat-value text-success">€@sessionShopCredit.ToString("F2")</span>
<!-- Indicatori Stato Aste (tutti gli stati) -->
<div class="status-indicators">
<div class="status-pill total" title="Totale aste">
<i class="bi bi-collection"></i>
<span>@auctions.Count</span>
</div>
@if (sessionAuctionsWon > 0)
{
<div class="divider"></div>
<div class="stat-compact">
<div class="status-pill active" title="Aste attive">
<i class="bi bi-play-circle-fill"></i>
<span>@GetActiveAuctionsCount()</span>
</div>
<div class="status-pill paused" title="Aste in pausa">
<i class="bi bi-pause-circle-fill"></i>
<span>@GetPausedAuctionsCount()</span>
</div>
<div class="status-pill stopped" title="Aste fermate">
<i class="bi bi-stop-circle-fill"></i>
<span>@GetStoppedAuctionsCount()</span>
</div>
<div class="status-pill won" title="Aste vinte">
<i class="bi bi-trophy-fill"></i>
<span class="stat-value text-warning">@sessionAuctionsWon</span>
<span>@GetWonAuctionsCount()</span>
</div>
}
<div class="status-pill lost" title="Aste perse">
<i class="bi bi-x-circle-fill"></i>
<span>@GetLostAuctionsCount()</span>
</div>
}
else
{
<div class="user-card disconnected">
<i class="bi bi-person-x user-icon"></i>
<span class="user-name text-muted">Non connesso</span>
</div>
}
</div>
<!-- Pulsanti Azioni (Centro-Destra) -->
<div class="toolbar-actions">
<button class="btn btn-success hover-lift" @onclick="StartAll">
<i class="bi bi-play-fill"></i> Avvia Tutto
<!-- Pulsanti Gestione -->
<div class="btn-group-manage">
<button class="manage-btn primary" @onclick="ShowAddAuctionDialog" title="Aggiungi nuova asta">
<i class="bi bi-plus-lg"></i>
</button>
<button class="btn btn-warning hover-lift" @onclick="PauseAll">
<i class="bi bi-pause-fill"></i> Pausa Tutto
<button class="manage-btn danger" @onclick="RemoveSelectedAuction" disabled="@(selectedAuction == null)" title="Rimuovi selezionata">
<i class="bi bi-trash"></i>
</button>
<button class="btn btn-danger hover-lift" @onclick="StopAll">
<i class="bi bi-stop-fill"></i> Ferma Tutto
<div class="manage-separator"></div>
<button class="manage-btn outline-success" @onclick="RemoveActiveAuctions" disabled="@(GetActiveAuctionsCount() == 0)" title="Rimuovi attive">
<i class="bi bi-play-circle"></i>
</button>
<button class="btn btn-primary ms-3 hover-lift" @onclick="ShowAddAuctionDialog">
<i class="bi bi-plus-lg"></i> Aggiungi Asta
<button class="manage-btn outline-warning" @onclick="RemovePausedAuctions" disabled="@(GetPausedAuctionsCount() == 0)" title="Rimuovi in pausa">
<i class="bi bi-pause-circle"></i>
</button>
<button class="btn btn-secondary hover-lift" @onclick="RemoveSelectedAuction" disabled="@(selectedAuction == null)">
<i class="bi bi-trash"></i> Rimuovi
<button class="manage-btn outline-secondary" @onclick="RemoveStoppedAuctions" disabled="@(GetStoppedAuctionsCount() == 0)" title="Rimuovi fermate">
<i class="bi bi-stop-circle"></i>
</button>
<button class="btn btn-outline-warning hover-lift" @onclick="RemoveCompletedAuctions" disabled="@(!HasCompletedAuctions())" title="Rimuovi aste terminate (vengono salvate nel database)">
<i class="bi bi-check2-all"></i> Rimuovi Terminate
<button class="manage-btn outline-gold" @onclick="RemoveWonAuctions" disabled="@(GetWonAuctionsCount() == 0)" title="Rimuovi vinte">
<i class="bi bi-trophy"></i>
</button>
<button class="btn btn-outline-danger hover-lift" @onclick="RemoveAllAuctions" disabled="@(auctions.Count == 0)" title="Rimuovi tutte le aste (quelle terminate verranno salvate)">
<i class="bi bi-trash-fill"></i> Rimuovi Tutte
<button class="manage-btn outline-danger" @onclick="RemoveLostAuctions" disabled="@(GetLostAuctionsCount() == 0)" title="Rimuovi perse">
<i class="bi bi-x-circle"></i>
</button>
<div class="manage-separator"></div>
<button class="manage-btn danger-fill" @onclick="RemoveAllAuctions" disabled="@(auctions.Count == 0)" title="Rimuovi TUTTE">
<i class="bi bi-trash-fill"></i>
</button>
</div>
</div>
<div class="content-layout">
<!-- GRIGLIA ASTE - PARTE SUPERIORE SINISTRA -->
<div class="auctions-grid-section animate-fade-in-left delay-100 shadow-hover">
<h3><i class="bi bi-list-check"></i> Aste Monitorate (@auctions.Count)</h3>
<!-- Area Principale con Layout a Griglia -->
<div class="main-content-area">
<!-- Riga Superiore: Aste + Log -->
<div class="top-row" id="topRow">
<!-- Pannello Aste -->
<div class="panel panel-auctions" id="panelAuctions">
<div class="panel-header">
<span><i class="bi bi-list-check"></i> Aste Monitorate</span>
</div>
@if (auctions.Count == 0)
{
<div class="alert alert-info animate-fade-in-up">
<i class="bi bi-info-circle"></i> Nessuna asta monitorata. Clicca su "Aggiungi Asta" per iniziare.
<div class="alert alert-info animate-fade-in-up m-2">
<i class="bi bi-info-circle"></i> Nessuna asta monitorata. Clicca su <i class="bi bi-plus-lg"></i> per iniziare.
</div>
}
else
{
<div class="table-responsive">
<table class="table table-striped table-hover mb-0 table-fixed">
<div class="table-responsive panel-content">
<table class="table table-striped table-hover mb-0 table-fixed table-compact">
<thead>
<tr>
<th class="col-stato sortable-header" @onclick='() => SortAuctionsBy("stato")'><i class="bi bi-toggle-on"></i> Stato @GetSortIndicator("stato")</th>
<th class="col-nome sortable-header" @onclick='() => SortAuctionsBy("nome")'><i class="bi bi-tag"></i> Nome @GetSortIndicator("nome")</th>
<th class="col-prezzo sortable-header" @onclick='() => SortAuctionsBy("prezzo")'><i class="bi bi-currency-euro"></i> Prezzo @GetSortIndicator("prezzo")</th>
<th class="col-timer sortable-header" @onclick='() => SortAuctionsBy("timer")'><i class="bi bi-clock"></i> Timer @GetSortIndicator("timer")</th>
<th class="col-ultimo"><i class="bi bi-person"></i> Ultimo</th>
<th class="col-click sortable-header" @onclick='() => SortAuctionsBy("puntate")'><i class="bi bi-hand-index" style="font-size: 0.85rem;"></i> Puntate @GetSortIndicator("puntate")</th>
<th class="col-ping sortable-header" @onclick='() => SortAuctionsBy("ping")'><i class="bi bi-speedometer"></i> Ping @GetSortIndicator("ping")</th>
<th class="col-azioni"><i class="bi bi-gear"></i> Azioni</th>
<th class="col-stato sortable-header" @onclick='() => SortAuctionsBy("stato")'>Stato @GetSortIndicator("stato")</th>
<th class="col-nome sortable-header" @onclick='() => SortAuctionsBy("nome")'>Nome @GetSortIndicator("nome")</th>
<th class="col-prezzo sortable-header" @onclick='() => SortAuctionsBy("prezzo")'> @GetSortIndicator("prezzo")</th>
<th class="col-timer sortable-header" @onclick='() => SortAuctionsBy("timer")'>Timer @GetSortIndicator("timer")</th>
<th class="col-ultimo">Ultimo</th>
<th class="col-click sortable-header" @onclick='() => SortAuctionsBy("puntate")'>Punt. @GetSortIndicator("puntate")</th>
<th class="col-ping sortable-header" @onclick='() => SortAuctionsBy("ping")'>Ping @GetSortIndicator("ping")</th>
<th class="col-azioni">Azioni</th>
</tr>
</thead>
<tbody>
@foreach (var auction in GetSortedAuctions())
{
<tr class="@GetRowClass(auction) @(selectedAuction == auction ? "selected-row" : "") table-row-enter transition-all"
@onclick="() => SelectAuction(auction)"
style="cursor: pointer;">
<tr class="@GetRowClass(auction) @(selectedAuction == auction ? "selected-row" : "")"
@onclick="() => SelectAuction(auction)">
<td class="col-stato">
<span class="badge @GetStatusBadgeClass(auction) @GetStatusAnimationClass(auction)">
@((MarkupString)GetStatusIcon(auction)) @GetStatusText(auction)
</span>
</td>
<td class="col-nome fw-semibold">@auction.Name</td>
<td class="col-nome">@auction.Name</td>
<td class="col-prezzo @GetPriceClass(auction)">@GetPriceDisplay(auction)</td>
<td class="col-timer">@GetTimerDisplay(auction)</td>
<td class="col-ultimo">@GetLastBidder(auction)</td>
<td class="col-click bids-column fw-bold">@GetMyBidsCount(auction)</td>
<td class="col-ping">@GetPingDisplay(auction)</td>
<td class="col-click bids-column">@GetMyBidsCount(auction)</td>
<td class="col-ping @GetPingClass(auction)">@GetPingDisplay(auction)</td>
<td class="col-azioni">
<div class="btn-group btn-group-sm" @onclick:stopPropagation="true">
<button class="btn btn-primary hover-scale"
<button class="btn btn-xs btn-primary"
@onclick="() => ManualBidAuction(auction)"
title="Punta Manualmente"
title="Punta"
disabled="@IsManualBidding(auction)">
@if (IsManualBidding(auction))
{
<span class="spinner-border spinner-border-sm" role="status"></span>
}
else
{
<i class="bi bi-hand-index-thumb"></i>
}
</button>
@if (auction.IsActive && !auction.IsPaused)
{
<button class="btn btn-warning hover-scale" @onclick="() => PauseAuction(auction)" title="Pausa">
<button class="btn btn-xs btn-warning" @onclick="() => PauseAuction(auction)" title="Pausa">
<i class="bi bi-pause-fill"></i>
</button>
}
else if (auction.IsPaused)
{
<button class="btn btn-success hover-scale" @onclick="() => ResumeAuction(auction)" title="Riprendi">
<button class="btn btn-xs btn-success" @onclick="() => ResumeAuction(auction)" title="Riprendi">
<i class="bi bi-play-fill"></i>
</button>
}
else
{
<button class="btn btn-success hover-scale" @onclick="() => StartAuction(auction)" title="Avvia">
<button class="btn btn-xs btn-success" @onclick="() => StartAuction(auction)" title="Avvia">
<i class="bi bi-play-fill"></i>
</button>
}
<button class="btn btn-danger hover-scale" @onclick="() => StopAuction(auction)" title="Ferma">
<button class="btn btn-xs btn-danger" @onclick="() => StopAuction(auction)" title="Ferma">
<i class="bi bi-stop-fill"></i>
</button>
</div>
@@ -161,18 +168,18 @@
}
</div>
<!-- SPLITTER VERTICALE -->
<div class="splitter-vertical"></div>
<!-- Splitter Verticale -->
<div class="gutter gutter-vertical" id="gutterVertical"></div>
<!-- LOG GLOBALE - PARTE SUPERIORE DESTRA -->
<div class="global-log animate-fade-in-right delay-200">
<div class="d-flex justify-content-between align-items-center">
<h4 class="mb-0"><i class="bi bi-terminal"></i> Log Globale</h4>
<button class="btn btn-sm btn-secondary" @onclick="ClearGlobalLog">
<!-- Pannello Log -->
<div class="panel panel-log" id="panelLog">
<div class="panel-header">
<span><i class="bi bi-terminal"></i> Log</span>
<button class="btn btn-xs btn-secondary" @onclick="ClearGlobalLog">
<i class="bi bi-trash"></i>
</button>
</div>
<div class="log-box" id="globalLogContainer" @ref="globalLogRef">
<div class="panel-content log-box" id="globalLogContainer" @ref="globalLogRef">
@if (globalLog.Count == 0)
{
<div class="text-muted"><i class="bi bi-inbox"></i> Nessun log ancora...</div>
@@ -184,19 +191,23 @@
<div class="@GetLogEntryClass(logEntry)">@logEntry.Message</div>
}
}
<div id="logScrollAnchor"></div>
</div>
</div>
</div>
<!-- SPLITTER ORIZZONTALE -->
<div class="splitter-horizontal"></div>
<!-- Splitter Orizzontale -->
<div class="gutter gutter-horizontal" id="gutterHorizontal"></div>
<!-- DETTAGLI ASTA CON TABS - PARTE INFERIORE (full width) -->
<!-- Riga Inferiore: Dettagli Asta -->
<div class="bottom-row" id="bottomRow">
<div class="panel panel-details" id="panelDetails">
@if (selectedAuction != null)
{
<div class="auction-details-tabs animate-fade-in-up delay-300 shadow-hover">
<h3><i class="bi bi-info-circle-fill"></i> @selectedAuction.Name <small class="text-muted">(ID: @selectedAuction.AuctionId)</small></h3>
<div class="auction-details-content">
<div class="details-header">
<i class="bi bi-info-circle-fill"></i> @selectedAuction.Name
<small class="text-muted">(ID: @selectedAuction.AuctionId)</small>
</div>
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
@@ -243,7 +254,6 @@
</div>
</div>
<!-- Layout compatto a griglia per impostazioni -->
<div class="settings-grid-compact">
<div class="setting-item">
<label><i class="bi bi-speedometer2"></i> Anticipo (ms)</label>
@@ -263,7 +273,6 @@
</div>
</div>
<!-- Pulsante Applica Limiti Consigliati -->
<div class="mt-2 pt-2 border-top">
<button class="btn btn-outline-primary btn-sm w-100"
@onclick="ApplyRecommendedLimitsToSelected"
@@ -295,7 +304,6 @@
<div class="tab-panel-content">
@if (selectedAuction.CalculatedValue != null)
{
<!-- Sezione Principale - Compatta -->
<div class="product-info-compact">
<div class="info-cards">
<div class="info-card primary">
@@ -305,7 +313,6 @@
<strong>@GetBuyNowPriceDisplay(selectedAuction)</strong>
</div>
</div>
<div class="info-card info">
<i class="bi bi-truck"></i>
<div>
@@ -314,41 +321,33 @@
</div>
</div>
</div>
<!-- Calcoli in linea -->
<div class="calc-inline">
<div class="calc-item">
<i class="bi bi-currency-euro"></i>
<span class="label">Prezzo attuale</span>
<span class="value">€@selectedAuction.CalculatedValue.CurrentPrice.ToString("F2")</span>
</div>
<div class="calc-item">
<i class="bi bi-hand-index"></i>
<span class="label">Totale puntate</span>
<span class="value">@selectedAuction.CalculatedValue.TotalBids</span>
</div>
<div class="calc-item highlight">
<i class="bi bi-person-check-fill"></i>
<span class="label">Tue puntate</span>
<span class="value">@selectedAuction.CalculatedValue.MyBids</span>
</div>
<div class="calc-item">
<i class="bi bi-cash-coin"></i>
<span class="label">Costo puntate</span>
<span class="value">€@selectedAuction.CalculatedValue.MyBidsCost.ToString("F2")</span>
</div>
</div>
<!-- Totali compatti -->
<div class="totals-compact">
<div class="total-item warning">
<span>Costo Totale se vinci</span>
<strong>€@selectedAuction.CalculatedValue.TotalCostIfWin.ToString("F2")</strong>
</div>
<div class="total-item @(selectedAuction.CalculatedValue.Savings > 0 ? "success" : "danger")">
<span>
<i class="bi bi-@(selectedAuction.CalculatedValue.Savings > 0 ? "arrow-down-circle-fill" : "arrow-up-circle-fill")"></i>
@@ -356,25 +355,17 @@
</span>
<strong>@GetSavingsDisplay(selectedAuction)</strong>
</div>
<div class="verdict-badge @(selectedAuction.CalculatedValue.Savings > 0 ? "success" : "danger")">
<i class="bi bi-@(selectedAuction.CalculatedValue.Savings > 0 ? "check-circle-fill" : "x-circle-fill")"></i>
@(selectedAuction.CalculatedValue.Savings > 0 ? "Conveniente!" : "Non conveniente")
</div>
</div>
</div>
@if (!string.IsNullOrEmpty(selectedAuction.CalculatedValue.Summary))
{
<div class="alert alert-info mt-3 mb-0">
<i class="bi bi-info-circle"></i> @selectedAuction.CalculatedValue.Summary
</div>
}
}
else
{
<div class="alert alert-secondary">
<i class="bi bi-hourglass-split"></i> Informazioni prodotto non ancora disponibili. Verranno caricate automaticamente.
<i class="bi bi-hourglass-split"></i> Informazioni prodotto non ancora disponibili.
</div>
}
</div>
@@ -384,13 +375,11 @@
<div class="tab-pane fade" id="content-history" role="tabpanel">
<div class="tab-panel-content">
@{
// ?? FIX: Rimuovi duplicati consecutivi (stesso prezzo + stesso utente)
var recentBidsList = GetRecentBidsSafe(selectedAuction);
var filteredBids = new List<BidHistoryEntry>();
BidHistoryEntry? lastBid = null;
foreach (var bid in recentBidsList)
{
// Salta se è un duplicato del precedente (stesso prezzo E stesso utente)
if (lastBid != null &&
Math.Abs(bid.Price - lastBid.Price) < 0.001m &&
bid.Username.Equals(lastBid.Username, StringComparison.OrdinalIgnoreCase))
@@ -405,29 +394,12 @@
{
<div class="table-responsive">
<table class="table table-sm table-striped">
<thead>
<tr>
<th>Utente</th>
<th>Prezzo</th>
<th>Data/Ora</th>
<th>Tipo</th>
</tr>
</thead>
<thead><tr><th>Utente</th><th>Prezzo</th><th>Data/Ora</th><th>Tipo</th></tr></thead>
<tbody>
@foreach (var bid in filteredBids.Take(50))
{
<tr class="@(bid.IsMyBid ? "my-bid-row" : "")">
<td>
@if (bid.IsMyBid)
{
<strong class="text-success">@bid.Username</strong>
<span class="badge bg-success ms-1">TU</span>
}
else
{
@bid.Username
}
</td>
<td>@if (bid.IsMyBid){<strong class="text-success">@bid.Username</strong><span class="badge bg-success ms-1">TU</span>}else{@bid.Username}</td>
<td class="fw-bold">€@bid.PriceFormatted</td>
<td class="text-muted small">@bid.TimeFormatted</td>
<td><span class="badge bg-secondary">@bid.BidType</span></td>
@@ -439,9 +411,7 @@
}
else
{
<div class="alert alert-secondary">
<i class="bi bi-inbox"></i> Nessuna puntata registrata per questa asta.
</div>
<div class="alert alert-secondary"><i class="bi bi-inbox"></i> Nessuna puntata registrata.</div>
}
</div>
</div>
@@ -450,75 +420,28 @@
<div class="tab-pane fade" id="content-bidders" role="tabpanel">
<div class="tab-panel-content">
@{
// ?? FIX: Usa BidderStats che contiene i conteggi CUMULATIVI (non limitati)
var bidderStatsCopy = selectedAuction.BidderStats
.Values
.OrderByDescending(b => b.BidCount)
.ToList();
// Per l'utente corrente, usa BidsUsedOnThisAuction (valore ufficiale dal server)
var bidderStatsCopy = selectedAuction.BidderStats.Values.OrderByDescending(b => b.BidCount).ToList();
var myOfficialBidsCount = selectedAuction.BidsUsedOnThisAuction ?? 0;
var currentUsername = GetCurrentUsername();
}
@if (bidderStatsCopy.Any())
{
// Calcola il totale CUMULATIVO
var totalBidsCumulative = bidderStatsCopy.Sum(b => b.BidCount);
// Correggi il conteggio per l'utente corrente se disponibile
var myBidder = bidderStatsCopy.FirstOrDefault(b =>
b.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase));
if (myBidder != null && myOfficialBidsCount > myBidder.BidCount)
{
// Usa il valore ufficiale se maggiore
totalBidsCumulative = totalBidsCumulative - myBidder.BidCount + myOfficialBidsCount;
}
<div class="table-responsive">
<table class="table table-sm table-striped">
<thead>
<tr>
<th>Posizione</th>
<th>Utente</th>
<th>Puntate</th>
<th>Percentuale</th>
</tr>
</thead>
<thead><tr><th>#</th><th>Utente</th><th>Puntate</th><th>%</th></tr></thead>
<tbody>
@for (int i = 0; i < bidderStatsCopy.Count; i++)
{
var bidder = bidderStatsCopy[i];
var isMe = bidder.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase);
// Per l'utente corrente usa il conteggio ufficiale
var displayCount = isMe && myOfficialBidsCount > bidder.BidCount
? myOfficialBidsCount
: bidder.BidCount;
var percentage = totalBidsCumulative > 0
? (displayCount * 100.0 / totalBidsCumulative)
: 0;
var displayCount = isMe && myOfficialBidsCount > bidder.BidCount ? myOfficialBidsCount : bidder.BidCount;
var percentage = totalBidsCumulative > 0 ? (displayCount * 100.0 / totalBidsCumulative) : 0;
<tr class="@(isMe ? "my-bid-row" : "")">
<td><span class="badge bg-primary">#@(i + 1)</span></td>
<td>
@if (isMe)
{
<strong class="text-success">@bidder.Username</strong>
<span class="badge bg-success ms-1">TU</span>
}
else
{
@bidder.Username
}
</td>
<td>@if (isMe){<strong class="text-success">@bidder.Username</strong>}else{@bidder.Username}</td>
<td class="fw-bold">@displayCount</td>
<td>
<div class="progress" style="height: 20px;">
<div class="progress-bar @(isMe ? "bg-success" : "bg-primary")"
role="progressbar"
style="width: @percentage.ToString("F1", System.Globalization.CultureInfo.InvariantCulture)%">
@percentage.ToString("F1")%
</div>
</div>
</td>
<td><div class="progress" style="height:16px;"><div class="progress-bar @(isMe?"bg-success":"bg-primary")" style="width:@percentage.ToString("F0")%">@percentage.ToString("F0")%</div></div></td>
</tr>
}
</tbody>
@@ -527,9 +450,7 @@
}
else
{
<div class="alert alert-secondary">
<i class="bi bi-inbox"></i> Nessun dato sui puntatori disponibile.
</div>
<div class="alert alert-secondary"><i class="bi bi-inbox"></i> Nessun dato sui puntatori.</div>
}
</div>
</div>
@@ -547,7 +468,7 @@
}
else
{
<div class="text-muted"><i class="bi bi-inbox"></i> Nessun log disponibile per questa asta.</div>
<div class="text-muted"><i class="bi bi-inbox"></i> Nessun log disponibile.</div>
}
</div>
</div>
@@ -557,13 +478,14 @@
}
else
{
<div class="auction-details-tabs animate-fade-in shadow-hover">
<div class="alert alert-secondary text-center my-5">
<i class="bi bi-arrow-up" style="font-size: 2rem; display: block; margin-bottom: 0.5rem;"></i>
<p class="mb-0">Seleziona un'asta dalla griglia per visualizzare i dettagli</p>
</div>
<div class="details-placeholder">
<i class="bi bi-arrow-up"></i>
<p>Seleziona un'asta per visualizzare i dettagli</p>
</div>
}
</div>
</div>
</div>
</div>
<!-- Modal Aggiungi Asta -->
@@ -571,7 +493,7 @@
{
<div class="modal show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content animate-scale-in">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-plus-circle"></i> Aggiungi Nuova Asta</h5>
<button type="button" class="btn-close" @onclick="CloseAddDialog"></button>
@@ -579,29 +501,97 @@
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-bold"><i class="bi bi-link-45deg"></i> URL Asta:</label>
<input type="text" class="form-control transition-colors @(addDialogError != null ? "is-invalid" : "")"
<input type="text" class="form-control @(addDialogError != null ? "is-invalid" : "")"
@bind="addDialogUrl"
placeholder="https://it.bidoo.com/asta/..." />
@if (addDialogError != null)
{
<div class="invalid-feedback d-block animate-shake">
<div class="invalid-feedback d-block">
<i class="bi bi-exclamation-triangle"></i> @addDialogError
</div>
}
<small class="form-text text-muted">
<i class="bi bi-info-circle"></i> Inserisci l'URL completo dell'asta da Bidoo.com. Il nome sarà rilevato automaticamente.
</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary hover-lift" @onclick="CloseAddDialog">
<i class="bi bi-x-circle"></i> Annulla
</button>
<button type="button" class="btn btn-primary hover-lift" @onclick="AddAuction" disabled="@string.IsNullOrWhiteSpace(addDialogUrl)">
<i class="bi bi-plus-lg"></i> Aggiungi
</button>
<button type="button" class="btn btn-secondary" @onclick="CloseAddDialog">Annulla</button>
<button type="button" class="btn btn-primary" @onclick="AddAuction" disabled="@string.IsNullOrWhiteSpace(addDialogUrl)">Aggiungi</button>
</div>
</div>
</div>
</div>
}
@* Script Splitter *@
<script suppress-error="BL9992">
(function() {
function initSplitters() {
const gutterV = document.getElementById('gutterVertical');
const gutterH = document.getElementById('gutterHorizontal');
const panelAuctions = document.getElementById('panelAuctions');
const panelLog = document.getElementById('panelLog');
const topRow = document.getElementById('topRow');
const bottomRow = document.getElementById('bottomRow');
if (!gutterV || !gutterH || !panelAuctions || !panelLog || !topRow || !bottomRow) {
setTimeout(initSplitters, 200);
return;
}
let active = null;
let startPos = 0;
let startSizeA = 0;
let startSizeB = 0;
function onMouseDown(e, type, elA, elB) {
active = { type, elA, elB };
startPos = type === 'v' ? e.clientX : e.clientY;
startSizeA = type === 'v' ? elA.offsetWidth : elA.offsetHeight;
startSizeB = type === 'v' ? elB.offsetWidth : elB.offsetHeight;
document.body.style.cursor = type === 'v' ? 'col-resize' : 'row-resize';
document.body.style.userSelect = 'none';
e.preventDefault();
}
gutterV.onmousedown = (e) => onMouseDown(e, 'v', panelAuctions, panelLog);
gutterH.onmousedown = (e) => onMouseDown(e, 'h', topRow, bottomRow);
document.onmousemove = function(e) {
if (!active) return;
const { type, elA, elB } = active;
const pos = type === 'v' ? e.clientX : e.clientY;
const diff = pos - startPos;
const total = startSizeA + startSizeB;
let newA = startSizeA + diff;
let newB = startSizeB - diff;
const minA = type === 'v' ? 200 : 150;
const minB = type === 'v' ? 150 : 80;
if (newA < minA) { newA = minA; newB = total - newA; }
if (newB < minB) { newB = minB; newA = total - newB; }
if (type === 'v') {
elA.style.width = newA + 'px';
elA.style.flex = 'none';
elB.style.width = newB + 'px';
elB.style.flex = 'none';
} else {
elA.style.height = newA + 'px';
elA.style.flex = 'none';
elB.style.height = newB + 'px';
elB.style.flex = 'none';
}
};
document.onmouseup = function() {
if (active) {
active = null;
document.body.style.cursor = '';
document.body.style.userSelect = '';
}
};
}
setTimeout(initSplitters, 300);
})();
</script>

View File

@@ -701,6 +701,94 @@ namespace AutoBidder.Pages
return auctions.Any(a => !a.IsActive);
}
// ???????????????????????????????????????????????????????????????????
// RIMOZIONE ASTE PER STATO
// ???????????????????????????????????????????????????????????????????
private async Task RemoveActiveAuctions()
{
await RemoveAuctionsByCondition(
a => a.IsActive && !a.IsPaused && (a.LastState == null || a.LastState.Status == AuctionStatus.Running),
"attive",
GetActiveAuctionsCount()
);
}
private async Task RemovePausedAuctions()
{
await RemoveAuctionsByCondition(
a => a.IsPaused || (a.LastState != null && a.LastState.Status == AuctionStatus.Paused),
"in pausa",
GetPausedAuctionsCount()
);
}
private async Task RemoveStoppedAuctions()
{
await RemoveAuctionsByCondition(
a => !a.IsActive && (a.LastState == null || (a.LastState.Status != AuctionStatus.EndedWon && a.LastState.Status != AuctionStatus.EndedLost)),
"fermate",
GetStoppedAuctionsCount()
);
}
private async Task RemoveWonAuctions()
{
await RemoveAuctionsByCondition(
a => a.LastState != null && a.LastState.Status == AuctionStatus.EndedWon,
"vinte",
GetWonAuctionsCount()
);
}
private async Task RemoveLostAuctions()
{
await RemoveAuctionsByCondition(
a => a.LastState != null && a.LastState.Status == AuctionStatus.EndedLost,
"perse",
GetLostAuctionsCount()
);
}
private async Task RemoveAuctionsByCondition(Func<AuctionInfo, bool> condition, string stateLabel, int count)
{
if (count == 0) return;
var confirmed = await JSRuntime.InvokeAsync<bool>("confirm",
$"Rimuovere {count} aste {stateLabel}?");
if (!confirmed) return;
try
{
var toRemove = auctions.Where(condition).ToList();
int removed = 0;
foreach (var auction in toRemove)
{
AuctionMonitor.RemoveAuction(auction.AuctionId);
AppState.RemoveAuction(auction);
removed++;
}
if (selectedAuction != null && condition(selectedAuction))
{
selectedAuction = null;
}
SaveAuctions();
AddLog($"[CLEANUP] Rimosse {removed} aste {stateLabel}");
}
catch (Exception ex)
{
AddLog($"Errore rimozione: {ex.Message}");
}
}
// ???????????????????????????????????????????????????????????????????
// SPLITTER RESIZE (gestito via JS)
// ???????????????????????????????????????????????????????????????????
private async Task RemoveSelectedAuctionWithConfirm()
{
if (selectedAuction == null) return;
@@ -1150,11 +1238,6 @@ namespace AutoBidder.Pages
var latency = auction.PollingLatencyMs;
if (latency <= 0) return "-";
// Colora in base al ping
var cssClass = latency < 100 ? "text-success" :
latency < 300 ? "text-warning" :
"text-danger";
return $"{latency}ms";
}
catch
@@ -1163,6 +1246,25 @@ namespace AutoBidder.Pages
}
}
private string GetPingClass(AuctionInfo? auction)
{
try
{
if (auction == null) return "text-muted";
var latency = auction.PollingLatencyMs;
if (latency <= 0) return "text-muted";
if (latency < 100) return "text-success";
if (latency < 300) return "text-warning";
return "text-danger";
}
catch
{
return "text-muted";
}
}
private IEnumerable<string> GetAuctionLog(AuctionInfo auction)
{
return auction.AuctionLog.TakeLast(50);
@@ -1366,5 +1468,41 @@ namespace AutoBidder.Pages
StateHasChanged();
}
}
// ???????????????????????????????????????????????????????????????????
// METODI CONTEGGIO STATO ASTE
// ???????????????????????????????????????????????????????????????????
private int GetActiveAuctionsCount()
{
return auctions.Count(a => a.IsActive && !a.IsPaused &&
(a.LastState == null || a.LastState.Status == AuctionStatus.Running));
}
private int GetPausedAuctionsCount()
{
return auctions.Count(a => a.IsPaused ||
(a.LastState != null && a.LastState.Status == AuctionStatus.Paused));
}
private int GetWonAuctionsCount()
{
return auctions.Count(a => a.LastState != null &&
a.LastState.Status == AuctionStatus.EndedWon);
}
private int GetLostAuctionsCount()
{
return auctions.Count(a => a.LastState != null &&
a.LastState.Status == AuctionStatus.EndedLost);
}
private int GetStoppedAuctionsCount()
{
return auctions.Count(a => !a.IsActive &&
(a.LastState == null ||
(a.LastState.Status != AuctionStatus.EndedWon &&
a.LastState.Status != AuctionStatus.EndedLost)));
}
}
}

View File

@@ -133,38 +133,93 @@
</h2>
<div id="collapse-defaults" class="accordion-collapse collapse" aria-labelledby="heading-defaults" data-bs-parent="#settingsAccordion">
<div class="accordion-body">
<div class="alert alert-info border-0 shadow-sm mb-3">
<i class="bi bi-info-circle me-2"></i>
Usa i pulsanti <i class="bi bi-arrow-repeat"></i> per applicare la singola impostazione a tutte le aste
</div>
<div class="row g-3">
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-speedometer2"></i> Anticipo puntata (ms)</label>
<div class="input-group">
<input type="number" class="form-control" @bind="settings.DefaultBidBeforeDeadlineMs" />
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultBidBeforeDeadlineMs))"
disabled="@applyingSettings.Contains(nameof(settings.DefaultBidBeforeDeadlineMs))"
title="Applica a tutte le aste">
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-hand-index-thumb"></i> Click massimi</label>
<div class="input-group">
<input type="number" class="form-control" @bind="settings.DefaultMaxClicks" />
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMaxClicks))"
disabled="@applyingSettings.Contains(nameof(settings.DefaultMaxClicks))"
title="Applica a tutte le aste">
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
<div class="form-text">0 = illimitati</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-currency-euro"></i> Prezzo minimo (€)</label>
<div class="input-group">
<input type="number" step="0.01" class="form-control" @bind="settings.DefaultMinPrice" />
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMinPrice))"
disabled="@applyingSettings.Contains(nameof(settings.DefaultMinPrice))"
title="Applica a tutte le aste">
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-currency-euro"></i> Prezzo massimo (€)</label>
<div class="input-group">
<input type="number" step="0.01" class="form-control" @bind="settings.DefaultMaxPrice" />
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMaxPrice))"
disabled="@applyingSettings.Contains(nameof(settings.DefaultMaxPrice))"
title="Applica a tutte le aste">
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-arrow-repeat"></i> Reset minimi</label>
<div class="input-group">
<input type="number" class="form-control" @bind="settings.DefaultMinResets" />
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMinResets))"
disabled="@applyingSettings.Contains(nameof(settings.DefaultMinResets))"
title="Applica a tutte le aste">
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-arrow-repeat"></i> Reset massimi</label>
<div class="input-group">
<input type="number" class="form-control" @bind="settings.DefaultMaxResets" />
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMaxResets))"
disabled="@applyingSettings.Contains(nameof(settings.DefaultMaxResets))"
title="Applica a tutte le aste">
<i class="bi bi-arrow-repeat"></i>
</button>
</div>
</div>
<div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-shield-check"></i> Puntate minime da mantenere</label>
<input type="number" class="form-control" @bind="settings.MinimumRemainingBids" />
<div class="form-text">Questa è un'impostazione globale</div>
</div>
</div>
@if (!string.IsNullOrEmpty(singleSettingMessage))
{
<div class="alert alert-success mt-3 mb-0 fade-in">
<i class="bi bi-check-circle me-2"></i>@singleSettingMessage
</div>
}
<div class="mt-3">
<button class="btn btn-success" @onclick="SaveSettings"><i class="bi bi-check-lg"></i> Salva</button>
</div>
@@ -689,6 +744,21 @@
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.925rem;
}
.fade-in {
animation: fadeIn 0.3s ease-in;
}
@@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
@code {
@@ -706,6 +776,10 @@ private bool isApplyingToAll = false;
private string? applyToAllMessage = null;
private bool applyToAllSuccess = false;
// Applica singole impostazioni
private HashSet<string> applyingSettings = new();
private string? singleSettingMessage = null;
private AutoBidder.Utilities.AppSettings settings = new();
private System.Threading.Timer? updateTimer;
@@ -781,6 +855,75 @@ private System.Threading.Timer? updateTimer;
}
}
private async Task ApplySingleSettingToAll(string settingName)
{
applyingSettings.Add(settingName);
singleSettingMessage = null;
StateHasChanged();
try
{
// Prima salva le impostazioni
SaveSettings();
var auctions = AuctionMonitor.GetAuctions().ToList();
int count = 0;
string settingLabel = "";
foreach (var auction in auctions)
{
switch (settingName)
{
case nameof(settings.DefaultBidBeforeDeadlineMs):
auction.BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs;
settingLabel = $"Anticipo puntata ({settings.DefaultBidBeforeDeadlineMs}ms)";
break;
case nameof(settings.DefaultMaxClicks):
// MaxClicks viene applicato tramite MaxBidsOverride
auction.MaxBidsOverride = settings.DefaultMaxClicks > 0 ? settings.DefaultMaxClicks : null;
settingLabel = $"Click massimi ({(settings.DefaultMaxClicks > 0 ? settings.DefaultMaxClicks.ToString() : "illimitati")})";
break;
case nameof(settings.DefaultMinPrice):
auction.MinPrice = settings.DefaultMinPrice;
settingLabel = $"Prezzo minimo (€{settings.DefaultMinPrice:F2})";
break;
case nameof(settings.DefaultMaxPrice):
auction.MaxPrice = settings.DefaultMaxPrice;
settingLabel = $"Prezzo massimo (€{settings.DefaultMaxPrice:F2})";
break;
case nameof(settings.DefaultMinResets):
auction.MinResets = settings.DefaultMinResets;
settingLabel = $"Reset minimi ({settings.DefaultMinResets})";
break;
case nameof(settings.DefaultMaxResets):
auction.MaxResets = settings.DefaultMaxResets;
settingLabel = $"Reset massimi ({settings.DefaultMaxResets})";
break;
}
count++;
}
// Salva le aste modificate
AutoBidder.Utilities.PersistenceManager.SaveAuctions(auctions);
singleSettingMessage = $"? {settingLabel} applicato a {count} aste!";
}
catch (Exception ex)
{
singleSettingMessage = $"Errore: {ex.Message}";
}
finally
{
applyingSettings.Remove(settingName);
StateHasChanged();
// Rimuovi messaggio dopo 3 secondi
await Task.Delay(3000);
singleSettingMessage = null;
StateHasChanged();
}
}
private void SyncStartupSelectionsFromSettings()
{
if (settings.RememberAuctionStates)

View File

@@ -143,7 +143,7 @@
<tbody>
@foreach (var auction in filteredAuctions)
{
<tr class="@(auction.Won ? "table-success-subtle" : "") auction-row"
<tr class="@(auction.Won ? "table-success-subtle" : "") @(selectedAuctionDetail?.Id == auction.Id ? "table-info" : "") auction-row"
@onclick="() => SelectAuction(auction)">
<td>
<small class="fw-bold">@TruncateName(auction.AuctionName, 30)</small>
@@ -200,15 +200,47 @@
<div class="card-header bg-success text-white">
<h5 class="mb-0">
<i class="bi bi-box-seam me-2"></i>
Prodotti Salvati
Prodotti Salvati (@(filteredProducts?.Count ?? 0))
</h5>
</div>
<!-- FILTRO PRODOTTI -->
<div class="card-body border-bottom py-2">
<div class="row g-2 align-items-center">
<div class="col-md-8">
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" class="form-control" placeholder="Cerca prodotto..."
@bind="filterProductName" @bind:event="oninput" @onkeyup="ApplyProductFilter" />
@if (!string.IsNullOrEmpty(filterProductName))
{
<button class="btn btn-outline-secondary" @onclick="ClearProductFilter">
<i class="bi bi-x"></i>
</button>
}
</div>
</div>
<div class="col-md-4 text-muted small d-flex align-items-center">
<i class="bi bi-info-circle me-1"></i> Clicca intestazioni per ordinare
</div>
</div>
</div>
<div class="card-body p-0">
@if (products == null || !products.Any())
@if (filteredProducts == null || !filteredProducts.Any())
{
<div class="text-center py-5 text-muted">
<i class="bi bi-inbox" style="font-size: 3rem;"></i>
<p class="mt-3">Nessun prodotto salvato</p>
<p class="mt-3">
@if (!string.IsNullOrEmpty(filterProductName))
{
<span>Nessun prodotto trovato per "@filterProductName"</span>
}
else
{
<span>Nessun prodotto salvato</span>
}
</p>
</div>
}
else
@@ -217,25 +249,47 @@
<table class="table table-hover table-sm mb-0">
<thead class="table-light sticky-top">
<tr>
<th>Prodotto</th>
<th class="text-center">Aste</th>
<th class="text-center">Win%</th>
<th class="text-end">Limiti €</th>
<th class="sortable-header" @onclick='() => SortProductsBy("name")'>
Prodotto @GetProductSortIndicator("name")
</th>
<th class="text-center sortable-header" @onclick='() => SortProductsBy("auctions")'>
Aste @GetProductSortIndicator("auctions")
</th>
<th class="text-center sortable-header" @onclick='() => SortProductsBy("winrate")'>
Win% @GetProductSortIndicator("winrate")
</th>
<th class="text-end sortable-header" @onclick='() => SortProductsBy("avgprice")'>
Prezzo Medio @GetProductSortIndicator("avgprice")
</th>
<th class="text-end">Range Storico</th>
<th class="text-end">Limiti Consigliati</th>
<th class="text-center">Azioni</th>
</tr>
</thead>
<tbody>
@foreach (var product in products)
@foreach (var product in filteredProducts)
{
var winRate = product.TotalAuctions > 0
? (product.WonAuctions * 100.0 / product.TotalAuctions)
: 0;
var isEditing = editingProductKey == product.ProductKey;
<tr>
<tr class="product-row @(selectedProduct?.ProductKey == product.ProductKey ? "table-info" : "")"
@onclick="() => SelectProduct(product)">
<td>
<div class="d-flex align-items-center gap-2">
<button class="btn btn-sm btn-outline-secondary"
@onclick="() => ToggleEditProduct(product)"
@onclick:stopPropagation="true"
title="@(isEditing ? "Chiudi editor" : "Modifica limiti default")">
<i class="bi bi-@(isEditing ? "chevron-up" : "chevron-down")"></i>
</button>
<div>
<small class="fw-bold">@product.ProductName</small>
<br/>
<small class="text-muted">@product.TotalAuctions totali (@product.WonAuctions vinte)</small>
</div>
</div>
</td>
<td class="text-center fw-bold">
@product.TotalAuctions
@@ -246,10 +300,13 @@
</span>
</td>
<td class="text-end">
@if (product.RecommendedMinPrice.HasValue && product.RecommendedMaxPrice.HasValue)
<span class="fw-bold text-primary">€@product.AvgFinalPrice.ToString("F2")</span>
</td>
<td class="text-end">
@if (product.MinFinalPrice.HasValue && product.MaxFinalPrice.HasValue)
{
<small class="text-muted">
@product.RecommendedMinPrice.Value.ToString("F2") - @product.RecommendedMaxPrice.Value.ToString("F2")
@product.MinFinalPrice.Value.ToString("F2") - @product.MaxFinalPrice.Value.ToString("F2")
</small>
}
else
@@ -257,21 +314,130 @@
<small class="text-muted">-</small>
}
</td>
<td class="text-center">
<td class="text-end">
@if (product.RecommendedMinPrice.HasValue && product.RecommendedMaxPrice.HasValue)
{
<button class="btn btn-sm btn-primary"
@onclick="() => ApplyLimitsToProduct(product)"
title="Applica limiti a tutte le aste di questo prodotto">
<i class="bi bi-check2-circle"></i> Applica
</button>
<small class="text-success fw-bold">
€@product.RecommendedMinPrice.Value.ToString("F2") - €@product.RecommendedMaxPrice.Value.ToString("F2")
</small>
}
else
{
<small class="text-muted">N/D</small>
}
</td>
<td class="text-center" @onclick:stopPropagation="true">
<div class="d-flex gap-1 justify-content-center">
@if (product.RecommendedMinPrice.HasValue && product.RecommendedMaxPrice.HasValue)
{
<button class="btn btn-sm btn-primary"
@onclick="() => ApplyLimitsToProduct(product)"
title="Applica limiti a tutte le aste di questo prodotto">
<i class="bi bi-check2-circle"></i>
</button>
}
<button class="btn btn-sm btn-danger"
@onclick="() => DeleteProduct(product)"
title="Elimina questo prodotto dalle statistiche"
disabled="@isDeletingProduct">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
<!-- RIGA ESPANDIBILE PER EDITING LIMITI DEFAULT -->
@if (isEditing)
{
<tr class="table-light">
<td colspan="7" @onclick:stopPropagation="true">
<div class="p-3 border rounded bg-white">
<h6 class="text-primary mb-3">
<i class="bi bi-sliders me-2"></i>
Limiti Default per: @product.ProductName
</h6>
<div class="row g-3">
<!-- Colonna 1: Prezzi -->
<div class="col-md-4">
<div class="mb-2">
<label class="form-label small fw-bold">Prezzo Minimo €</label>
<input type="number" step="0.01" class="form-control form-control-sm"
@bind="tempUserDefaultMinPrice" placeholder="es. @product.RecommendedMinPrice?.ToString("F2")" />
</div>
<div class="mb-2">
<label class="form-label small fw-bold">Prezzo Massimo €</label>
<input type="number" step="0.01" class="form-control form-control-sm"
@bind="tempUserDefaultMaxPrice" placeholder="es. @product.RecommendedMaxPrice?.ToString("F2")" />
</div>
</div>
<!-- Colonna 2: Reset e Puntate -->
<div class="col-md-4">
<div class="mb-2">
<label class="form-label small fw-bold">Reset Minimo</label>
<input type="number" class="form-control form-control-sm"
@bind="tempUserDefaultMinResets" placeholder="es. @product.RecommendedMinResets" />
</div>
<div class="mb-2">
<label class="form-label small fw-bold">Reset Massimo</label>
<input type="number" class="form-control form-control-sm"
@bind="tempUserDefaultMaxResets" placeholder="es. @product.RecommendedMaxResets" />
</div>
<div class="mb-2">
<label class="form-label small fw-bold">Max Puntate</label>
<input type="number" class="form-control form-control-sm"
@bind="tempUserDefaultMaxBids" placeholder="es. @product.RecommendedMaxBids" />
</div>
</div>
<!-- Colonna 3: Timing -->
<div class="col-md-4">
<div class="mb-2">
<label class="form-label small fw-bold">Anticipo Puntata (ms)</label>
<input type="number" class="form-control form-control-sm"
@bind="tempUserDefaultBidDeadline" placeholder="es. 200" />
</div>
<div class="alert alert-info p-2 mb-2 small">
<i class="bi bi-info-circle me-1"></i>
Valori consigliati dall'algoritmo:<br/>
<strong>€@product.RecommendedMinPrice?.ToString("F2") - €@product.RecommendedMaxPrice?.ToString("F2")</strong><br/>
Reset: @product.RecommendedMinResets - @product.RecommendedMaxResets
</div>
</div>
</div>
<!-- Pulsanti Azioni -->
<div class="d-flex gap-2 justify-content-end mt-3">
<button class="btn btn-sm btn-secondary"
@onclick="() => CopyRecommendedToTemp(product)"
disabled="@(!product.RecommendedMinPrice.HasValue)"
title="Copia i valori consigliati dall'algoritmo">
<i class="bi bi-arrow-down-circle me-1"></i>
Usa Consigliati
</button>
<button class="btn btn-sm btn-success"
@onclick="() => SaveProductDefaults(product)"
disabled="@isSavingDefaults">
<i class="bi bi-floppy me-1"></i>
Salva Default
</button>
<button class="btn btn-sm btn-primary"
@onclick="() => ApplyDefaultsToAllAuctions(product)"
disabled="@(!HasUserDefaults(product) || isSavingDefaults)">
<i class="bi bi-check2-all me-1"></i>
Applica a Tutte le Aste
</button>
<button class="btn btn-sm btn-outline-secondary"
@onclick="() => CancelEditProduct()">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
</td>
</tr>
}
}
</tbody>
</table>
@@ -421,20 +587,202 @@
</div>
</div>
}
<!-- PANNELLO ASTE DEL PRODOTTO SELEZIONATO -->
@if (selectedProduct != null)
{
<div class="row mt-4">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="bi bi-box-seam me-2"></i>
Aste di: @selectedProduct.ProductName
<span class="badge bg-light text-dark ms-2">@(selectedProductAuctions?.Count ?? 0) aste</span>
</h5>
<button class="btn btn-sm btn-outline-light" @onclick="() => { selectedProduct = null; selectedProductAuctions = null; }">
<i class="bi bi-x-lg"></i>
</button>
</div>
@if (isLoadingProductAuctions)
{
<div class="card-body text-center py-4">
<div class="spinner-border text-success" role="status">
<span class="visually-hidden">Caricamento...</span>
</div>
<p class="text-muted mt-2">Caricamento aste...</p>
</div>
}
else if (selectedProductAuctions == null || !selectedProductAuctions.Any())
{
<div class="card-body text-center py-4 text-muted">
<i class="bi bi-inbox" style="font-size: 2rem;"></i>
<p class="mt-2">Nessuna asta trovata per questo prodotto</p>
</div>
}
else
{
<div class="card-body p-0">
<div class="table-responsive" style="max-height: 500px; overflow-y: auto;">
<table class="table table-hover table-sm mb-0">
<thead class="table-light sticky-top">
<tr>
<th>ID Asta</th>
<th class="text-end">Prezzo Finale</th>
<th class="text-center">Stato</th>
<th>Vincitore</th>
<th class="text-end">Le Mie Puntate</th>
<th class="text-end">Puntate Vincitore</th>
<th class="text-center">Reset</th>
<th class="text-center">Ora Chiusura</th>
<th>Data</th>
<th class="text-end">Risparmio</th>
</tr>
</thead>
<tbody>
@foreach (var auction in selectedProductAuctions)
{
<tr class="@(auction.Won ? "table-success-subtle" : "")">
<td><small class="font-monospace">@auction.AuctionId</small></td>
<td class="text-end fw-bold">€@auction.FinalPrice.ToString("F2")</td>
<td class="text-center">
@if (auction.Won)
{
<span class="badge bg-success">? Vinta</span>
}
else
{
<span class="badge bg-secondary">? Persa</span>
}
</td>
<td><small>@(auction.WinnerUsername ?? "-")</small></td>
<td class="text-end">
<span class="badge bg-primary">@auction.BidsUsed</span>
</td>
<td class="text-end">
@if (auction.WinnerBidsUsed.HasValue)
{
<span class="badge bg-info text-dark">@auction.WinnerBidsUsed</span>
}
else
{
<span class="text-muted">-</span>
}
</td>
<td class="text-center">
@if (auction.TotalResets.HasValue)
{
<span class="badge @GetResetBadgeClass(auction.TotalResets.Value)">
@auction.TotalResets
</span>
}
else
{
<span class="text-muted">-</span>
}
</td>
<td class="text-center">
@if (auction.ClosedAtHour.HasValue)
{
<small>@auction.ClosedAtHour:00</small>
}
else
{
<small class="text-muted">-</small>
}
</td>
<td><small class="text-muted">@FormatTimestamp(auction.Timestamp)</small></td>
<td class="text-end">
@if (auction.Savings.HasValue)
{
<small class="@(auction.Savings.Value > 0 ? "text-success" : "text-danger") fw-bold">
@(auction.Savings.Value > 0 ? "+" : "")€@auction.Savings.Value.ToString("F2")
</small>
}
else
{
<small class="text-muted">-</small>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<!-- Riepilogo Statistiche Prodotto -->
<div class="card-footer bg-light">
<div class="row g-3 text-center">
<div class="col-6 col-md-3">
<small class="text-muted d-block">Prezzo Medio</small>
<strong class="text-primary">€@selectedProduct.AvgFinalPrice.ToString("F2")</strong>
</div>
<div class="col-6 col-md-3">
<small class="text-muted d-block">Range Prezzi</small>
<strong>
@if (selectedProduct.MinFinalPrice.HasValue && selectedProduct.MaxFinalPrice.HasValue)
{
<span>€@selectedProduct.MinFinalPrice.Value.ToString("F2") - €@selectedProduct.MaxFinalPrice.Value.ToString("F2")</span>
}
else
{
<span class="text-muted">-</span>
}
</strong>
</div>
<div class="col-6 col-md-3">
<small class="text-muted d-block">Media Puntate Vincita</small>
<strong class="text-info">@selectedProduct.AvgBidsToWin.ToString("F1")</strong>
</div>
<div class="col-6 col-md-3">
<small class="text-muted d-block">Media Reset</small>
<strong class="text-warning">@selectedProduct.AvgResets.ToString("F1")</strong>
</div>
</div>
</div>
}
</div>
</div>
</div>
}
}
</div>
@code {
private bool isLoading = true;
private bool isDeletingProduct = false;
private List<AuctionResultExtended>? recentAuctions;
private List<AuctionResultExtended>? filteredAuctions;
private List<ProductStatisticsRecord>? products;
private List<ProductStatisticsRecord>? filteredProducts;
// Filtri e ordinamento
// Filtri e ordinamento aste
private string filterName = "";
private string filterWon = "";
private AuctionResultExtended? selectedAuctionDetail;
// Filtri e ordinamento prodotti
private string filterProductName = "";
private string productSortColumn = "name";
private bool productSortDescending = false;
// Prodotto selezionato e sue aste
private ProductStatisticsRecord? selectedProduct = null;
private List<AuctionResultExtended>? selectedProductAuctions = null;
private bool isLoadingProductAuctions = false;
// Editing limiti default prodotto
private string? editingProductKey = null;
private bool isSavingDefaults = false;
private double? tempUserDefaultMinPrice;
private double? tempUserDefaultMaxPrice;
private int? tempUserDefaultMinResets;
private int? tempUserDefaultMaxResets;
private int? tempUserDefaultMaxBids;
private int? tempUserDefaultBidDeadline;
protected override async Task OnInitializedAsync()
{
await RefreshStats();
@@ -453,6 +801,7 @@ private AuctionResultExtended? selectedAuctionDetail;
// Carica prodotti con statistiche
products = await DatabaseService.GetAllProductStatisticsAsync();
ApplyProductFilter();
}
catch (Exception ex)
{
@@ -548,6 +897,30 @@ private AuctionResultExtended? selectedAuctionDetail;
selectedAuctionDetail = auction;
}
private async Task SelectProduct(ProductStatisticsRecord product)
{
selectedProduct = product;
selectedProductAuctions = null;
isLoadingProductAuctions = true;
StateHasChanged();
try
{
// Carica tutte le aste per questo prodotto
selectedProductAuctions = await DatabaseService.GetAuctionResultsByProductAsync(product.ProductKey, 1000);
}
catch (Exception ex)
{
Console.WriteLine($"[Statistics] Error loading product auctions: {ex.Message}");
await JSRuntime.InvokeVoidAsync("alert", $"Errore caricamento aste: {ex.Message}");
}
finally
{
isLoadingProductAuctions = false;
StateHasChanged();
}
}
private string GetHeatBadgeClass(int heat)
{
if (heat < 30) return "bg-success";
@@ -555,6 +928,13 @@ private AuctionResultExtended? selectedAuctionDetail;
return "bg-danger";
}
private string GetResetBadgeClass(int resets)
{
if (resets < 10) return "bg-success";
if (resets < 30) return "bg-warning text-dark";
return "bg-danger";
}
private string TruncateName(string name, int maxLength)
{
if (string.IsNullOrEmpty(name)) return "-";
@@ -610,6 +990,285 @@ private AuctionResultExtended? selectedAuctionDetail;
await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
}
}
private async Task DeleteProduct(ProductStatisticsRecord product)
{
try
{
// Conferma eliminazione
var confirmed = await JSRuntime.InvokeAsync<bool>("confirm",
$"Eliminare il prodotto '{product.ProductName}'?\n\n" +
$"Questo rimuoverà le statistiche di {product.TotalAuctions} aste.\n" +
$"L'operazione NON può essere annullata!");
if (!confirmed)
return;
isDeletingProduct = true;
StateHasChanged();
// Elimina dal database
var deleted = await DatabaseService.DeleteProductStatisticsAsync(product.ProductKey);
if (deleted > 0)
{
// Rimuovi dalla lista locale
products?.Remove(product);
ApplyProductFilter();
await JSRuntime.InvokeVoidAsync("alert",
$"? Prodotto '{product.ProductName}' eliminato con successo!\n" +
$"Rimosse {deleted} righe dal database.");
}
else
{
await JSRuntime.InvokeVoidAsync("alert",
"Nessuna riga eliminata. Il prodotto potrebbe essere già stato rimosso.");
}
}
catch (Exception ex)
{
await JSRuntime.InvokeVoidAsync("alert", $"Errore durante eliminazione: {ex.Message}");
}
finally
{
isDeletingProduct = false;
StateHasChanged();
}
}
// ???????????????????????????????????????????????????????????????
// METODI FILTRO E ORDINAMENTO PRODOTTI
// ???????????????????????????????????????????????????????????????
private void ApplyProductFilter()
{
if (products == null)
{
filteredProducts = null;
return;
}
var filtered = products.AsEnumerable();
// Filtro per nome
if (!string.IsNullOrWhiteSpace(filterProductName))
{
filtered = filtered.Where(p =>
p.ProductName.Contains(filterProductName, StringComparison.OrdinalIgnoreCase));
}
// Ordinamento
filtered = productSortColumn switch
{
"name" => productSortDescending
? filtered.OrderByDescending(p => p.ProductName)
: filtered.OrderBy(p => p.ProductName),
"auctions" => productSortDescending
? filtered.OrderByDescending(p => p.TotalAuctions)
: filtered.OrderBy(p => p.TotalAuctions),
"winrate" => productSortDescending
? filtered.OrderByDescending(p => p.TotalAuctions > 0 ? (p.WonAuctions * 100.0 / p.TotalAuctions) : 0)
: filtered.OrderBy(p => p.TotalAuctions > 0 ? (p.WonAuctions * 100.0 / p.TotalAuctions) : 0),
"avgprice" => productSortDescending
? filtered.OrderByDescending(p => p.AvgFinalPrice)
: filtered.OrderBy(p => p.AvgFinalPrice),
_ => filtered.OrderBy(p => p.ProductName) // Default alfabetico ascendente
};
filteredProducts = filtered.ToList();
}
private void ClearProductFilter()
{
filterProductName = "";
ApplyProductFilter();
}
private void SortProductsBy(string column)
{
if (productSortColumn == column)
{
// Toggle direzione se stessa colonna
productSortDescending = !productSortDescending;
}
else
{
// Nuova colonna
productSortColumn = column;
// Default: nome alfabetico ascendente, resto discendente
productSortDescending = column != "name";
}
ApplyProductFilter();
}
private MarkupString GetProductSortIndicator(string column)
{
if (productSortColumn != column)
return new MarkupString("<i class=\"bi bi-chevron-expand text-muted\" style=\"font-size: 0.7rem;\"></i>");
return productSortDescending
? new MarkupString("<i class=\"bi bi-chevron-down\"></i>")
: new MarkupString("<i class=\"bi bi-chevron-up\"></i>");
}
// ???????????????????????????????????????????????????????????????????
// METODI EDITING LIMITI DEFAULT PRODOTTO
// ???????????????????????????????????????????????????????????????????
private void ToggleEditProduct(ProductStatisticsRecord product)
{
if (editingProductKey == product.ProductKey)
{
// Chiudi editor
CancelEditProduct();
}
else
{
// Apri editor
editingProductKey = product.ProductKey;
LoadCurrentDefaults(product);
}
}
private void LoadCurrentDefaults(ProductStatisticsRecord product)
{
tempUserDefaultMinPrice = product.UserDefaultMinPrice;
tempUserDefaultMaxPrice = product.UserDefaultMaxPrice;
tempUserDefaultMinResets = product.UserDefaultMinResets;
tempUserDefaultMaxResets = product.UserDefaultMaxResets;
tempUserDefaultMaxBids = product.UserDefaultMaxBids;
tempUserDefaultBidDeadline = product.UserDefaultBidBeforeDeadlineMs;
}
private void CopyRecommendedToTemp(ProductStatisticsRecord product)
{
tempUserDefaultMinPrice = product.RecommendedMinPrice;
tempUserDefaultMaxPrice = product.RecommendedMaxPrice;
tempUserDefaultMinResets = product.RecommendedMinResets;
tempUserDefaultMaxResets = product.RecommendedMaxResets;
tempUserDefaultMaxBids = product.RecommendedMaxBids;
// Bid deadline rimane quello dell'utente o default 200ms
if (!tempUserDefaultBidDeadline.HasValue)
tempUserDefaultBidDeadline = 200;
StateHasChanged();
}
private void CancelEditProduct()
{
editingProductKey = null;
tempUserDefaultMinPrice = null;
tempUserDefaultMaxPrice = null;
tempUserDefaultMinResets = null;
tempUserDefaultMaxResets = null;
tempUserDefaultMaxBids = null;
tempUserDefaultBidDeadline = null;
}
private async Task SaveProductDefaults(ProductStatisticsRecord product)
{
try
{
isSavingDefaults = true;
StateHasChanged();
// Aggiorna nel database
await DatabaseService.UpdateProductUserDefaultsAsync(
product.ProductKey,
tempUserDefaultMinPrice,
tempUserDefaultMaxPrice,
tempUserDefaultMinResets,
tempUserDefaultMaxResets,
tempUserDefaultMaxBids,
tempUserDefaultBidDeadline
);
// Aggiorna l'oggetto locale
product.UserDefaultMinPrice = tempUserDefaultMinPrice;
product.UserDefaultMaxPrice = tempUserDefaultMaxPrice;
product.UserDefaultMinResets = tempUserDefaultMinResets;
product.UserDefaultMaxResets = tempUserDefaultMaxResets;
product.UserDefaultMaxBids = tempUserDefaultMaxBids;
product.UserDefaultBidBeforeDeadlineMs = tempUserDefaultBidDeadline;
await JSRuntime.InvokeVoidAsync("alert",
$"? Limiti default salvati per '{product.ProductName}'!\n\n" +
$"Min: €{tempUserDefaultMinPrice:F2} - Max: €{tempUserDefaultMaxPrice:F2}\n" +
$"Reset: {tempUserDefaultMinResets} - {tempUserDefaultMaxResets}\n" +
$"Max Puntate: {tempUserDefaultMaxBids}\n" +
$"Anticipo: {tempUserDefaultBidDeadline}ms");
CancelEditProduct();
}
catch (Exception ex)
{
await JSRuntime.InvokeVoidAsync("alert", $"Errore salvataggio: {ex.Message}");
}
finally
{
isSavingDefaults = false;
StateHasChanged();
}
}
private async Task ApplyDefaultsToAllAuctions(ProductStatisticsRecord product)
{
try
{
var matchingAuctions = AppState.Auctions
.Where(a => ProductStatisticsService.GenerateProductKey(a.Name) == product.ProductKey)
.ToList();
if (!matchingAuctions.Any())
{
await JSRuntime.InvokeVoidAsync("alert", $"Nessuna asta trovata per '{product.ProductName}'");
return;
}
var confirmed = await JSRuntime.InvokeAsync<bool>("confirm",
$"Applicare i limiti default a {matchingAuctions.Count} aste di '{product.ProductName}'?\n\n" +
$"Min: €{product.UserDefaultMinPrice:F2} - Max: €{product.UserDefaultMaxPrice:F2}\n" +
$"Reset: {product.UserDefaultMinResets} - {product.UserDefaultMaxResets}\n" +
$"Max Puntate: {product.UserDefaultMaxBids}\n" +
$"Anticipo: {product.UserDefaultBidBeforeDeadlineMs}ms");
if (!confirmed) return;
// Applica i limiti
foreach (var auction in matchingAuctions)
{
if (product.UserDefaultMinPrice.HasValue)
auction.MinPrice = product.UserDefaultMinPrice.Value;
if (product.UserDefaultMaxPrice.HasValue)
auction.MaxPrice = product.UserDefaultMaxPrice.Value;
if (product.UserDefaultMinResets.HasValue)
auction.MinResets = product.UserDefaultMinResets.Value;
if (product.UserDefaultMaxResets.HasValue)
auction.MaxResets = product.UserDefaultMaxResets.Value;
if (product.UserDefaultMaxBids.HasValue)
auction.MaxClicks = product.UserDefaultMaxBids.Value;
if (product.UserDefaultBidBeforeDeadlineMs.HasValue)
auction.BidBeforeDeadlineMs = product.UserDefaultBidBeforeDeadlineMs.Value;
}
// Salva
AutoBidder.Utilities.PersistenceManager.SaveAuctions(AppState.Auctions.ToList());
await JSRuntime.InvokeVoidAsync("alert",
$"? Limiti applicati a {matchingAuctions.Count} aste di '{product.ProductName}'!");
}
catch (Exception ex)
{
await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
}
}
private bool HasUserDefaults(ProductStatisticsRecord product)
{
return product.UserDefaultMinPrice.HasValue
&& product.UserDefaultMaxPrice.HasValue;
}
}
<style>
@@ -631,7 +1290,25 @@ private AuctionResultExtended? selectedAuctionDetail;
background-color: rgba(0,123,255,0.1) !important;
}
.product-row {
cursor: pointer;
transition: background-color 0.2s;
}
.product-row:hover {
background-color: rgba(25,135,84,0.1) !important;
}
.table-success-subtle {
background-color: rgba(25, 135, 84, 0.1);
}
.table-info {
background-color: rgba(13, 202, 240, 0.2) !important;
}
.font-monospace {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.875rem;
}
</style>

View File

@@ -428,6 +428,22 @@ namespace AutoBidder.Services
auction.AddLog($"[ASTA TERMINATA] {statusMsg}");
OnLog?.Invoke($"[FINE] [{auction.AuctionId}] Asta {statusMsg} - Polling fermato");
// 💡 SUGGERIMENTO: Se persa e non abbiamo mai provato a puntare, potrebbe essere un problema di timing
if (!won && auction.SessionBidCount == 0)
{
var settings = Utilities.SettingsManager.Load();
int offsetMs = auction.BidBeforeDeadlineMs > 0
? auction.BidBeforeDeadlineMs
: settings.DefaultBidBeforeDeadlineMs;
// Se l'offset è <= 1000ms, il polling (~1s) potrebbe non catturare il momento giusto
if (offsetMs <= 1000)
{
auction.AddLog($"[💡 SUGGERIMENTO] Asta persa senza mai puntare. Con offset={offsetMs}ms e polling~1s, " +
$"potresti non vedere mai il timer scendere sotto {offsetMs}ms. Considera di aumentare l'offset a 1500-2000ms nelle impostazioni.");
}
}
auction.BidHistory.Add(new BidHistory
{
Timestamp = DateTime.UtcNow,
@@ -531,7 +547,7 @@ namespace AutoBidder.Services
{
var settings = SettingsManager.Load();
// Offset: millisecondi prima della scadenza
// Offset: millisecondi prima della scadenza (configurato dall'utente)
int offsetMs = auction.BidBeforeDeadlineMs > 0
? auction.BidBeforeDeadlineMs
: settings.DefaultBidBeforeDeadlineMs;
@@ -542,42 +558,53 @@ namespace AutoBidder.Services
// Skip se già vincitore o timer scaduto
if (state.IsMyBid || timerMs <= 0) return;
// ?? STIMA TEMPO RIMANENTE
// L'API dà timer in secondi interi (1000, 2000, ecc.)
// Quando cambia, salvo il timestamp. Poi stimo localmente.
bool timerChanged = Math.Abs(auction.LastRawTimer - timerMs) > 500;
if (timerChanged || !auction.LastDeadlineUpdateUtc.HasValue)
{
auction.LastRawTimer = timerMs;
auction.LastDeadlineUpdateUtc = DateTime.UtcNow;
auction.BidScheduled = false;
}
// Calcola tempo stimato rimanente
var elapsed = (DateTime.UtcNow - auction.LastDeadlineUpdateUtc.Value).TotalMilliseconds;
double estimatedRemaining = timerMs - elapsed;
// Log timing solo se abilitato
if (settings.LogTiming)
{
auction.AddLog($"[TIMING] API={timerMs:F0}ms, Elapsed={elapsed:F0}ms, Stima={estimatedRemaining:F0}ms");
auction.AddLog($"[TIMING] API={timerMs:F0}ms, Offset={offsetMs}ms");
}
// ?? È il momento di puntare?
if (estimatedRemaining > offsetMs) return; // Troppo presto
if (estimatedRemaining < -200) return; // Troppo tardi
// Punta quando il timer API è <= offset configurato dall'utente
// NESSUNA modifica automatica - l'utente decide il timing
if (timerMs > offsetMs)
{
return;
}
// Protezione doppia puntata
if (auction.BidScheduled) return;
// Timer <= offset = È IL MOMENTO DI PUNTARE!
auction.AddLog($"[BID WINDOW] Timer={timerMs:F0}ms <= Offset={offsetMs}ms - Verifica condizioni...");
// Cooldown 1 secondo
if (auction.LastClickAt.HasValue && (DateTime.UtcNow - auction.LastClickAt.Value).TotalMilliseconds < 1000) return;
// Resetta BidScheduled se il timer è AUMENTATO (qualcun altro ha puntato = nuovo ciclo)
if (timerMs > auction.LastScheduledTimerMs + 500)
{
auction.BidScheduled = false;
}
// Resetta anche se è passato troppo tempo dall'ultima puntata (nuovo ciclo)
if (auction.LastClickAt.HasValue &&
(DateTime.UtcNow - auction.LastClickAt.Value).TotalSeconds > 10)
{
auction.BidScheduled = false;
}
// Protezione doppia puntata SOLO per lo stesso ciclo di timer
if (auction.BidScheduled && Math.Abs(auction.LastScheduledTimerMs - timerMs) < 100)
{
auction.AddLog($"[SKIP] Puntata già schedulata per timer~={timerMs:F0}ms in questo ciclo");
return;
}
// Cooldown 1 secondo tra puntate
if (auction.LastClickAt.HasValue && (DateTime.UtcNow - auction.LastClickAt.Value).TotalMilliseconds < 1000)
{
auction.AddLog($"[COOLDOWN] Attesa cooldown puntata precedente");
return;
}
// 🔴 CONTROLLI FONDAMENTALI (prezzo, reset, limiti, puntate residue)
if (!ShouldBid(auction, state))
{
// I motivi vengono ora loggati sempre dentro ShouldBid
return;
}
@@ -587,19 +614,25 @@ namespace AutoBidder.Services
if (!decision.ShouldBid)
{
// 🔥 FIX: Logga SEMPRE il motivo del blocco strategia, non solo se LogStrategyDecisions è attivo
// Questo aiuta a capire perché si perdono le aste
auction.AddLog($"[STRATEGY] {decision.Reason}");
// Log aggiuntivo solo se debug strategie attivo
if (settings.LogStrategyDecisions)
{
auction.AddLog($"[STRATEGY] {decision.Reason}");
OnLog?.Invoke($"[{auction.Name}] STRATEGY blocked: {decision.Reason}");
}
return;
}
// ?? PUNTA!
auction.BidScheduled = true;
auction.LastScheduledTimerMs = timerMs;
if (settings.LogBids)
{
auction.AddLog($"[BID] Puntata a ~{estimatedRemaining:F0}ms dalla scadenza");
auction.AddLog($"[BID] Puntata con timer API={timerMs:F0}ms");
}
await ExecuteBid(auction, state, token);
@@ -649,12 +682,15 @@ namespace AutoBidder.Services
if (result.Success)
{
auction.AddLog($"[BID OK] Latenza: {result.LatencyMs}ms -> EUR{result.NewPrice:F2}");
// Log dettagliato con info ping per analisi timing
var pollingPing = auction.PollingLatencyMs;
auction.AddLog($"[BID OK] Latenza puntata: {result.LatencyMs}ms | Ping polling: {pollingPing}ms | Totale stimato: {result.LatencyMs + pollingPing}ms");
OnLog?.Invoke($"[OK] Puntata riuscita su {auction.Name} ({auction.AuctionId}): {result.LatencyMs}ms");
}
else
{
auction.AddLog($"[BID FAIL] {result.Error}");
var pollingPing = auction.PollingLatencyMs;
auction.AddLog($"[BID FAIL] {result.Error} | Ping: {pollingPing}ms");
OnLog?.Invoke($"[FAIL] Puntata fallita su {auction.Name} ({auction.AuctionId}): {result.Error}");
}
@@ -667,7 +703,7 @@ namespace AutoBidder.Services
Timer = state.Timer,
LatencyMs = result.LatencyMs,
Success = result.Success,
Notes = result.Success ? $"EUR{result.NewPrice:F2}" : (result.Error ?? "Errore sconosciuto")
Notes = result.Success ? $"OK" : (result.Error ?? "Errore sconosciuto")
});
}
catch (Exception ex)
@@ -698,6 +734,7 @@ namespace AutoBidder.Services
if (auction.CalculatedValue.SavingsPercentage.HasValue &&
auction.CalculatedValue.SavingsPercentage.Value < settings.MinSavingsPercentage)
{
// 🔥 Logga SEMPRE - è un blocco frequente e importante
auction.AddLog($"[VALUE] Puntata bloccata: risparmio {auction.CalculatedValue.SavingsPercentage.Value:F1}% < {settings.MinSavingsPercentage:F1}% richiesto");
return false;
}
@@ -705,11 +742,13 @@ namespace AutoBidder.Services
if (settings.LogTiming && settings.ValueCheckEnabled)
{
auction.AddLog($"[DEBUG] ? Controllo convenienza OK");
auction.AddLog($"[DEBUG] Controllo convenienza OK");
}
// ?? CONTROLLO ANTI-COLLISIONE: Rileva aste troppo "affollate"
// Se negli ultimi 10 secondi ci sono state 3+ puntate di utenti diversi, evita
// ?? CONTROLLO ANTI-COLLISIONE (OPZIONALE): Rileva aste troppo "affollate"
// DISABILITATO DI DEFAULT - può far perdere aste competitive!
if (settings.HardcodedAntiCollisionEnabled)
{
var recentBidsThreshold = 10; // secondi
var maxActiveBidders = 3; // se 3+ bidder attivi, potrebbe essere troppo affollata
@@ -745,6 +784,7 @@ namespace AutoBidder.Services
}
}
catch { /* Ignora errori nel controllo competizione */ }
}
if (settings.LogTiming)
{
@@ -757,13 +797,14 @@ namespace AutoBidder.Services
var session = _apiClient.GetSession();
if (session != null && session.RemainingBids <= settings.MinimumRemainingBids)
{
// 🔥 Logga SEMPRE - è un blocco importante
auction.AddLog($"[LIMIT] Puntata bloccata: puntate residue ({session.RemainingBids}) <= limite ({settings.MinimumRemainingBids})");
return false;
}
if (settings.LogTiming && session != null)
{
auction.AddLog($"[DEBUG] ? Puntate residue OK ({session.RemainingBids} > {settings.MinimumRemainingBids})");
auction.AddLog($"[DEBUG] Puntate residue OK ({session.RemainingBids} > {settings.MinimumRemainingBids})");
}
}
@@ -780,12 +821,14 @@ namespace AutoBidder.Services
// ?? CONTROLLO 3: MinPrice/MaxPrice
if (auction.MinPrice > 0 && state.Price < auction.MinPrice)
{
// 🔥 Logga SEMPRE questo blocco - è critico per capire perché non punta
auction.AddLog($"[PRICE] Prezzo troppo basso: €{state.Price:F2} < Min €{auction.MinPrice:F2}");
return false;
}
if (auction.MaxPrice > 0 && state.Price > auction.MaxPrice)
{
// 🔥 Logga SEMPRE questo blocco - è critico
auction.AddLog($"[PRICE] Prezzo troppo alto: €{state.Price:F2} > Max €{auction.MaxPrice:F2}");
return false;
}
@@ -801,12 +844,14 @@ namespace AutoBidder.Services
// ?? CONTROLLO 4: MinResets/MaxResets
if (auction.MinResets > 0 && auction.ResetCount < auction.MinResets)
{
// 🔥 Logga SEMPRE - è un motivo comune di aste perse
auction.AddLog($"[RESET] Reset troppo bassi: {auction.ResetCount} < Min {auction.MinResets}");
return false;
}
if (auction.MaxResets > 0 && auction.ResetCount >= auction.MaxResets)
{
// 🔥 Logga SEMPRE - è un motivo comune di aste perse
auction.AddLog($"[RESET] Reset massimi raggiunti: {auction.ResetCount} >= Max {auction.MaxResets}");
return false;
}

View File

@@ -732,6 +732,21 @@ namespace AutoBidder.Services
await using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
await cmd.ExecuteNonQueryAsync();
}),
new Migration(15, "Add user-defined default limits to ProductStatistics", async (conn) => {
var sql = @"
-- Aggiungi colonne per limiti definiti dall'utente (separati dai calcolati)
ALTER TABLE ProductStatistics ADD COLUMN UserDefaultMinPrice REAL;
ALTER TABLE ProductStatistics ADD COLUMN UserDefaultMaxPrice REAL;
ALTER TABLE ProductStatistics ADD COLUMN UserDefaultMinResets INTEGER;
ALTER TABLE ProductStatistics ADD COLUMN UserDefaultMaxResets INTEGER;
ALTER TABLE ProductStatistics ADD COLUMN UserDefaultMaxBids INTEGER;
ALTER TABLE ProductStatistics ADD COLUMN UserDefaultBidBeforeDeadlineMs INTEGER;
";
await using var cmd = conn.CreateCommand();
cmd.CommandText = sql;
await cmd.ExecuteNonQueryAsync();
})
};
@@ -1407,12 +1422,14 @@ namespace AutoBidder.Services
AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
AvgResets, MinResets, MaxResets,
RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs,
HourlyStatsJson, LastUpdated)
VALUES (@productKey, @productName, @totalAuctions, @wonAuctions, @lostAuctions,
@avgFinalPrice, @minFinalPrice, @maxFinalPrice,
@avgBidsToWin, @minBidsToWin, @maxBidsToWin,
@avgResets, @minResets, @maxResets,
@recMinPrice, @recMaxPrice, @recMinResets, @recMaxResets, @recMaxBids,
@userMinPrice, @userMaxPrice, @userMinResets, @userMaxResets, @userMaxBids, @userBidDeadline,
@hourlyJson, @lastUpdated)
ON CONFLICT(ProductKey) DO UPDATE SET
ProductName = @productName,
@@ -1433,6 +1450,12 @@ namespace AutoBidder.Services
RecommendedMinResets = @recMinResets,
RecommendedMaxResets = @recMaxResets,
RecommendedMaxBids = @recMaxBids,
UserDefaultMinPrice = COALESCE(@userMinPrice, UserDefaultMinPrice),
UserDefaultMaxPrice = COALESCE(@userMaxPrice, UserDefaultMaxPrice),
UserDefaultMinResets = COALESCE(@userMinResets, UserDefaultMinResets),
UserDefaultMaxResets = COALESCE(@userMaxResets, UserDefaultMaxResets),
UserDefaultMaxBids = COALESCE(@userMaxBids, UserDefaultMaxBids),
UserDefaultBidBeforeDeadlineMs = COALESCE(@userBidDeadline, UserDefaultBidBeforeDeadlineMs),
HourlyStatsJson = @hourlyJson,
LastUpdated = @lastUpdated;
";
@@ -1457,6 +1480,12 @@ namespace AutoBidder.Services
new SqliteParameter("@recMinResets", (object?)stats.RecommendedMinResets ?? DBNull.Value),
new SqliteParameter("@recMaxResets", (object?)stats.RecommendedMaxResets ?? DBNull.Value),
new SqliteParameter("@recMaxBids", (object?)stats.RecommendedMaxBids ?? DBNull.Value),
new SqliteParameter("@userMinPrice", (object?)stats.UserDefaultMinPrice ?? DBNull.Value),
new SqliteParameter("@userMaxPrice", (object?)stats.UserDefaultMaxPrice ?? DBNull.Value),
new SqliteParameter("@userMinResets", (object?)stats.UserDefaultMinResets ?? DBNull.Value),
new SqliteParameter("@userMaxResets", (object?)stats.UserDefaultMaxResets ?? DBNull.Value),
new SqliteParameter("@userMaxBids", (object?)stats.UserDefaultMaxBids ?? DBNull.Value),
new SqliteParameter("@userBidDeadline", (object?)stats.UserDefaultBidBeforeDeadlineMs ?? DBNull.Value),
new SqliteParameter("@hourlyJson", (object?)stats.HourlyStatsJson ?? DBNull.Value),
new SqliteParameter("@lastUpdated", DateTime.UtcNow.ToString("O"))
);
@@ -1473,6 +1502,7 @@ namespace AutoBidder.Services
AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
AvgResets, MinResets, MaxResets,
RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs,
HourlyStatsJson, LastUpdated
FROM ProductStatistics
WHERE ProductKey = @productKey;
@@ -1507,8 +1537,14 @@ namespace AutoBidder.Services
RecommendedMinResets = reader.IsDBNull(16) ? null : reader.GetInt32(16),
RecommendedMaxResets = reader.IsDBNull(17) ? null : reader.GetInt32(17),
RecommendedMaxBids = reader.IsDBNull(18) ? null : reader.GetInt32(18),
HourlyStatsJson = reader.IsDBNull(19) ? null : reader.GetString(19),
LastUpdated = reader.GetString(20)
UserDefaultMinPrice = reader.IsDBNull(19) ? null : reader.GetDouble(19),
UserDefaultMaxPrice = reader.IsDBNull(20) ? null : reader.GetDouble(20),
UserDefaultMinResets = reader.IsDBNull(21) ? null : reader.GetInt32(21),
UserDefaultMaxResets = reader.IsDBNull(22) ? null : reader.GetInt32(22),
UserDefaultMaxBids = reader.IsDBNull(23) ? null : reader.GetInt32(23),
UserDefaultBidBeforeDeadlineMs = reader.IsDBNull(24) ? null : reader.GetInt32(24),
HourlyStatsJson = reader.IsDBNull(25) ? null : reader.GetString(25),
LastUpdated = reader.GetString(26)
};
}
@@ -1558,6 +1594,7 @@ namespace AutoBidder.Services
AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
AvgResets, MinResets, MaxResets,
RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs,
HourlyStatsJson, LastUpdated
FROM ProductStatistics
ORDER BY TotalAuctions DESC;
@@ -1593,14 +1630,67 @@ namespace AutoBidder.Services
RecommendedMinResets = reader.IsDBNull(16) ? null : reader.GetInt32(16),
RecommendedMaxResets = reader.IsDBNull(17) ? null : reader.GetInt32(17),
RecommendedMaxBids = reader.IsDBNull(18) ? null : reader.GetInt32(18),
HourlyStatsJson = reader.IsDBNull(19) ? null : reader.GetString(19),
LastUpdated = reader.GetString(20)
UserDefaultMinPrice = reader.IsDBNull(19) ? null : reader.GetDouble(19),
UserDefaultMaxPrice = reader.IsDBNull(20) ? null : reader.GetDouble(20),
UserDefaultMinResets = reader.IsDBNull(21) ? null : reader.GetInt32(21),
UserDefaultMaxResets = reader.IsDBNull(22) ? null : reader.GetInt32(22),
UserDefaultMaxBids = reader.IsDBNull(23) ? null : reader.GetInt32(23),
UserDefaultBidBeforeDeadlineMs = reader.IsDBNull(24) ? null : reader.GetInt32(24),
HourlyStatsJson = reader.IsDBNull(25) ? null : reader.GetString(25),
LastUpdated = reader.GetString(26)
});
}
return results;
}
/// <summary>
/// Elimina un prodotto dalle statistiche per ProductKey
/// </summary>
public async Task<int> DeleteProductStatisticsAsync(string productKey)
{
var sql = @"
DELETE FROM ProductStatistics
WHERE ProductKey = @productKey;
";
return await ExecuteNonQueryAsync(sql,
new SqliteParameter("@productKey", productKey)
);
}
/// <summary>
/// Aggiorna i valori di default definiti dall'utente per un prodotto
/// </summary>
public async Task UpdateProductUserDefaultsAsync(string productKey,
double? minPrice, double? maxPrice,
int? minResets, int? maxResets,
int? maxBids, int? bidBeforeDeadlineMs)
{
var sql = @"
UPDATE ProductStatistics
SET UserDefaultMinPrice = @minPrice,
UserDefaultMaxPrice = @maxPrice,
UserDefaultMinResets = @minResets,
UserDefaultMaxResets = @maxResets,
UserDefaultMaxBids = @maxBids,
UserDefaultBidBeforeDeadlineMs = @bidDeadline,
LastUpdated = @lastUpdated
WHERE ProductKey = @productKey;
";
await ExecuteNonQueryAsync(sql,
new SqliteParameter("@productKey", productKey),
new SqliteParameter("@minPrice", (object?)minPrice ?? DBNull.Value),
new SqliteParameter("@maxPrice", (object?)maxPrice ?? DBNull.Value),
new SqliteParameter("@minResets", (object?)minResets ?? DBNull.Value),
new SqliteParameter("@maxResets", (object?)maxResets ?? DBNull.Value),
new SqliteParameter("@maxBids", (object?)maxBids ?? DBNull.Value),
new SqliteParameter("@bidDeadline", (object?)bidBeforeDeadlineMs ?? DBNull.Value),
new SqliteParameter("@lastUpdated", DateTime.UtcNow.ToString("O"))
);
}
private AuctionResultExtended ReadAuctionResultExtended(Microsoft.Data.Sqlite.SqliteDataReader reader)
{
return new AuctionResultExtended

View File

@@ -1,6 +1,8 @@
@using Microsoft.AspNetCore.Components.Authorization
@inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject AuctionMonitor AuctionMonitor
@implements IDisposable
<div class="nav-sidebar">
<div class="nav-header">
@@ -36,11 +38,32 @@
</div>
<div class="nav-footer">
<!-- Info Sessione Utente -->
@if (!string.IsNullOrEmpty(sessionUsername))
{
<div class="session-stats">
<div class="session-stat">
<i class="bi bi-hand-index-thumb-fill"></i>
<div class="stat-content">
<span class="stat-label">Puntate</span>
<span class="stat-value @GetBidsClass()">@sessionRemainingBids</span>
</div>
</div>
<div class="session-stat">
<i class="bi bi-wallet2"></i>
<div class="stat-content">
<span class="stat-label">Credito</span>
<span class="stat-value text-success">€@sessionShopCredit.ToString("F2")</span>
</div>
</div>
</div>
}
<AuthorizeView>
<Authorized>
<div class="user-badge">
<div class="user-badge @(string.IsNullOrEmpty(sessionUsername) ? "disconnected" : "connected")">
<i class="bi bi-person-circle"></i>
<span>@context.User.Identity?.Name</span>
<span>@(string.IsNullOrEmpty(sessionUsername) ? "Non connesso" : sessionUsername)</span>
</div>
<a href="/Account/Logout" class="nav-menu-item logout-item">
<i class="bi bi-box-arrow-right"></i>
@@ -52,6 +75,52 @@
</nav>
</div>
@code {
private string? sessionUsername;
private int sessionRemainingBids;
private double sessionShopCredit;
private System.Threading.Timer? refreshTimer;
protected override void OnInitialized()
{
LoadSessionInfo();
// Refresh ogni 5 secondi
refreshTimer = new System.Threading.Timer(async _ =>
{
LoadSessionInfo();
await InvokeAsync(StateHasChanged);
}, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
}
private void LoadSessionInfo()
{
try
{
var session = AuctionMonitor.GetSession();
if (session != null)
{
sessionUsername = session.Username;
sessionRemainingBids = session.RemainingBids;
sessionShopCredit = session.ShopCredit;
}
}
catch { }
}
private string GetBidsClass()
{
if (sessionRemainingBids <= 10) return "text-danger";
if (sessionRemainingBids <= 50) return "text-warning";
return "text-success";
}
public void Dispose()
{
refreshTimer?.Dispose();
}
}
<style>
.nav-sidebar {
display: flex;
@@ -150,6 +219,52 @@
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.session-stats {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.75rem;
margin-bottom: 0.75rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.session-stat {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.375rem 0;
}
.session-stat i {
font-size: 0.875rem;
width: 1.25rem;
text-align: center;
color: rgba(255, 255, 255, 0.5);
}
.session-stat .stat-content {
display: flex;
justify-content: space-between;
flex: 1;
align-items: center;
}
.session-stat .stat-label {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.5);
}
.session-stat .stat-value {
font-size: 0.875rem;
font-weight: 600;
}
.session-stat .text-success { color: #22c55e; }
.session-stat .text-warning { color: #f59e0b; }
.session-stat .text-danger { color: #ef4444; }
.user-badge {
display: flex;
align-items: center;
@@ -162,6 +277,15 @@
font-size: 0.875rem;
}
.user-badge.connected {
border-left: 3px solid #22c55e;
}
.user-badge.disconnected {
border-left: 3px solid #ef4444;
color: rgba(255, 255, 255, 0.4);
}
.user-badge i {
font-size: 1.25rem;
}

View File

@@ -147,6 +147,13 @@ namespace AutoBidder.Utilities
/// </summary>
public bool LogErrors { get; set; } = true;
/// <summary>
/// Applica automaticamente i limiti salvati nel prodotto quando si aggiunge una nuova asta.
/// Se TRUE e il prodotto ha valori di default salvati, li applica automaticamente.
/// Default: true (consigliato per coerenza)
/// </summary>
public bool AutoApplyProductDefaults { get; set; } = true;
/// <summary>
/// Log stato asta (terminata, reset, ecc.) [STATUS]
/// Default: true
@@ -203,6 +210,14 @@ namespace AutoBidder.Utilities
/// </summary>
public double MinSavingsPercentage { get; set; } = -5.0;
/// <summary>
/// Abilita il controllo anti-collisione hardcoded.
/// Se attivo, blocca le puntate quando ci sono 3+ bidder attivi negli ultimi 10 secondi.
/// ATTENZIONE: Questo controllo può far perdere aste competitive!
/// Default: false (DISABILITATO - non blocca mai)
/// </summary>
public bool HardcodedAntiCollisionEnabled { get; set; } = false;
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
// RILEVAMENTO COMPETIZIONE E HEAT METRIC
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥

View File

@@ -1,5 +1,423 @@
/* === MODERN PAGE STYLES (append to app-wpf.css) === */
/* ═══════════════════════════════════════════════════════════════════
TOOLBAR COMPATTA CON PULSANTI E CONTEGGI
═══════════════════════════════════════════════════════════════════ */
.toolbar-compact {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0.75rem;
background: linear-gradient(135deg, rgba(20, 20, 30, 0.8) 0%, rgba(30, 30, 45, 0.8) 100%);
border-radius: var(--radius-md);
border: 1px solid rgba(255, 255, 255, 0.05);
margin-bottom: 0.5rem;
flex-wrap: wrap;
}
.btn-group-actions {
display: flex;
gap: 0.25rem;
}
.action-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: var(--radius-sm);
font-size: 1rem;
cursor: pointer;
transition: all 0.2s ease;
}
.action-btn.success {
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
color: white;
}
.action-btn.success:hover {
box-shadow: 0 0 10px rgba(34, 197, 94, 0.5);
transform: translateY(-1px);
}
.action-btn.warning {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
color: #1a1a1a;
}
.action-btn.warning:hover {
box-shadow: 0 0 10px rgba(245, 158, 11, 0.5);
transform: translateY(-1px);
}
.action-btn.secondary {
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
color: white;
}
.action-btn.secondary:hover {
box-shadow: 0 0 10px rgba(107, 114, 128, 0.5);
transform: translateY(-1px);
}
/* Indicatori Stato */
.status-indicators {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0 0.5rem;
border-left: 1px solid rgba(255, 255, 255, 0.1);
border-right: 1px solid rgba(255, 255, 255, 0.1);
}
.status-pill {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
transition: all 0.15s ease;
}
.status-pill:hover {
background: rgba(255, 255, 255, 0.1);
}
.status-pill i {
font-size: 0.7rem;
}
.status-pill.total { color: #a5b4fc; border-color: rgba(99, 102, 241, 0.3); }
.status-pill.active { color: #4ade80; border-color: rgba(34, 197, 94, 0.3); }
.status-pill.paused { color: #fbbf24; border-color: rgba(251, 191, 36, 0.3); }
.status-pill.stopped { color: #9ca3af; border-color: rgba(156, 163, 175, 0.3); }
.status-pill.won { color: #fbbf24; border-color: rgba(251, 191, 36, 0.3); }
.status-pill.lost { color: #f87171; border-color: rgba(248, 113, 113, 0.3); }
/* Pulsanti Gestione */
.btn-group-manage {
display: flex;
align-items: center;
gap: 0.25rem;
margin-left: auto;
}
.manage-separator {
width: 1px;
height: 20px;
background: rgba(255, 255, 255, 0.15);
margin: 0 0.25rem;
}
.manage-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: 1px solid transparent;
border-radius: var(--radius-sm);
font-size: 0.8rem;
cursor: pointer;
transition: all 0.15s ease;
background: transparent;
}
.manage-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.manage-btn.primary {
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
color: white;
}
.manage-btn.primary:hover:not(:disabled) {
box-shadow: 0 0 8px rgba(99, 102, 241, 0.5);
}
.manage-btn.danger {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: white;
}
.manage-btn.danger:hover:not(:disabled) {
box-shadow: 0 0 8px rgba(239, 68, 68, 0.5);
}
.manage-btn.danger-fill {
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
color: white;
}
.manage-btn.danger-fill:hover:not(:disabled) {
box-shadow: 0 0 10px rgba(220, 38, 38, 0.6);
}
.manage-btn.outline-success {
border-color: rgba(34, 197, 94, 0.4);
color: #4ade80;
}
.manage-btn.outline-success:hover:not(:disabled) {
background: rgba(34, 197, 94, 0.15);
}
.manage-btn.outline-warning {
border-color: rgba(245, 158, 11, 0.4);
color: #fbbf24;
}
.manage-btn.outline-warning:hover:not(:disabled) {
background: rgba(245, 158, 11, 0.15);
}
.manage-btn.outline-secondary {
border-color: rgba(156, 163, 175, 0.4);
color: #9ca3af;
}
.manage-btn.outline-secondary:hover:not(:disabled) {
background: rgba(156, 163, 175, 0.15);
}
.manage-btn.outline-gold {
border-color: rgba(251, 191, 36, 0.4);
color: #fbbf24;
}
.manage-btn.outline-gold:hover:not(:disabled) {
background: rgba(251, 191, 36, 0.15);
}
.manage-btn.outline-danger {
border-color: rgba(239, 68, 68, 0.4);
color: #f87171;
}
.manage-btn.outline-danger:hover:not(:disabled) {
background: rgba(239, 68, 68, 0.15);
}
/* ═══════════════════════════════════════════════════════════════════
LAYOUT CON SPLITTER TRASCINABILI
═══════════════════════════════════════════════════════════════════ */
/* Container principale - occupa tutto lo spazio disponibile */
.auction-monitor-container {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
overflow: hidden;
box-sizing: border-box;
}
/* Area contenuto principale */
.main-content-area {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
gap: 0;
}
/* Riga superiore (Aste + Log) */
.top-row {
display: flex;
flex-direction: row;
flex: 1;
min-height: 150px;
overflow: hidden;
gap: 0;
}
/* Riga inferiore (Dettagli) */
.bottom-row {
display: flex;
flex-direction: column;
min-height: 80px;
overflow: hidden;
}
/* Pannello generico */
.panel {
display: flex;
flex-direction: column;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
overflow: hidden;
box-sizing: border-box;
}
/* Pannello Aste */
.panel-auctions {
flex: 1;
min-width: 200px;
}
/* Pannello Log */
.panel-log {
flex: 0 0 280px;
min-width: 150px;
max-width: 450px;
}
/* Pannello Dettagli */
.panel-details {
flex: 1;
min-height: 80px;
overflow: auto;
}
/* Gutter/Splitter */
.gutter {
background: rgba(255, 255, 255, 0.03);
flex-shrink: 0;
transition: background 0.15s ease;
}
.gutter:hover {
background: rgba(99, 102, 241, 0.25);
}
.gutter:active {
background: rgba(99, 102, 241, 0.4);
}
.gutter-vertical {
width: 6px;
cursor: col-resize;
}
.gutter-horizontal {
height: 6px;
cursor: row-resize;
}
/* Header pannello */
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.4rem 0.6rem;
background: rgba(255, 255, 255, 0.03);
border-bottom: 1px solid var(--border-color);
font-size: 0.75rem;
font-weight: 600;
color: var(--text-secondary);
flex-shrink: 0;
}
.panel-header i {
margin-right: 0.4rem;
opacity: 0.7;
}
.panel-content {
flex: 1;
overflow: auto;
min-height: 0;
}
/* Contenuto dettagli */
.auction-details-content {
padding: 0.5rem;
height: 100%;
overflow: auto;
}
.details-header {
font-size: 0.9rem;
font-weight: 600;
padding: 0.25rem 0 0.5rem 0;
border-bottom: 1px solid var(--border-color);
margin-bottom: 0.5rem;
}
/* Placeholder dettagli */
.details-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: 1rem;
color: var(--text-muted);
text-align: center;
}
.details-placeholder i {
font-size: 1.25rem;
margin-bottom: 0.375rem;
opacity: 0.5;
}
.details-placeholder p {
margin: 0;
font-size: 0.8rem;
}
/* Colonna Ping */
.col-ping {
width: 55px;
min-width: 55px;
text-align: center;
font-size: 0.7rem;
font-weight: 500;
}
/* Responsive */
@media (max-width: 992px) {
.top-row {
flex-direction: column;
}
.gutter-vertical {
display: none;
}
.panel-log {
flex: 0 0 150px;
max-width: none;
}
.toolbar-compact {
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
}
.btn-group-actions,
.status-indicators,
.btn-group-manage {
justify-content: center;
}
.status-indicators {
border: none;
padding: 0.5rem 0;
border-top: 1px solid rgba(255, 255, 255, 0.1);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.btn-group-manage {
margin-left: 0;
flex-wrap: wrap;
}
}
/* ✅ NUOVO: Stili per selezione riga e colonna puntate */
.table-hover tbody tr {
cursor: pointer;
@@ -712,3 +1130,52 @@
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%) !important;
color: #e5e5e5 !important;
}
/* ═══════════════════════════════════════════════════════════════════
BANNER STATISTICHE ASTE - RIMOSSO (sostituito da toolbar compatta)
═══════════════════════════════════════════════════════════════════ */
/* Legacy support - nascosto */
.auctions-stats-banner {
display: none;
}
/* ═══════════════════════════════════════════════════════════════════
TABELLA COMPATTA
═══════════════════════════════════════════════════════════════════ */
.table-compact {
font-size: 0.8rem;
}
.table-compact th {
padding: 0.4rem 0.5rem;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.3px;
white-space: nowrap;
}
.table-compact td {
padding: 0.35rem 0.5rem;
vertical-align: middle;
}
.table-compact .col-stato {
width: 80px;
}
.table-compact .col-azioni {
width: 90px;
}
/* Pulsanti extra small */
.btn-xs {
padding: 0.15rem 0.35rem;
font-size: 0.7rem;
line-height: 1.2;
}
.btn-xs i {
font-size: 0.75rem;
}