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] [JsonIgnore]
public bool BidScheduled { get; set; } 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 // Storico
public List<BidHistory> BidHistory { get; set; } = new List<BidHistory>(); public List<BidHistory> BidHistory { get; set; } = new List<BidHistory>();
public Dictionary<string, BidderInfo> BidderStats { get; set; } = new(StringComparer.OrdinalIgnoreCase); 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? RecommendedMaxResets { get; set; }
public int? RecommendedMaxBids { 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 // JSON con statistiche per fascia oraria
public string? HourlyStatsJson { get; set; } public string? HourlyStatsJson { get; set; }

View File

@@ -7,148 +7,155 @@
<PageTitle>Monitor Aste - AutoBidder</PageTitle> <PageTitle>Monitor Aste - AutoBidder</PageTitle>
<div class="auction-monitor animate-fade-in"> <div class="auction-monitor-container">
<!-- Toolbar Superiore --> <!-- Toolbar Compatta -->
<div class="toolbar animate-fade-in-down"> <div class="toolbar-compact">
<!-- Box Sessione Utente - Compatto in linea --> <!-- Pulsanti Azioni Massiva (senza conteggi) -->
<div class="toolbar-user-info"> <div class="btn-group-actions">
@if (!string.IsNullOrEmpty(sessionUsername)) <button class="action-btn success" @onclick="StartAll" title="Avvia tutte le aste">
{ <i class="bi bi-play-fill"></i>
<div class="user-card connected"> </button>
<i class="bi bi-person-circle user-icon"></i> <button class="action-btn warning" @onclick="PauseAll" title="Metti in pausa tutte le aste">
<span class="user-name">@sessionUsername</span> <i class="bi bi-pause-fill"></i>
<div class="divider"></div> </button>
<div class="stat-compact"> <button class="action-btn secondary" @onclick="StopAll" title="Ferma tutte le aste">
<i class="bi bi-hand-index-thumb-fill"></i> <i class="bi bi-stop-fill"></i>
<span class="stat-value @GetBidsClass()">@sessionRemainingBids</span> </button>
</div> </div>
<div class="divider"></div>
<div class="stat-compact"> <!-- Indicatori Stato Aste (tutti gli stati) -->
<i class="bi bi-wallet2"></i> <div class="status-indicators">
<span class="stat-value text-success">€@sessionShopCredit.ToString("F2")</span> <div class="status-pill total" title="Totale aste">
<i class="bi bi-collection"></i>
<span>@auctions.Count</span>
</div> </div>
@if (sessionAuctionsWon > 0) <div class="status-pill active" title="Aste attive">
{ <i class="bi bi-play-circle-fill"></i>
<div class="divider"></div> <span>@GetActiveAuctionsCount()</span>
<div class="stat-compact"> </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> <i class="bi bi-trophy-fill"></i>
<span class="stat-value text-warning">@sessionAuctionsWon</span> <span>@GetWonAuctionsCount()</span>
</div> </div>
} <div class="status-pill lost" title="Aste perse">
<i class="bi bi-x-circle-fill"></i>
<span>@GetLostAuctionsCount()</span>
</div> </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> </div>
<!-- Pulsanti Azioni (Centro-Destra) --> <!-- Pulsanti Gestione -->
<div class="toolbar-actions"> <div class="btn-group-manage">
<button class="btn btn-success hover-lift" @onclick="StartAll"> <button class="manage-btn primary" @onclick="ShowAddAuctionDialog" title="Aggiungi nuova asta">
<i class="bi bi-play-fill"></i> Avvia Tutto <i class="bi bi-plus-lg"></i>
</button> </button>
<button class="btn btn-warning hover-lift" @onclick="PauseAll"> <button class="manage-btn danger" @onclick="RemoveSelectedAuction" disabled="@(selectedAuction == null)" title="Rimuovi selezionata">
<i class="bi bi-pause-fill"></i> Pausa Tutto <i class="bi bi-trash"></i>
</button> </button>
<button class="btn btn-danger hover-lift" @onclick="StopAll"> <div class="manage-separator"></div>
<i class="bi bi-stop-fill"></i> Ferma Tutto <button class="manage-btn outline-success" @onclick="RemoveActiveAuctions" disabled="@(GetActiveAuctionsCount() == 0)" title="Rimuovi attive">
<i class="bi bi-play-circle"></i>
</button> </button>
<button class="btn btn-primary ms-3 hover-lift" @onclick="ShowAddAuctionDialog"> <button class="manage-btn outline-warning" @onclick="RemovePausedAuctions" disabled="@(GetPausedAuctionsCount() == 0)" title="Rimuovi in pausa">
<i class="bi bi-plus-lg"></i> Aggiungi Asta <i class="bi bi-pause-circle"></i>
</button> </button>
<button class="btn btn-secondary hover-lift" @onclick="RemoveSelectedAuction" disabled="@(selectedAuction == null)"> <button class="manage-btn outline-secondary" @onclick="RemoveStoppedAuctions" disabled="@(GetStoppedAuctionsCount() == 0)" title="Rimuovi fermate">
<i class="bi bi-trash"></i> Rimuovi <i class="bi bi-stop-circle"></i>
</button> </button>
<button class="btn btn-outline-warning hover-lift" @onclick="RemoveCompletedAuctions" disabled="@(!HasCompletedAuctions())" title="Rimuovi aste terminate (vengono salvate nel database)"> <button class="manage-btn outline-gold" @onclick="RemoveWonAuctions" disabled="@(GetWonAuctionsCount() == 0)" title="Rimuovi vinte">
<i class="bi bi-check2-all"></i> Rimuovi Terminate <i class="bi bi-trophy"></i>
</button> </button>
<button class="btn btn-outline-danger hover-lift" @onclick="RemoveAllAuctions" disabled="@(auctions.Count == 0)" title="Rimuovi tutte le aste (quelle terminate verranno salvate)"> <button class="manage-btn outline-danger" @onclick="RemoveLostAuctions" disabled="@(GetLostAuctionsCount() == 0)" title="Rimuovi perse">
<i class="bi bi-trash-fill"></i> Rimuovi Tutte <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> </button>
</div> </div>
</div> </div>
<div class="content-layout"> <!-- Area Principale con Layout a Griglia -->
<!-- GRIGLIA ASTE - PARTE SUPERIORE SINISTRA --> <div class="main-content-area">
<div class="auctions-grid-section animate-fade-in-left delay-100 shadow-hover"> <!-- Riga Superiore: Aste + Log -->
<h3><i class="bi bi-list-check"></i> Aste Monitorate (@auctions.Count)</h3> <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) @if (auctions.Count == 0)
{ {
<div class="alert alert-info animate-fade-in-up"> <div class="alert alert-info animate-fade-in-up m-2">
<i class="bi bi-info-circle"></i> Nessuna asta monitorata. Clicca su "Aggiungi Asta" per iniziare. <i class="bi bi-info-circle"></i> Nessuna asta monitorata. Clicca su <i class="bi bi-plus-lg"></i> per iniziare.
</div> </div>
} }
else else
{ {
<div class="table-responsive"> <div class="table-responsive panel-content">
<table class="table table-striped table-hover mb-0 table-fixed"> <table class="table table-striped table-hover mb-0 table-fixed table-compact">
<thead> <thead>
<tr> <tr>
<th class="col-stato sortable-header" @onclick='() => SortAuctionsBy("stato")'><i class="bi bi-toggle-on"></i> Stato @GetSortIndicator("stato")</th> <th class="col-stato sortable-header" @onclick='() => SortAuctionsBy("stato")'>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-nome sortable-header" @onclick='() => SortAuctionsBy("nome")'>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-prezzo sortable-header" @onclick='() => SortAuctionsBy("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-timer sortable-header" @onclick='() => SortAuctionsBy("timer")'>Timer @GetSortIndicator("timer")</th>
<th class="col-ultimo"><i class="bi bi-person"></i> Ultimo</th> <th class="col-ultimo">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-click sortable-header" @onclick='() => SortAuctionsBy("puntate")'>Punt. @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-ping sortable-header" @onclick='() => SortAuctionsBy("ping")'>Ping @GetSortIndicator("ping")</th>
<th class="col-azioni"><i class="bi bi-gear"></i> Azioni</th> <th class="col-azioni">Azioni</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (var auction in GetSortedAuctions()) @foreach (var auction in GetSortedAuctions())
{ {
<tr class="@GetRowClass(auction) @(selectedAuction == auction ? "selected-row" : "") table-row-enter transition-all" <tr class="@GetRowClass(auction) @(selectedAuction == auction ? "selected-row" : "")"
@onclick="() => SelectAuction(auction)" @onclick="() => SelectAuction(auction)">
style="cursor: pointer;">
<td class="col-stato"> <td class="col-stato">
<span class="badge @GetStatusBadgeClass(auction) @GetStatusAnimationClass(auction)"> <span class="badge @GetStatusBadgeClass(auction) @GetStatusAnimationClass(auction)">
@((MarkupString)GetStatusIcon(auction)) @GetStatusText(auction) @((MarkupString)GetStatusIcon(auction)) @GetStatusText(auction)
</span> </span>
</td> </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-prezzo @GetPriceClass(auction)">@GetPriceDisplay(auction)</td>
<td class="col-timer">@GetTimerDisplay(auction)</td> <td class="col-timer">@GetTimerDisplay(auction)</td>
<td class="col-ultimo">@GetLastBidder(auction)</td> <td class="col-ultimo">@GetLastBidder(auction)</td>
<td class="col-click bids-column fw-bold">@GetMyBidsCount(auction)</td> <td class="col-click bids-column">@GetMyBidsCount(auction)</td>
<td class="col-ping">@GetPingDisplay(auction)</td> <td class="col-ping @GetPingClass(auction)">@GetPingDisplay(auction)</td>
<td class="col-azioni"> <td class="col-azioni">
<div class="btn-group btn-group-sm" @onclick:stopPropagation="true"> <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)" @onclick="() => ManualBidAuction(auction)"
title="Punta Manualmente" title="Punta"
disabled="@IsManualBidding(auction)"> 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> <i class="bi bi-hand-index-thumb"></i>
}
</button> </button>
@if (auction.IsActive && !auction.IsPaused) @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> <i class="bi bi-pause-fill"></i>
</button> </button>
} }
else if (auction.IsPaused) 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> <i class="bi bi-play-fill"></i>
</button> </button>
} }
else 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> <i class="bi bi-play-fill"></i>
</button> </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> <i class="bi bi-stop-fill"></i>
</button> </button>
</div> </div>
@@ -161,18 +168,18 @@
} }
</div> </div>
<!-- SPLITTER VERTICALE --> <!-- Splitter Verticale -->
<div class="splitter-vertical"></div> <div class="gutter gutter-vertical" id="gutterVertical"></div>
<!-- LOG GLOBALE - PARTE SUPERIORE DESTRA --> <!-- Pannello Log -->
<div class="global-log animate-fade-in-right delay-200"> <div class="panel panel-log" id="panelLog">
<div class="d-flex justify-content-between align-items-center"> <div class="panel-header">
<h4 class="mb-0"><i class="bi bi-terminal"></i> Log Globale</h4> <span><i class="bi bi-terminal"></i> Log</span>
<button class="btn btn-sm btn-secondary" @onclick="ClearGlobalLog"> <button class="btn btn-xs btn-secondary" @onclick="ClearGlobalLog">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</button> </button>
</div> </div>
<div class="log-box" id="globalLogContainer" @ref="globalLogRef"> <div class="panel-content log-box" id="globalLogContainer" @ref="globalLogRef">
@if (globalLog.Count == 0) @if (globalLog.Count == 0)
{ {
<div class="text-muted"><i class="bi bi-inbox"></i> Nessun log ancora...</div> <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 class="@GetLogEntryClass(logEntry)">@logEntry.Message</div>
} }
} }
<div id="logScrollAnchor"></div>
</div> </div>
</div> </div>
</div> </div>
<!-- SPLITTER ORIZZONTALE --> <!-- Splitter Orizzontale -->
<div class="splitter-horizontal"></div> <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) @if (selectedAuction != null)
{ {
<div class="auction-details-tabs animate-fade-in-up delay-300 shadow-hover"> <div class="auction-details-content">
<h3><i class="bi bi-info-circle-fill"></i> @selectedAuction.Name <small class="text-muted">(ID: @selectedAuction.AuctionId)</small></h3> <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"> <ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
@@ -243,7 +254,6 @@
</div> </div>
</div> </div>
<!-- Layout compatto a griglia per impostazioni -->
<div class="settings-grid-compact"> <div class="settings-grid-compact">
<div class="setting-item"> <div class="setting-item">
<label><i class="bi bi-speedometer2"></i> Anticipo (ms)</label> <label><i class="bi bi-speedometer2"></i> Anticipo (ms)</label>
@@ -263,7 +273,6 @@
</div> </div>
</div> </div>
<!-- Pulsante Applica Limiti Consigliati -->
<div class="mt-2 pt-2 border-top"> <div class="mt-2 pt-2 border-top">
<button class="btn btn-outline-primary btn-sm w-100" <button class="btn btn-outline-primary btn-sm w-100"
@onclick="ApplyRecommendedLimitsToSelected" @onclick="ApplyRecommendedLimitsToSelected"
@@ -295,7 +304,6 @@
<div class="tab-panel-content"> <div class="tab-panel-content">
@if (selectedAuction.CalculatedValue != null) @if (selectedAuction.CalculatedValue != null)
{ {
<!-- Sezione Principale - Compatta -->
<div class="product-info-compact"> <div class="product-info-compact">
<div class="info-cards"> <div class="info-cards">
<div class="info-card primary"> <div class="info-card primary">
@@ -305,7 +313,6 @@
<strong>@GetBuyNowPriceDisplay(selectedAuction)</strong> <strong>@GetBuyNowPriceDisplay(selectedAuction)</strong>
</div> </div>
</div> </div>
<div class="info-card info"> <div class="info-card info">
<i class="bi bi-truck"></i> <i class="bi bi-truck"></i>
<div> <div>
@@ -314,41 +321,33 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Calcoli in linea -->
<div class="calc-inline"> <div class="calc-inline">
<div class="calc-item"> <div class="calc-item">
<i class="bi bi-currency-euro"></i> <i class="bi bi-currency-euro"></i>
<span class="label">Prezzo attuale</span> <span class="label">Prezzo attuale</span>
<span class="value">€@selectedAuction.CalculatedValue.CurrentPrice.ToString("F2")</span> <span class="value">€@selectedAuction.CalculatedValue.CurrentPrice.ToString("F2")</span>
</div> </div>
<div class="calc-item"> <div class="calc-item">
<i class="bi bi-hand-index"></i> <i class="bi bi-hand-index"></i>
<span class="label">Totale puntate</span> <span class="label">Totale puntate</span>
<span class="value">@selectedAuction.CalculatedValue.TotalBids</span> <span class="value">@selectedAuction.CalculatedValue.TotalBids</span>
</div> </div>
<div class="calc-item highlight"> <div class="calc-item highlight">
<i class="bi bi-person-check-fill"></i> <i class="bi bi-person-check-fill"></i>
<span class="label">Tue puntate</span> <span class="label">Tue puntate</span>
<span class="value">@selectedAuction.CalculatedValue.MyBids</span> <span class="value">@selectedAuction.CalculatedValue.MyBids</span>
</div> </div>
<div class="calc-item"> <div class="calc-item">
<i class="bi bi-cash-coin"></i> <i class="bi bi-cash-coin"></i>
<span class="label">Costo puntate</span> <span class="label">Costo puntate</span>
<span class="value">€@selectedAuction.CalculatedValue.MyBidsCost.ToString("F2")</span> <span class="value">€@selectedAuction.CalculatedValue.MyBidsCost.ToString("F2")</span>
</div> </div>
</div> </div>
<!-- Totali compatti -->
<div class="totals-compact"> <div class="totals-compact">
<div class="total-item warning"> <div class="total-item warning">
<span>Costo Totale se vinci</span> <span>Costo Totale se vinci</span>
<strong>€@selectedAuction.CalculatedValue.TotalCostIfWin.ToString("F2")</strong> <strong>€@selectedAuction.CalculatedValue.TotalCostIfWin.ToString("F2")</strong>
</div> </div>
<div class="total-item @(selectedAuction.CalculatedValue.Savings > 0 ? "success" : "danger")"> <div class="total-item @(selectedAuction.CalculatedValue.Savings > 0 ? "success" : "danger")">
<span> <span>
<i class="bi bi-@(selectedAuction.CalculatedValue.Savings > 0 ? "arrow-down-circle-fill" : "arrow-up-circle-fill")"></i> <i class="bi bi-@(selectedAuction.CalculatedValue.Savings > 0 ? "arrow-down-circle-fill" : "arrow-up-circle-fill")"></i>
@@ -356,25 +355,17 @@
</span> </span>
<strong>@GetSavingsDisplay(selectedAuction)</strong> <strong>@GetSavingsDisplay(selectedAuction)</strong>
</div> </div>
<div class="verdict-badge @(selectedAuction.CalculatedValue.Savings > 0 ? "success" : "danger")"> <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> <i class="bi bi-@(selectedAuction.CalculatedValue.Savings > 0 ? "check-circle-fill" : "x-circle-fill")"></i>
@(selectedAuction.CalculatedValue.Savings > 0 ? "Conveniente!" : "Non conveniente") @(selectedAuction.CalculatedValue.Savings > 0 ? "Conveniente!" : "Non conveniente")
</div> </div>
</div> </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 else
{ {
<div class="alert alert-secondary"> <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>
} }
</div> </div>
@@ -384,13 +375,11 @@
<div class="tab-pane fade" id="content-history" role="tabpanel"> <div class="tab-pane fade" id="content-history" role="tabpanel">
<div class="tab-panel-content"> <div class="tab-panel-content">
@{ @{
// ?? FIX: Rimuovi duplicati consecutivi (stesso prezzo + stesso utente)
var recentBidsList = GetRecentBidsSafe(selectedAuction); var recentBidsList = GetRecentBidsSafe(selectedAuction);
var filteredBids = new List<BidHistoryEntry>(); var filteredBids = new List<BidHistoryEntry>();
BidHistoryEntry? lastBid = null; BidHistoryEntry? lastBid = null;
foreach (var bid in recentBidsList) foreach (var bid in recentBidsList)
{ {
// Salta se è un duplicato del precedente (stesso prezzo E stesso utente)
if (lastBid != null && if (lastBid != null &&
Math.Abs(bid.Price - lastBid.Price) < 0.001m && Math.Abs(bid.Price - lastBid.Price) < 0.001m &&
bid.Username.Equals(lastBid.Username, StringComparison.OrdinalIgnoreCase)) bid.Username.Equals(lastBid.Username, StringComparison.OrdinalIgnoreCase))
@@ -405,29 +394,12 @@
{ {
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-sm table-striped"> <table class="table table-sm table-striped">
<thead> <thead><tr><th>Utente</th><th>Prezzo</th><th>Data/Ora</th><th>Tipo</th></tr></thead>
<tr>
<th>Utente</th>
<th>Prezzo</th>
<th>Data/Ora</th>
<th>Tipo</th>
</tr>
</thead>
<tbody> <tbody>
@foreach (var bid in filteredBids.Take(50)) @foreach (var bid in filteredBids.Take(50))
{ {
<tr class="@(bid.IsMyBid ? "my-bid-row" : "")"> <tr class="@(bid.IsMyBid ? "my-bid-row" : "")">
<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>
@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="fw-bold">€@bid.PriceFormatted</td>
<td class="text-muted small">@bid.TimeFormatted</td> <td class="text-muted small">@bid.TimeFormatted</td>
<td><span class="badge bg-secondary">@bid.BidType</span></td> <td><span class="badge bg-secondary">@bid.BidType</span></td>
@@ -439,9 +411,7 @@
} }
else else
{ {
<div class="alert alert-secondary"> <div class="alert alert-secondary"><i class="bi bi-inbox"></i> Nessuna puntata registrata.</div>
<i class="bi bi-inbox"></i> Nessuna puntata registrata per questa asta.
</div>
} }
</div> </div>
</div> </div>
@@ -450,75 +420,28 @@
<div class="tab-pane fade" id="content-bidders" role="tabpanel"> <div class="tab-pane fade" id="content-bidders" role="tabpanel">
<div class="tab-panel-content"> <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();
var bidderStatsCopy = selectedAuction.BidderStats
.Values
.OrderByDescending(b => b.BidCount)
.ToList();
// Per l'utente corrente, usa BidsUsedOnThisAuction (valore ufficiale dal server)
var myOfficialBidsCount = selectedAuction.BidsUsedOnThisAuction ?? 0; var myOfficialBidsCount = selectedAuction.BidsUsedOnThisAuction ?? 0;
var currentUsername = GetCurrentUsername(); var currentUsername = GetCurrentUsername();
} }
@if (bidderStatsCopy.Any()) @if (bidderStatsCopy.Any())
{ {
// Calcola il totale CUMULATIVO
var totalBidsCumulative = bidderStatsCopy.Sum(b => b.BidCount); 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"> <div class="table-responsive">
<table class="table table-sm table-striped"> <table class="table table-sm table-striped">
<thead> <thead><tr><th>#</th><th>Utente</th><th>Puntate</th><th>%</th></tr></thead>
<tr>
<th>Posizione</th>
<th>Utente</th>
<th>Puntate</th>
<th>Percentuale</th>
</tr>
</thead>
<tbody> <tbody>
@for (int i = 0; i < bidderStatsCopy.Count; i++) @for (int i = 0; i < bidderStatsCopy.Count; i++)
{ {
var bidder = bidderStatsCopy[i]; var bidder = bidderStatsCopy[i];
var isMe = bidder.Username.Equals(currentUsername, StringComparison.OrdinalIgnoreCase); 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 displayCount = isMe && myOfficialBidsCount > bidder.BidCount var percentage = totalBidsCumulative > 0 ? (displayCount * 100.0 / totalBidsCumulative) : 0;
? myOfficialBidsCount
: bidder.BidCount;
var percentage = totalBidsCumulative > 0
? (displayCount * 100.0 / totalBidsCumulative)
: 0;
<tr class="@(isMe ? "my-bid-row" : "")"> <tr class="@(isMe ? "my-bid-row" : "")">
<td><span class="badge bg-primary">#@(i + 1)</span></td> <td><span class="badge bg-primary">#@(i + 1)</span></td>
<td> <td>@if (isMe){<strong class="text-success">@bidder.Username</strong>}else{@bidder.Username}</td>
@if (isMe)
{
<strong class="text-success">@bidder.Username</strong>
<span class="badge bg-success ms-1">TU</span>
}
else
{
@bidder.Username
}
</td>
<td class="fw-bold">@displayCount</td> <td class="fw-bold">@displayCount</td>
<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>
<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>
</tr> </tr>
} }
</tbody> </tbody>
@@ -527,9 +450,7 @@
} }
else else
{ {
<div class="alert alert-secondary"> <div class="alert alert-secondary"><i class="bi bi-inbox"></i> Nessun dato sui puntatori.</div>
<i class="bi bi-inbox"></i> Nessun dato sui puntatori disponibile.
</div>
} }
</div> </div>
</div> </div>
@@ -547,7 +468,7 @@
} }
else 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>
</div> </div>
@@ -557,13 +478,14 @@
} }
else else
{ {
<div class="auction-details-tabs animate-fade-in shadow-hover"> <div class="details-placeholder">
<div class="alert alert-secondary text-center my-5"> <i class="bi bi-arrow-up"></i>
<i class="bi bi-arrow-up" style="font-size: 2rem; display: block; margin-bottom: 0.5rem;"></i> <p>Seleziona un'asta per visualizzare i dettagli</p>
<p class="mb-0">Seleziona un'asta dalla griglia per visualizzare i dettagli</p>
</div>
</div> </div>
} }
</div>
</div>
</div>
</div> </div>
<!-- Modal Aggiungi Asta --> <!-- 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 show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">
<div class="modal-dialog modal-dialog-centered"> <div class="modal-dialog modal-dialog-centered">
<div class="modal-content animate-scale-in"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"><i class="bi bi-plus-circle"></i> Aggiungi Nuova Asta</h5> <h5 class="modal-title"><i class="bi bi-plus-circle"></i> Aggiungi Nuova Asta</h5>
<button type="button" class="btn-close" @onclick="CloseAddDialog"></button> <button type="button" class="btn-close" @onclick="CloseAddDialog"></button>
@@ -579,29 +501,97 @@
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">
<label class="form-label fw-bold"><i class="bi bi-link-45deg"></i> URL Asta:</label> <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" @bind="addDialogUrl"
placeholder="https://it.bidoo.com/asta/..." /> placeholder="https://it.bidoo.com/asta/..." />
@if (addDialogError != null) @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 <i class="bi bi-exclamation-triangle"></i> @addDialogError
</div> </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> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary hover-lift" @onclick="CloseAddDialog"> <button type="button" class="btn btn-secondary" @onclick="CloseAddDialog">Annulla</button>
<i class="bi bi-x-circle"></i> Annulla <button type="button" class="btn btn-primary" @onclick="AddAuction" disabled="@string.IsNullOrWhiteSpace(addDialogUrl)">Aggiungi</button>
</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>
</div> </div>
</div> </div>
</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); 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() private async Task RemoveSelectedAuctionWithConfirm()
{ {
if (selectedAuction == null) return; if (selectedAuction == null) return;
@@ -1150,11 +1238,6 @@ namespace AutoBidder.Pages
var latency = auction.PollingLatencyMs; var latency = auction.PollingLatencyMs;
if (latency <= 0) return "-"; if (latency <= 0) return "-";
// Colora in base al ping
var cssClass = latency < 100 ? "text-success" :
latency < 300 ? "text-warning" :
"text-danger";
return $"{latency}ms"; return $"{latency}ms";
} }
catch 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) private IEnumerable<string> GetAuctionLog(AuctionInfo auction)
{ {
return auction.AuctionLog.TakeLast(50); return auction.AuctionLog.TakeLast(50);
@@ -1366,5 +1468,41 @@ namespace AutoBidder.Pages
StateHasChanged(); 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> </h2>
<div id="collapse-defaults" class="accordion-collapse collapse" aria-labelledby="heading-defaults" data-bs-parent="#settingsAccordion"> <div id="collapse-defaults" class="accordion-collapse collapse" aria-labelledby="heading-defaults" data-bs-parent="#settingsAccordion">
<div class="accordion-body"> <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="row g-3">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-speedometer2"></i> Anticipo puntata (ms)</label> <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" /> <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>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-hand-index-thumb"></i> Click massimi</label> <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" /> <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 class="form-text">0 = illimitati</div>
</div> </div>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-currency-euro"></i> Prezzo minimo (€)</label> <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" /> <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>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-currency-euro"></i> Prezzo massimo (€)</label> <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" /> <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>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-arrow-repeat"></i> Reset minimi</label> <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" /> <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>
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<label class="form-label fw-bold"><i class="bi bi-arrow-repeat"></i> Reset massimi</label> <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" /> <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>
<div class="col-12 col-md-6"> <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> <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" /> <input type="number" class="form-control" @bind="settings.MinimumRemainingBids" />
<div class="form-text">Questa è un'impostazione globale</div>
</div> </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"> <div class="mt-3">
<button class="btn btn-success" @onclick="SaveSettings"><i class="bi bi-check-lg"></i> Salva</button> <button class="btn btn-success" @onclick="SaveSettings"><i class="bi bi-check-lg"></i> Salva</button>
</div> </div>
@@ -689,6 +744,21 @@
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.925rem; 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> </style>
@code { @code {
@@ -706,6 +776,10 @@ private bool isApplyingToAll = false;
private string? applyToAllMessage = null; private string? applyToAllMessage = null;
private bool applyToAllSuccess = false; private bool applyToAllSuccess = false;
// Applica singole impostazioni
private HashSet<string> applyingSettings = new();
private string? singleSettingMessage = null;
private AutoBidder.Utilities.AppSettings settings = new(); private AutoBidder.Utilities.AppSettings settings = new();
private System.Threading.Timer? updateTimer; 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() private void SyncStartupSelectionsFromSettings()
{ {
if (settings.RememberAuctionStates) if (settings.RememberAuctionStates)

View File

@@ -143,7 +143,7 @@
<tbody> <tbody>
@foreach (var auction in filteredAuctions) @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)"> @onclick="() => SelectAuction(auction)">
<td> <td>
<small class="fw-bold">@TruncateName(auction.AuctionName, 30)</small> <small class="fw-bold">@TruncateName(auction.AuctionName, 30)</small>
@@ -200,15 +200,47 @@
<div class="card-header bg-success text-white"> <div class="card-header bg-success text-white">
<h5 class="mb-0"> <h5 class="mb-0">
<i class="bi bi-box-seam me-2"></i> <i class="bi bi-box-seam me-2"></i>
Prodotti Salvati Prodotti Salvati (@(filteredProducts?.Count ?? 0))
</h5> </h5>
</div> </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"> <div class="card-body p-0">
@if (products == null || !products.Any()) @if (filteredProducts == null || !filteredProducts.Any())
{ {
<div class="text-center py-5 text-muted"> <div class="text-center py-5 text-muted">
<i class="bi bi-inbox" style="font-size: 3rem;"></i> <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> </div>
} }
else else
@@ -217,25 +249,47 @@
<table class="table table-hover table-sm mb-0"> <table class="table table-hover table-sm mb-0">
<thead class="table-light sticky-top"> <thead class="table-light sticky-top">
<tr> <tr>
<th>Prodotto</th> <th class="sortable-header" @onclick='() => SortProductsBy("name")'>
<th class="text-center">Aste</th> Prodotto @GetProductSortIndicator("name")
<th class="text-center">Win%</th> </th>
<th class="text-end">Limiti €</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> <th class="text-center">Azioni</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@foreach (var product in products) @foreach (var product in filteredProducts)
{ {
var winRate = product.TotalAuctions > 0 var winRate = product.TotalAuctions > 0
? (product.WonAuctions * 100.0 / product.TotalAuctions) ? (product.WonAuctions * 100.0 / product.TotalAuctions)
: 0; : 0;
var isEditing = editingProductKey == product.ProductKey;
<tr> <tr class="product-row @(selectedProduct?.ProductKey == product.ProductKey ? "table-info" : "")"
@onclick="() => SelectProduct(product)">
<td> <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> <small class="fw-bold">@product.ProductName</small>
<br/> <br/>
<small class="text-muted">@product.TotalAuctions totali (@product.WonAuctions vinte)</small> <small class="text-muted">@product.TotalAuctions totali (@product.WonAuctions vinte)</small>
</div>
</div>
</td> </td>
<td class="text-center fw-bold"> <td class="text-center fw-bold">
@product.TotalAuctions @product.TotalAuctions
@@ -246,10 +300,13 @@
</span> </span>
</td> </td>
<td class="text-end"> <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"> <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> </small>
} }
else else
@@ -257,21 +314,130 @@
<small class="text-muted">-</small> <small class="text-muted">-</small>
} }
</td> </td>
<td class="text-center"> <td class="text-end">
@if (product.RecommendedMinPrice.HasValue && product.RecommendedMaxPrice.HasValue) @if (product.RecommendedMinPrice.HasValue && product.RecommendedMaxPrice.HasValue)
{ {
<button class="btn btn-sm btn-primary" <small class="text-success fw-bold">
@onclick="() => ApplyLimitsToProduct(product)" €@product.RecommendedMinPrice.Value.ToString("F2") - €@product.RecommendedMaxPrice.Value.ToString("F2")
title="Applica limiti a tutte le aste di questo prodotto"> </small>
<i class="bi bi-check2-circle"></i> Applica
</button>
} }
else else
{ {
<small class="text-muted">N/D</small> <small class="text-muted">N/D</small>
} }
</td> </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> </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> </tbody>
</table> </table>
@@ -421,20 +587,202 @@
</div> </div>
</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> </div>
@code { @code {
private bool isLoading = true; private bool isLoading = true;
private bool isDeletingProduct = false;
private List<AuctionResultExtended>? recentAuctions; private List<AuctionResultExtended>? recentAuctions;
private List<AuctionResultExtended>? filteredAuctions; private List<AuctionResultExtended>? filteredAuctions;
private List<ProductStatisticsRecord>? products; private List<ProductStatisticsRecord>? products;
private List<ProductStatisticsRecord>? filteredProducts;
// Filtri e ordinamento // Filtri e ordinamento aste
private string filterName = ""; private string filterName = "";
private string filterWon = ""; private string filterWon = "";
private AuctionResultExtended? selectedAuctionDetail; 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() protected override async Task OnInitializedAsync()
{ {
await RefreshStats(); await RefreshStats();
@@ -453,6 +801,7 @@ private AuctionResultExtended? selectedAuctionDetail;
// Carica prodotti con statistiche // Carica prodotti con statistiche
products = await DatabaseService.GetAllProductStatisticsAsync(); products = await DatabaseService.GetAllProductStatisticsAsync();
ApplyProductFilter();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -548,6 +897,30 @@ private AuctionResultExtended? selectedAuctionDetail;
selectedAuctionDetail = auction; 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) private string GetHeatBadgeClass(int heat)
{ {
if (heat < 30) return "bg-success"; if (heat < 30) return "bg-success";
@@ -555,6 +928,13 @@ private AuctionResultExtended? selectedAuctionDetail;
return "bg-danger"; 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) private string TruncateName(string name, int maxLength)
{ {
if (string.IsNullOrEmpty(name)) return "-"; if (string.IsNullOrEmpty(name)) return "-";
@@ -610,6 +990,285 @@ private AuctionResultExtended? selectedAuctionDetail;
await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}"); 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> <style>
@@ -631,7 +1290,25 @@ private AuctionResultExtended? selectedAuctionDetail;
background-color: rgba(0,123,255,0.1) !important; 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 { .table-success-subtle {
background-color: rgba(25, 135, 84, 0.1); 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> </style>

View File

@@ -428,6 +428,22 @@ namespace AutoBidder.Services
auction.AddLog($"[ASTA TERMINATA] {statusMsg}"); auction.AddLog($"[ASTA TERMINATA] {statusMsg}");
OnLog?.Invoke($"[FINE] [{auction.AuctionId}] Asta {statusMsg} - Polling fermato"); 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 auction.BidHistory.Add(new BidHistory
{ {
Timestamp = DateTime.UtcNow, Timestamp = DateTime.UtcNow,
@@ -531,7 +547,7 @@ namespace AutoBidder.Services
{ {
var settings = SettingsManager.Load(); var settings = SettingsManager.Load();
// Offset: millisecondi prima della scadenza // Offset: millisecondi prima della scadenza (configurato dall'utente)
int offsetMs = auction.BidBeforeDeadlineMs > 0 int offsetMs = auction.BidBeforeDeadlineMs > 0
? auction.BidBeforeDeadlineMs ? auction.BidBeforeDeadlineMs
: settings.DefaultBidBeforeDeadlineMs; : settings.DefaultBidBeforeDeadlineMs;
@@ -542,42 +558,53 @@ namespace AutoBidder.Services
// Skip se già vincitore o timer scaduto // Skip se già vincitore o timer scaduto
if (state.IsMyBid || timerMs <= 0) return; 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 // Log timing solo se abilitato
if (settings.LogTiming) 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? // Punta quando il timer API è <= offset configurato dall'utente
if (estimatedRemaining > offsetMs) return; // Troppo presto // NESSUNA modifica automatica - l'utente decide il timing
if (estimatedRemaining < -200) return; // Troppo tardi if (timerMs > offsetMs)
{
return;
}
// Protezione doppia puntata // Timer <= offset = È IL MOMENTO DI PUNTARE!
if (auction.BidScheduled) return; auction.AddLog($"[BID WINDOW] Timer={timerMs:F0}ms <= Offset={offsetMs}ms - Verifica condizioni...");
// Cooldown 1 secondo // Resetta BidScheduled se il timer è AUMENTATO (qualcun altro ha puntato = nuovo ciclo)
if (auction.LastClickAt.HasValue && (DateTime.UtcNow - auction.LastClickAt.Value).TotalMilliseconds < 1000) return; 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) // 🔴 CONTROLLI FONDAMENTALI (prezzo, reset, limiti, puntate residue)
if (!ShouldBid(auction, state)) if (!ShouldBid(auction, state))
{ {
// I motivi vengono ora loggati sempre dentro ShouldBid
return; return;
} }
@@ -587,19 +614,25 @@ namespace AutoBidder.Services
if (!decision.ShouldBid) 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) if (settings.LogStrategyDecisions)
{ {
auction.AddLog($"[STRATEGY] {decision.Reason}"); OnLog?.Invoke($"[{auction.Name}] STRATEGY blocked: {decision.Reason}");
} }
return; return;
} }
// ?? PUNTA! // ?? PUNTA!
auction.BidScheduled = true; auction.BidScheduled = true;
auction.LastScheduledTimerMs = timerMs;
if (settings.LogBids) 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); await ExecuteBid(auction, state, token);
@@ -649,12 +682,15 @@ namespace AutoBidder.Services
if (result.Success) 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"); OnLog?.Invoke($"[OK] Puntata riuscita su {auction.Name} ({auction.AuctionId}): {result.LatencyMs}ms");
} }
else 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}"); OnLog?.Invoke($"[FAIL] Puntata fallita su {auction.Name} ({auction.AuctionId}): {result.Error}");
} }
@@ -667,7 +703,7 @@ namespace AutoBidder.Services
Timer = state.Timer, Timer = state.Timer,
LatencyMs = result.LatencyMs, LatencyMs = result.LatencyMs,
Success = result.Success, 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) catch (Exception ex)
@@ -698,6 +734,7 @@ namespace AutoBidder.Services
if (auction.CalculatedValue.SavingsPercentage.HasValue && if (auction.CalculatedValue.SavingsPercentage.HasValue &&
auction.CalculatedValue.SavingsPercentage.Value < settings.MinSavingsPercentage) 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"); auction.AddLog($"[VALUE] Puntata bloccata: risparmio {auction.CalculatedValue.SavingsPercentage.Value:F1}% < {settings.MinSavingsPercentage:F1}% richiesto");
return false; return false;
} }
@@ -705,11 +742,13 @@ namespace AutoBidder.Services
if (settings.LogTiming && settings.ValueCheckEnabled) if (settings.LogTiming && settings.ValueCheckEnabled)
{ {
auction.AddLog($"[DEBUG] ? Controllo convenienza OK"); auction.AddLog($"[DEBUG] Controllo convenienza OK");
} }
// ?? CONTROLLO ANTI-COLLISIONE: Rileva aste troppo "affollate" // ?? CONTROLLO ANTI-COLLISIONE (OPZIONALE): Rileva aste troppo "affollate"
// Se negli ultimi 10 secondi ci sono state 3+ puntate di utenti diversi, evita // DISABILITATO DI DEFAULT - può far perdere aste competitive!
if (settings.HardcodedAntiCollisionEnabled)
{
var recentBidsThreshold = 10; // secondi var recentBidsThreshold = 10; // secondi
var maxActiveBidders = 3; // se 3+ bidder attivi, potrebbe essere troppo affollata var maxActiveBidders = 3; // se 3+ bidder attivi, potrebbe essere troppo affollata
@@ -745,6 +784,7 @@ namespace AutoBidder.Services
} }
} }
catch { /* Ignora errori nel controllo competizione */ } catch { /* Ignora errori nel controllo competizione */ }
}
if (settings.LogTiming) if (settings.LogTiming)
{ {
@@ -757,13 +797,14 @@ namespace AutoBidder.Services
var session = _apiClient.GetSession(); var session = _apiClient.GetSession();
if (session != null && session.RemainingBids <= settings.MinimumRemainingBids) if (session != null && session.RemainingBids <= settings.MinimumRemainingBids)
{ {
// 🔥 Logga SEMPRE - è un blocco importante
auction.AddLog($"[LIMIT] Puntata bloccata: puntate residue ({session.RemainingBids}) <= limite ({settings.MinimumRemainingBids})"); auction.AddLog($"[LIMIT] Puntata bloccata: puntate residue ({session.RemainingBids}) <= limite ({settings.MinimumRemainingBids})");
return false; return false;
} }
if (settings.LogTiming && session != null) 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 // ?? CONTROLLO 3: MinPrice/MaxPrice
if (auction.MinPrice > 0 && state.Price < auction.MinPrice) 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}"); auction.AddLog($"[PRICE] Prezzo troppo basso: €{state.Price:F2} < Min €{auction.MinPrice:F2}");
return false; return false;
} }
if (auction.MaxPrice > 0 && state.Price > auction.MaxPrice) 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}"); auction.AddLog($"[PRICE] Prezzo troppo alto: €{state.Price:F2} > Max €{auction.MaxPrice:F2}");
return false; return false;
} }
@@ -801,12 +844,14 @@ namespace AutoBidder.Services
// ?? CONTROLLO 4: MinResets/MaxResets // ?? CONTROLLO 4: MinResets/MaxResets
if (auction.MinResets > 0 && auction.ResetCount < auction.MinResets) 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}"); auction.AddLog($"[RESET] Reset troppo bassi: {auction.ResetCount} < Min {auction.MinResets}");
return false; return false;
} }
if (auction.MaxResets > 0 && auction.ResetCount >= auction.MaxResets) 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}"); auction.AddLog($"[RESET] Reset massimi raggiunti: {auction.ResetCount} >= Max {auction.MaxResets}");
return false; return false;
} }

View File

@@ -732,6 +732,21 @@ namespace AutoBidder.Services
await using var cmd = conn.CreateCommand(); await using var cmd = conn.CreateCommand();
cmd.CommandText = sql; cmd.CommandText = sql;
await cmd.ExecuteNonQueryAsync(); 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, AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
AvgResets, MinResets, MaxResets, AvgResets, MinResets, MaxResets,
RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids, RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs,
HourlyStatsJson, LastUpdated) HourlyStatsJson, LastUpdated)
VALUES (@productKey, @productName, @totalAuctions, @wonAuctions, @lostAuctions, VALUES (@productKey, @productName, @totalAuctions, @wonAuctions, @lostAuctions,
@avgFinalPrice, @minFinalPrice, @maxFinalPrice, @avgFinalPrice, @minFinalPrice, @maxFinalPrice,
@avgBidsToWin, @minBidsToWin, @maxBidsToWin, @avgBidsToWin, @minBidsToWin, @maxBidsToWin,
@avgResets, @minResets, @maxResets, @avgResets, @minResets, @maxResets,
@recMinPrice, @recMaxPrice, @recMinResets, @recMaxResets, @recMaxBids, @recMinPrice, @recMaxPrice, @recMinResets, @recMaxResets, @recMaxBids,
@userMinPrice, @userMaxPrice, @userMinResets, @userMaxResets, @userMaxBids, @userBidDeadline,
@hourlyJson, @lastUpdated) @hourlyJson, @lastUpdated)
ON CONFLICT(ProductKey) DO UPDATE SET ON CONFLICT(ProductKey) DO UPDATE SET
ProductName = @productName, ProductName = @productName,
@@ -1433,6 +1450,12 @@ namespace AutoBidder.Services
RecommendedMinResets = @recMinResets, RecommendedMinResets = @recMinResets,
RecommendedMaxResets = @recMaxResets, RecommendedMaxResets = @recMaxResets,
RecommendedMaxBids = @recMaxBids, 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, HourlyStatsJson = @hourlyJson,
LastUpdated = @lastUpdated; LastUpdated = @lastUpdated;
"; ";
@@ -1457,6 +1480,12 @@ namespace AutoBidder.Services
new SqliteParameter("@recMinResets", (object?)stats.RecommendedMinResets ?? DBNull.Value), new SqliteParameter("@recMinResets", (object?)stats.RecommendedMinResets ?? DBNull.Value),
new SqliteParameter("@recMaxResets", (object?)stats.RecommendedMaxResets ?? DBNull.Value), new SqliteParameter("@recMaxResets", (object?)stats.RecommendedMaxResets ?? DBNull.Value),
new SqliteParameter("@recMaxBids", (object?)stats.RecommendedMaxBids ?? 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("@hourlyJson", (object?)stats.HourlyStatsJson ?? DBNull.Value),
new SqliteParameter("@lastUpdated", DateTime.UtcNow.ToString("O")) new SqliteParameter("@lastUpdated", DateTime.UtcNow.ToString("O"))
); );
@@ -1473,6 +1502,7 @@ namespace AutoBidder.Services
AvgBidsToWin, MinBidsToWin, MaxBidsToWin, AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
AvgResets, MinResets, MaxResets, AvgResets, MinResets, MaxResets,
RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids, RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs,
HourlyStatsJson, LastUpdated HourlyStatsJson, LastUpdated
FROM ProductStatistics FROM ProductStatistics
WHERE ProductKey = @productKey; WHERE ProductKey = @productKey;
@@ -1507,8 +1537,14 @@ namespace AutoBidder.Services
RecommendedMinResets = reader.IsDBNull(16) ? null : reader.GetInt32(16), RecommendedMinResets = reader.IsDBNull(16) ? null : reader.GetInt32(16),
RecommendedMaxResets = reader.IsDBNull(17) ? null : reader.GetInt32(17), RecommendedMaxResets = reader.IsDBNull(17) ? null : reader.GetInt32(17),
RecommendedMaxBids = reader.IsDBNull(18) ? null : reader.GetInt32(18), RecommendedMaxBids = reader.IsDBNull(18) ? null : reader.GetInt32(18),
HourlyStatsJson = reader.IsDBNull(19) ? null : reader.GetString(19), UserDefaultMinPrice = reader.IsDBNull(19) ? null : reader.GetDouble(19),
LastUpdated = reader.GetString(20) 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, AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
AvgResets, MinResets, MaxResets, AvgResets, MinResets, MaxResets,
RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids, RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs,
HourlyStatsJson, LastUpdated HourlyStatsJson, LastUpdated
FROM ProductStatistics FROM ProductStatistics
ORDER BY TotalAuctions DESC; ORDER BY TotalAuctions DESC;
@@ -1593,14 +1630,67 @@ namespace AutoBidder.Services
RecommendedMinResets = reader.IsDBNull(16) ? null : reader.GetInt32(16), RecommendedMinResets = reader.IsDBNull(16) ? null : reader.GetInt32(16),
RecommendedMaxResets = reader.IsDBNull(17) ? null : reader.GetInt32(17), RecommendedMaxResets = reader.IsDBNull(17) ? null : reader.GetInt32(17),
RecommendedMaxBids = reader.IsDBNull(18) ? null : reader.GetInt32(18), RecommendedMaxBids = reader.IsDBNull(18) ? null : reader.GetInt32(18),
HourlyStatsJson = reader.IsDBNull(19) ? null : reader.GetString(19), UserDefaultMinPrice = reader.IsDBNull(19) ? null : reader.GetDouble(19),
LastUpdated = reader.GetString(20) 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; 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) private AuctionResultExtended ReadAuctionResultExtended(Microsoft.Data.Sqlite.SqliteDataReader reader)
{ {
return new AuctionResultExtended return new AuctionResultExtended

View File

@@ -1,6 +1,8 @@
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthenticationStateProvider @inject AuthenticationStateProvider AuthenticationStateProvider
@inject AuctionMonitor AuctionMonitor
@implements IDisposable
<div class="nav-sidebar"> <div class="nav-sidebar">
<div class="nav-header"> <div class="nav-header">
@@ -36,11 +38,32 @@
</div> </div>
<div class="nav-footer"> <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> <AuthorizeView>
<Authorized> <Authorized>
<div class="user-badge"> <div class="user-badge @(string.IsNullOrEmpty(sessionUsername) ? "disconnected" : "connected")">
<i class="bi bi-person-circle"></i> <i class="bi bi-person-circle"></i>
<span>@context.User.Identity?.Name</span> <span>@(string.IsNullOrEmpty(sessionUsername) ? "Non connesso" : sessionUsername)</span>
</div> </div>
<a href="/Account/Logout" class="nav-menu-item logout-item"> <a href="/Account/Logout" class="nav-menu-item logout-item">
<i class="bi bi-box-arrow-right"></i> <i class="bi bi-box-arrow-right"></i>
@@ -52,6 +75,52 @@
</nav> </nav>
</div> </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> <style>
.nav-sidebar { .nav-sidebar {
display: flex; display: flex;
@@ -150,6 +219,52 @@
border-top: 1px solid rgba(255, 255, 255, 0.06); 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 { .user-badge {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -162,6 +277,15 @@
font-size: 0.875rem; 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 { .user-badge i {
font-size: 1.25rem; font-size: 1.25rem;
} }

View File

@@ -147,6 +147,13 @@ namespace AutoBidder.Utilities
/// </summary> /// </summary>
public bool LogErrors { get; set; } = true; 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> /// <summary>
/// Log stato asta (terminata, reset, ecc.) [STATUS] /// Log stato asta (terminata, reset, ecc.) [STATUS]
/// Default: true /// Default: true
@@ -203,6 +210,14 @@ namespace AutoBidder.Utilities
/// </summary> /// </summary>
public double MinSavingsPercentage { get; set; } = -5.0; 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 // RILEVAMENTO COMPETIZIONE E HEAT METRIC
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥 // 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥

View File

@@ -1,5 +1,423 @@
/* === MODERN PAGE STYLES (append to app-wpf.css) === */ /* === 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 */ /* ✅ NUOVO: Stili per selezione riga e colonna puntate */
.table-hover tbody tr { .table-hover tbody tr {
cursor: pointer; cursor: pointer;
@@ -712,3 +1130,52 @@
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%) !important; background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%) !important;
color: #e5e5e5 !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;
}