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:
@@ -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);
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
|
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user