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]
|
||||
public bool BidScheduled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timer per cui è stata schedulata l'ultima puntata.
|
||||
/// Usato per evitare doppie puntate sullo stesso ciclo.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public double LastScheduledTimerMs { get; set; }
|
||||
|
||||
// Storico
|
||||
public List<BidHistory> BidHistory { get; set; } = new List<BidHistory>();
|
||||
public Dictionary<string, BidderInfo> BidderStats { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@@ -35,6 +35,14 @@ namespace AutoBidder.Models
|
||||
public int? RecommendedMaxResets { get; set; }
|
||||
public int? RecommendedMaxBids { get; set; }
|
||||
|
||||
// Valori di default definiti dall'utente (editabili)
|
||||
public double? UserDefaultMinPrice { get; set; }
|
||||
public double? UserDefaultMaxPrice { get; set; }
|
||||
public int? UserDefaultMinResets { get; set; }
|
||||
public int? UserDefaultMaxResets { get; set; }
|
||||
public int? UserDefaultMaxBids { get; set; }
|
||||
public int? UserDefaultBidBeforeDeadlineMs { get; set; }
|
||||
|
||||
// JSON con statistiche per fascia oraria
|
||||
public string? HourlyStatsJson { get; set; }
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -700,6 +700,94 @@ namespace AutoBidder.Pages
|
||||
{
|
||||
return auctions.Any(a => !a.IsActive);
|
||||
}
|
||||
|
||||
// ???????????????????????????????????????????????????????????????????
|
||||
// RIMOZIONE ASTE PER STATO
|
||||
// ???????????????????????????????????????????????????????????????????
|
||||
|
||||
private async Task RemoveActiveAuctions()
|
||||
{
|
||||
await RemoveAuctionsByCondition(
|
||||
a => a.IsActive && !a.IsPaused && (a.LastState == null || a.LastState.Status == AuctionStatus.Running),
|
||||
"attive",
|
||||
GetActiveAuctionsCount()
|
||||
);
|
||||
}
|
||||
|
||||
private async Task RemovePausedAuctions()
|
||||
{
|
||||
await RemoveAuctionsByCondition(
|
||||
a => a.IsPaused || (a.LastState != null && a.LastState.Status == AuctionStatus.Paused),
|
||||
"in pausa",
|
||||
GetPausedAuctionsCount()
|
||||
);
|
||||
}
|
||||
|
||||
private async Task RemoveStoppedAuctions()
|
||||
{
|
||||
await RemoveAuctionsByCondition(
|
||||
a => !a.IsActive && (a.LastState == null || (a.LastState.Status != AuctionStatus.EndedWon && a.LastState.Status != AuctionStatus.EndedLost)),
|
||||
"fermate",
|
||||
GetStoppedAuctionsCount()
|
||||
);
|
||||
}
|
||||
|
||||
private async Task RemoveWonAuctions()
|
||||
{
|
||||
await RemoveAuctionsByCondition(
|
||||
a => a.LastState != null && a.LastState.Status == AuctionStatus.EndedWon,
|
||||
"vinte",
|
||||
GetWonAuctionsCount()
|
||||
);
|
||||
}
|
||||
|
||||
private async Task RemoveLostAuctions()
|
||||
{
|
||||
await RemoveAuctionsByCondition(
|
||||
a => a.LastState != null && a.LastState.Status == AuctionStatus.EndedLost,
|
||||
"perse",
|
||||
GetLostAuctionsCount()
|
||||
);
|
||||
}
|
||||
|
||||
private async Task RemoveAuctionsByCondition(Func<AuctionInfo, bool> condition, string stateLabel, int count)
|
||||
{
|
||||
if (count == 0) return;
|
||||
|
||||
var confirmed = await JSRuntime.InvokeAsync<bool>("confirm",
|
||||
$"Rimuovere {count} aste {stateLabel}?");
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try
|
||||
{
|
||||
var toRemove = auctions.Where(condition).ToList();
|
||||
int removed = 0;
|
||||
|
||||
foreach (var auction in toRemove)
|
||||
{
|
||||
AuctionMonitor.RemoveAuction(auction.AuctionId);
|
||||
AppState.RemoveAuction(auction);
|
||||
removed++;
|
||||
}
|
||||
|
||||
if (selectedAuction != null && condition(selectedAuction))
|
||||
{
|
||||
selectedAuction = null;
|
||||
}
|
||||
|
||||
SaveAuctions();
|
||||
AddLog($"[CLEANUP] Rimosse {removed} aste {stateLabel}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AddLog($"Errore rimozione: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ???????????????????????????????????????????????????????????????????
|
||||
// SPLITTER RESIZE (gestito via JS)
|
||||
// ???????????????????????????????????????????????????????????????????
|
||||
|
||||
private async Task RemoveSelectedAuctionWithConfirm()
|
||||
{
|
||||
@@ -1150,11 +1238,6 @@ namespace AutoBidder.Pages
|
||||
var latency = auction.PollingLatencyMs;
|
||||
if (latency <= 0) return "-";
|
||||
|
||||
// Colora in base al ping
|
||||
var cssClass = latency < 100 ? "text-success" :
|
||||
latency < 300 ? "text-warning" :
|
||||
"text-danger";
|
||||
|
||||
return $"{latency}ms";
|
||||
}
|
||||
catch
|
||||
@@ -1162,6 +1245,25 @@ namespace AutoBidder.Pages
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -1366,5 +1468,41 @@ namespace AutoBidder.Pages
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// ???????????????????????????????????????????????????????????????????
|
||||
// METODI CONTEGGIO STATO ASTE
|
||||
// ???????????????????????????????????????????????????????????????????
|
||||
|
||||
private int GetActiveAuctionsCount()
|
||||
{
|
||||
return auctions.Count(a => a.IsActive && !a.IsPaused &&
|
||||
(a.LastState == null || a.LastState.Status == AuctionStatus.Running));
|
||||
}
|
||||
|
||||
private int GetPausedAuctionsCount()
|
||||
{
|
||||
return auctions.Count(a => a.IsPaused ||
|
||||
(a.LastState != null && a.LastState.Status == AuctionStatus.Paused));
|
||||
}
|
||||
|
||||
private int GetWonAuctionsCount()
|
||||
{
|
||||
return auctions.Count(a => a.LastState != null &&
|
||||
a.LastState.Status == AuctionStatus.EndedWon);
|
||||
}
|
||||
|
||||
private int GetLostAuctionsCount()
|
||||
{
|
||||
return auctions.Count(a => a.LastState != null &&
|
||||
a.LastState.Status == AuctionStatus.EndedLost);
|
||||
}
|
||||
|
||||
private int GetStoppedAuctionsCount()
|
||||
{
|
||||
return auctions.Count(a => !a.IsActive &&
|
||||
(a.LastState == null ||
|
||||
(a.LastState.Status != AuctionStatus.EndedWon &&
|
||||
a.LastState.Status != AuctionStatus.EndedLost)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,37 +133,92 @@
|
||||
</h2>
|
||||
<div id="collapse-defaults" class="accordion-collapse collapse" aria-labelledby="heading-defaults" data-bs-parent="#settingsAccordion">
|
||||
<div class="accordion-body">
|
||||
<div class="alert alert-info border-0 shadow-sm mb-3">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
Usa i pulsanti <i class="bi bi-arrow-repeat"></i> per applicare la singola impostazione a tutte le aste
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-bold"><i class="bi bi-speedometer2"></i> Anticipo puntata (ms)</label>
|
||||
<input type="number" class="form-control" @bind="settings.DefaultBidBeforeDeadlineMs" />
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" @bind="settings.DefaultBidBeforeDeadlineMs" />
|
||||
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultBidBeforeDeadlineMs))"
|
||||
disabled="@applyingSettings.Contains(nameof(settings.DefaultBidBeforeDeadlineMs))"
|
||||
title="Applica a tutte le aste">
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-bold"><i class="bi bi-hand-index-thumb"></i> Click massimi</label>
|
||||
<input type="number" class="form-control" @bind="settings.DefaultMaxClicks" />
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" @bind="settings.DefaultMaxClicks" />
|
||||
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMaxClicks))"
|
||||
disabled="@applyingSettings.Contains(nameof(settings.DefaultMaxClicks))"
|
||||
title="Applica a tutte le aste">
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text">0 = illimitati</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-bold"><i class="bi bi-currency-euro"></i> Prezzo minimo (€)</label>
|
||||
<input type="number" step="0.01" class="form-control" @bind="settings.DefaultMinPrice" />
|
||||
<div class="input-group">
|
||||
<input type="number" step="0.01" class="form-control" @bind="settings.DefaultMinPrice" />
|
||||
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMinPrice))"
|
||||
disabled="@applyingSettings.Contains(nameof(settings.DefaultMinPrice))"
|
||||
title="Applica a tutte le aste">
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-bold"><i class="bi bi-currency-euro"></i> Prezzo massimo (€)</label>
|
||||
<input type="number" step="0.01" class="form-control" @bind="settings.DefaultMaxPrice" />
|
||||
<div class="input-group">
|
||||
<input type="number" step="0.01" class="form-control" @bind="settings.DefaultMaxPrice" />
|
||||
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMaxPrice))"
|
||||
disabled="@applyingSettings.Contains(nameof(settings.DefaultMaxPrice))"
|
||||
title="Applica a tutte le aste">
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-bold"><i class="bi bi-arrow-repeat"></i> Reset minimi</label>
|
||||
<input type="number" class="form-control" @bind="settings.DefaultMinResets" />
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" @bind="settings.DefaultMinResets" />
|
||||
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMinResets))"
|
||||
disabled="@applyingSettings.Contains(nameof(settings.DefaultMinResets))"
|
||||
title="Applica a tutte le aste">
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-bold"><i class="bi bi-arrow-repeat"></i> Reset massimi</label>
|
||||
<input type="number" class="form-control" @bind="settings.DefaultMaxResets" />
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control" @bind="settings.DefaultMaxResets" />
|
||||
<button class="btn btn-outline-primary" @onclick="() => ApplySingleSettingToAll(nameof(settings.DefaultMaxResets))"
|
||||
disabled="@applyingSettings.Contains(nameof(settings.DefaultMaxResets))"
|
||||
title="Applica a tutte le aste">
|
||||
<i class="bi bi-arrow-repeat"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label fw-bold"><i class="bi bi-shield-check"></i> Puntate minime da mantenere</label>
|
||||
<input type="number" class="form-control" @bind="settings.MinimumRemainingBids" />
|
||||
<div class="form-text">Questa è un'impostazione globale</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(singleSettingMessage))
|
||||
{
|
||||
<div class="alert alert-success mt-3 mb-0 fade-in">
|
||||
<i class="bi bi-check-circle me-2"></i>@singleSettingMessage
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-success" @onclick="SaveSettings"><i class="bi bi-check-lg"></i> Salva</button>
|
||||
@@ -689,6 +744,21 @@
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 0.925rem;
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
@@ -706,6 +776,10 @@ private bool isApplyingToAll = false;
|
||||
private string? applyToAllMessage = null;
|
||||
private bool applyToAllSuccess = false;
|
||||
|
||||
// Applica singole impostazioni
|
||||
private HashSet<string> applyingSettings = new();
|
||||
private string? singleSettingMessage = null;
|
||||
|
||||
private AutoBidder.Utilities.AppSettings settings = new();
|
||||
private System.Threading.Timer? updateTimer;
|
||||
|
||||
@@ -780,6 +854,75 @@ private System.Threading.Timer? updateTimer;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
<tbody>
|
||||
@foreach (var auction in filteredAuctions)
|
||||
{
|
||||
<tr class="@(auction.Won ? "table-success-subtle" : "") auction-row"
|
||||
<tr class="@(auction.Won ? "table-success-subtle" : "") @(selectedAuctionDetail?.Id == auction.Id ? "table-info" : "") auction-row"
|
||||
@onclick="() => SelectAuction(auction)">
|
||||
<td>
|
||||
<small class="fw-bold">@TruncateName(auction.AuctionName, 30)</small>
|
||||
@@ -200,15 +200,47 @@
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-box-seam me-2"></i>
|
||||
Prodotti Salvati
|
||||
Prodotti Salvati (@(filteredProducts?.Count ?? 0))
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<!-- FILTRO PRODOTTI -->
|
||||
<div class="card-body border-bottom py-2">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-md-8">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||||
<input type="text" class="form-control" placeholder="Cerca prodotto..."
|
||||
@bind="filterProductName" @bind:event="oninput" @onkeyup="ApplyProductFilter" />
|
||||
@if (!string.IsNullOrEmpty(filterProductName))
|
||||
{
|
||||
<button class="btn btn-outline-secondary" @onclick="ClearProductFilter">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 text-muted small d-flex align-items-center">
|
||||
<i class="bi bi-info-circle me-1"></i> Clicca intestazioni per ordinare
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-0">
|
||||
@if (products == null || !products.Any())
|
||||
@if (filteredProducts == null || !filteredProducts.Any())
|
||||
{
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="bi bi-inbox" style="font-size: 3rem;"></i>
|
||||
<p class="mt-3">Nessun prodotto salvato</p>
|
||||
<p class="mt-3">
|
||||
@if (!string.IsNullOrEmpty(filterProductName))
|
||||
{
|
||||
<span>Nessun prodotto trovato per "@filterProductName"</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Nessun prodotto salvato</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
@@ -217,25 +249,47 @@
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th>Prodotto</th>
|
||||
<th class="text-center">Aste</th>
|
||||
<th class="text-center">Win%</th>
|
||||
<th class="text-end">Limiti €</th>
|
||||
<th class="sortable-header" @onclick='() => SortProductsBy("name")'>
|
||||
Prodotto @GetProductSortIndicator("name")
|
||||
</th>
|
||||
<th class="text-center sortable-header" @onclick='() => SortProductsBy("auctions")'>
|
||||
Aste @GetProductSortIndicator("auctions")
|
||||
</th>
|
||||
<th class="text-center sortable-header" @onclick='() => SortProductsBy("winrate")'>
|
||||
Win% @GetProductSortIndicator("winrate")
|
||||
</th>
|
||||
<th class="text-end sortable-header" @onclick='() => SortProductsBy("avgprice")'>
|
||||
Prezzo Medio @GetProductSortIndicator("avgprice")
|
||||
</th>
|
||||
<th class="text-end">Range Storico</th>
|
||||
<th class="text-end">Limiti Consigliati</th>
|
||||
<th class="text-center">Azioni</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var product in products)
|
||||
@foreach (var product in filteredProducts)
|
||||
{
|
||||
var winRate = product.TotalAuctions > 0
|
||||
? (product.WonAuctions * 100.0 / product.TotalAuctions)
|
||||
: 0;
|
||||
var isEditing = editingProductKey == product.ProductKey;
|
||||
|
||||
<tr>
|
||||
<tr class="product-row @(selectedProduct?.ProductKey == product.ProductKey ? "table-info" : "")"
|
||||
@onclick="() => SelectProduct(product)">
|
||||
<td>
|
||||
<small class="fw-bold">@product.ProductName</small>
|
||||
<br/>
|
||||
<small class="text-muted">@product.TotalAuctions totali (@product.WonAuctions vinte)</small>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button class="btn btn-sm btn-outline-secondary"
|
||||
@onclick="() => ToggleEditProduct(product)"
|
||||
@onclick:stopPropagation="true"
|
||||
title="@(isEditing ? "Chiudi editor" : "Modifica limiti default")">
|
||||
<i class="bi bi-@(isEditing ? "chevron-up" : "chevron-down")"></i>
|
||||
</button>
|
||||
<div>
|
||||
<small class="fw-bold">@product.ProductName</small>
|
||||
<br/>
|
||||
<small class="text-muted">@product.TotalAuctions totali (@product.WonAuctions vinte)</small>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center fw-bold">
|
||||
@product.TotalAuctions
|
||||
@@ -246,10 +300,13 @@
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
@if (product.RecommendedMinPrice.HasValue && product.RecommendedMaxPrice.HasValue)
|
||||
<span class="fw-bold text-primary">€@product.AvgFinalPrice.ToString("F2")</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
@if (product.MinFinalPrice.HasValue && product.MaxFinalPrice.HasValue)
|
||||
{
|
||||
<small class="text-muted">
|
||||
@product.RecommendedMinPrice.Value.ToString("F2") - @product.RecommendedMaxPrice.Value.ToString("F2")
|
||||
€@product.MinFinalPrice.Value.ToString("F2") - €@product.MaxFinalPrice.Value.ToString("F2")
|
||||
</small>
|
||||
}
|
||||
else
|
||||
@@ -257,21 +314,130 @@
|
||||
<small class="text-muted">-</small>
|
||||
}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<td class="text-end">
|
||||
@if (product.RecommendedMinPrice.HasValue && product.RecommendedMaxPrice.HasValue)
|
||||
{
|
||||
<button class="btn btn-sm btn-primary"
|
||||
@onclick="() => ApplyLimitsToProduct(product)"
|
||||
title="Applica limiti a tutte le aste di questo prodotto">
|
||||
<i class="bi bi-check2-circle"></i> Applica
|
||||
</button>
|
||||
<small class="text-success fw-bold">
|
||||
€@product.RecommendedMinPrice.Value.ToString("F2") - €@product.RecommendedMaxPrice.Value.ToString("F2")
|
||||
</small>
|
||||
}
|
||||
else
|
||||
{
|
||||
<small class="text-muted">N/D</small>
|
||||
}
|
||||
</td>
|
||||
<td class="text-center" @onclick:stopPropagation="true">
|
||||
<div class="d-flex gap-1 justify-content-center">
|
||||
@if (product.RecommendedMinPrice.HasValue && product.RecommendedMaxPrice.HasValue)
|
||||
{
|
||||
<button class="btn btn-sm btn-primary"
|
||||
@onclick="() => ApplyLimitsToProduct(product)"
|
||||
title="Applica limiti a tutte le aste di questo prodotto">
|
||||
<i class="bi bi-check2-circle"></i>
|
||||
</button>
|
||||
}
|
||||
<button class="btn btn-sm btn-danger"
|
||||
@onclick="() => DeleteProduct(product)"
|
||||
title="Elimina questo prodotto dalle statistiche"
|
||||
disabled="@isDeletingProduct">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- RIGA ESPANDIBILE PER EDITING LIMITI DEFAULT -->
|
||||
@if (isEditing)
|
||||
{
|
||||
<tr class="table-light">
|
||||
<td colspan="7" @onclick:stopPropagation="true">
|
||||
<div class="p-3 border rounded bg-white">
|
||||
<h6 class="text-primary mb-3">
|
||||
<i class="bi bi-sliders me-2"></i>
|
||||
Limiti Default per: @product.ProductName
|
||||
</h6>
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- Colonna 1: Prezzi -->
|
||||
<div class="col-md-4">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small fw-bold">Prezzo Minimo €</label>
|
||||
<input type="number" step="0.01" class="form-control form-control-sm"
|
||||
@bind="tempUserDefaultMinPrice" placeholder="es. @product.RecommendedMinPrice?.ToString("F2")" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small fw-bold">Prezzo Massimo €</label>
|
||||
<input type="number" step="0.01" class="form-control form-control-sm"
|
||||
@bind="tempUserDefaultMaxPrice" placeholder="es. @product.RecommendedMaxPrice?.ToString("F2")" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Colonna 2: Reset e Puntate -->
|
||||
<div class="col-md-4">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small fw-bold">Reset Minimo</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="tempUserDefaultMinResets" placeholder="es. @product.RecommendedMinResets" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small fw-bold">Reset Massimo</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="tempUserDefaultMaxResets" placeholder="es. @product.RecommendedMaxResets" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small fw-bold">Max Puntate</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="tempUserDefaultMaxBids" placeholder="es. @product.RecommendedMaxBids" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Colonna 3: Timing -->
|
||||
<div class="col-md-4">
|
||||
<div class="mb-2">
|
||||
<label class="form-label small fw-bold">Anticipo Puntata (ms)</label>
|
||||
<input type="number" class="form-control form-control-sm"
|
||||
@bind="tempUserDefaultBidDeadline" placeholder="es. 200" />
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info p-2 mb-2 small">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Valori consigliati dall'algoritmo:<br/>
|
||||
<strong>€@product.RecommendedMinPrice?.ToString("F2") - €@product.RecommendedMaxPrice?.ToString("F2")</strong><br/>
|
||||
Reset: @product.RecommendedMinResets - @product.RecommendedMaxResets
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pulsanti Azioni -->
|
||||
<div class="d-flex gap-2 justify-content-end mt-3">
|
||||
<button class="btn btn-sm btn-secondary"
|
||||
@onclick="() => CopyRecommendedToTemp(product)"
|
||||
disabled="@(!product.RecommendedMinPrice.HasValue)"
|
||||
title="Copia i valori consigliati dall'algoritmo">
|
||||
<i class="bi bi-arrow-down-circle me-1"></i>
|
||||
Usa Consigliati
|
||||
</button>
|
||||
<button class="btn btn-sm btn-success"
|
||||
@onclick="() => SaveProductDefaults(product)"
|
||||
disabled="@isSavingDefaults">
|
||||
<i class="bi bi-floppy me-1"></i>
|
||||
Salva Default
|
||||
</button>
|
||||
<button class="btn btn-sm btn-primary"
|
||||
@onclick="() => ApplyDefaultsToAllAuctions(product)"
|
||||
disabled="@(!HasUserDefaults(product) || isSavingDefaults)">
|
||||
<i class="bi bi-check2-all me-1"></i>
|
||||
Applica a Tutte le Aste
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary"
|
||||
@onclick="() => CancelEditProduct()">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -421,20 +587,202 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- PANNELLO ASTE DEL PRODOTTO SELEZIONATO -->
|
||||
@if (selectedProduct != null)
|
||||
{
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-box-seam me-2"></i>
|
||||
Aste di: @selectedProduct.ProductName
|
||||
<span class="badge bg-light text-dark ms-2">@(selectedProductAuctions?.Count ?? 0) aste</span>
|
||||
</h5>
|
||||
<button class="btn btn-sm btn-outline-light" @onclick="() => { selectedProduct = null; selectedProductAuctions = null; }">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (isLoadingProductAuctions)
|
||||
{
|
||||
<div class="card-body text-center py-4">
|
||||
<div class="spinner-border text-success" role="status">
|
||||
<span class="visually-hidden">Caricamento...</span>
|
||||
</div>
|
||||
<p class="text-muted mt-2">Caricamento aste...</p>
|
||||
</div>
|
||||
}
|
||||
else if (selectedProductAuctions == null || !selectedProductAuctions.Any())
|
||||
{
|
||||
<div class="card-body text-center py-4 text-muted">
|
||||
<i class="bi bi-inbox" style="font-size: 2rem;"></i>
|
||||
<p class="mt-2">Nessuna asta trovata per questo prodotto</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive" style="max-height: 500px; overflow-y: auto;">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead class="table-light sticky-top">
|
||||
<tr>
|
||||
<th>ID Asta</th>
|
||||
<th class="text-end">Prezzo Finale</th>
|
||||
<th class="text-center">Stato</th>
|
||||
<th>Vincitore</th>
|
||||
<th class="text-end">Le Mie Puntate</th>
|
||||
<th class="text-end">Puntate Vincitore</th>
|
||||
<th class="text-center">Reset</th>
|
||||
<th class="text-center">Ora Chiusura</th>
|
||||
<th>Data</th>
|
||||
<th class="text-end">Risparmio</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var auction in selectedProductAuctions)
|
||||
{
|
||||
<tr class="@(auction.Won ? "table-success-subtle" : "")">
|
||||
<td><small class="font-monospace">@auction.AuctionId</small></td>
|
||||
<td class="text-end fw-bold">€@auction.FinalPrice.ToString("F2")</td>
|
||||
<td class="text-center">
|
||||
@if (auction.Won)
|
||||
{
|
||||
<span class="badge bg-success">? Vinta</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary">? Persa</span>
|
||||
}
|
||||
</td>
|
||||
<td><small>@(auction.WinnerUsername ?? "-")</small></td>
|
||||
<td class="text-end">
|
||||
<span class="badge bg-primary">@auction.BidsUsed</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
@if (auction.WinnerBidsUsed.HasValue)
|
||||
{
|
||||
<span class="badge bg-info text-dark">@auction.WinnerBidsUsed</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">-</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
@if (auction.TotalResets.HasValue)
|
||||
{
|
||||
<span class="badge @GetResetBadgeClass(auction.TotalResets.Value)">
|
||||
@auction.TotalResets
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">-</span>
|
||||
}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
@if (auction.ClosedAtHour.HasValue)
|
||||
{
|
||||
<small>@auction.ClosedAtHour:00</small>
|
||||
}
|
||||
else
|
||||
{
|
||||
<small class="text-muted">-</small>
|
||||
}
|
||||
</td>
|
||||
<td><small class="text-muted">@FormatTimestamp(auction.Timestamp)</small></td>
|
||||
<td class="text-end">
|
||||
@if (auction.Savings.HasValue)
|
||||
{
|
||||
<small class="@(auction.Savings.Value > 0 ? "text-success" : "text-danger") fw-bold">
|
||||
@(auction.Savings.Value > 0 ? "+" : "")€@auction.Savings.Value.ToString("F2")
|
||||
</small>
|
||||
}
|
||||
else
|
||||
{
|
||||
<small class="text-muted">-</small>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Riepilogo Statistiche Prodotto -->
|
||||
<div class="card-footer bg-light">
|
||||
<div class="row g-3 text-center">
|
||||
<div class="col-6 col-md-3">
|
||||
<small class="text-muted d-block">Prezzo Medio</small>
|
||||
<strong class="text-primary">€@selectedProduct.AvgFinalPrice.ToString("F2")</strong>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<small class="text-muted d-block">Range Prezzi</small>
|
||||
<strong>
|
||||
@if (selectedProduct.MinFinalPrice.HasValue && selectedProduct.MaxFinalPrice.HasValue)
|
||||
{
|
||||
<span>€@selectedProduct.MinFinalPrice.Value.ToString("F2") - €@selectedProduct.MaxFinalPrice.Value.ToString("F2")</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">-</span>
|
||||
}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<small class="text-muted d-block">Media Puntate Vincita</small>
|
||||
<strong class="text-info">@selectedProduct.AvgBidsToWin.ToString("F1")</strong>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<small class="text-muted d-block">Media Reset</small>
|
||||
<strong class="text-warning">@selectedProduct.AvgResets.ToString("F1")</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool isLoading = true;
|
||||
private bool isDeletingProduct = false;
|
||||
private List<AuctionResultExtended>? recentAuctions;
|
||||
private List<AuctionResultExtended>? filteredAuctions;
|
||||
private List<ProductStatisticsRecord>? products;
|
||||
private List<ProductStatisticsRecord>? filteredProducts;
|
||||
|
||||
// Filtri e ordinamento
|
||||
// Filtri e ordinamento aste
|
||||
private string filterName = "";
|
||||
private string filterWon = "";
|
||||
private AuctionResultExtended? selectedAuctionDetail;
|
||||
|
||||
// Filtri e ordinamento prodotti
|
||||
private string filterProductName = "";
|
||||
private string productSortColumn = "name";
|
||||
private bool productSortDescending = false;
|
||||
|
||||
// Prodotto selezionato e sue aste
|
||||
private ProductStatisticsRecord? selectedProduct = null;
|
||||
private List<AuctionResultExtended>? selectedProductAuctions = null;
|
||||
private bool isLoadingProductAuctions = false;
|
||||
|
||||
// Editing limiti default prodotto
|
||||
private string? editingProductKey = null;
|
||||
private bool isSavingDefaults = false;
|
||||
private double? tempUserDefaultMinPrice;
|
||||
private double? tempUserDefaultMaxPrice;
|
||||
private int? tempUserDefaultMinResets;
|
||||
private int? tempUserDefaultMaxResets;
|
||||
private int? tempUserDefaultMaxBids;
|
||||
private int? tempUserDefaultBidDeadline;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await RefreshStats();
|
||||
@@ -453,6 +801,7 @@ private AuctionResultExtended? selectedAuctionDetail;
|
||||
|
||||
// Carica prodotti con statistiche
|
||||
products = await DatabaseService.GetAllProductStatisticsAsync();
|
||||
ApplyProductFilter();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -547,6 +896,30 @@ private AuctionResultExtended? selectedAuctionDetail;
|
||||
{
|
||||
selectedAuctionDetail = auction;
|
||||
}
|
||||
|
||||
private async Task SelectProduct(ProductStatisticsRecord product)
|
||||
{
|
||||
selectedProduct = product;
|
||||
selectedProductAuctions = null;
|
||||
isLoadingProductAuctions = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
// Carica tutte le aste per questo prodotto
|
||||
selectedProductAuctions = await DatabaseService.GetAuctionResultsByProductAsync(product.ProductKey, 1000);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[Statistics] Error loading product auctions: {ex.Message}");
|
||||
await JSRuntime.InvokeVoidAsync("alert", $"Errore caricamento aste: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
isLoadingProductAuctions = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetHeatBadgeClass(int heat)
|
||||
{
|
||||
@@ -554,6 +927,13 @@ private AuctionResultExtended? selectedAuctionDetail;
|
||||
if (heat < 60) return "bg-warning text-dark";
|
||||
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)
|
||||
{
|
||||
@@ -610,6 +990,285 @@ private AuctionResultExtended? selectedAuctionDetail;
|
||||
await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteProduct(ProductStatisticsRecord product)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Conferma eliminazione
|
||||
var confirmed = await JSRuntime.InvokeAsync<bool>("confirm",
|
||||
$"Eliminare il prodotto '{product.ProductName}'?\n\n" +
|
||||
$"Questo rimuoverà le statistiche di {product.TotalAuctions} aste.\n" +
|
||||
$"L'operazione NON può essere annullata!");
|
||||
|
||||
if (!confirmed)
|
||||
return;
|
||||
|
||||
isDeletingProduct = true;
|
||||
StateHasChanged();
|
||||
|
||||
// Elimina dal database
|
||||
var deleted = await DatabaseService.DeleteProductStatisticsAsync(product.ProductKey);
|
||||
|
||||
if (deleted > 0)
|
||||
{
|
||||
// Rimuovi dalla lista locale
|
||||
products?.Remove(product);
|
||||
ApplyProductFilter();
|
||||
|
||||
await JSRuntime.InvokeVoidAsync("alert",
|
||||
$"? Prodotto '{product.ProductName}' eliminato con successo!\n" +
|
||||
$"Rimosse {deleted} righe dal database.");
|
||||
}
|
||||
else
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("alert",
|
||||
"Nessuna riga eliminata. Il prodotto potrebbe essere già stato rimosso.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("alert", $"Errore durante eliminazione: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
isDeletingProduct = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// ???????????????????????????????????????????????????????????????
|
||||
// METODI FILTRO E ORDINAMENTO PRODOTTI
|
||||
// ???????????????????????????????????????????????????????????????
|
||||
|
||||
private void ApplyProductFilter()
|
||||
{
|
||||
if (products == null)
|
||||
{
|
||||
filteredProducts = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var filtered = products.AsEnumerable();
|
||||
|
||||
// Filtro per nome
|
||||
if (!string.IsNullOrWhiteSpace(filterProductName))
|
||||
{
|
||||
filtered = filtered.Where(p =>
|
||||
p.ProductName.Contains(filterProductName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
// Ordinamento
|
||||
filtered = productSortColumn switch
|
||||
{
|
||||
"name" => productSortDescending
|
||||
? filtered.OrderByDescending(p => p.ProductName)
|
||||
: filtered.OrderBy(p => p.ProductName),
|
||||
"auctions" => productSortDescending
|
||||
? filtered.OrderByDescending(p => p.TotalAuctions)
|
||||
: filtered.OrderBy(p => p.TotalAuctions),
|
||||
"winrate" => productSortDescending
|
||||
? filtered.OrderByDescending(p => p.TotalAuctions > 0 ? (p.WonAuctions * 100.0 / p.TotalAuctions) : 0)
|
||||
: filtered.OrderBy(p => p.TotalAuctions > 0 ? (p.WonAuctions * 100.0 / p.TotalAuctions) : 0),
|
||||
"avgprice" => productSortDescending
|
||||
? filtered.OrderByDescending(p => p.AvgFinalPrice)
|
||||
: filtered.OrderBy(p => p.AvgFinalPrice),
|
||||
_ => filtered.OrderBy(p => p.ProductName) // Default alfabetico ascendente
|
||||
};
|
||||
|
||||
filteredProducts = filtered.ToList();
|
||||
}
|
||||
|
||||
private void ClearProductFilter()
|
||||
{
|
||||
filterProductName = "";
|
||||
ApplyProductFilter();
|
||||
}
|
||||
|
||||
private void SortProductsBy(string column)
|
||||
{
|
||||
if (productSortColumn == column)
|
||||
{
|
||||
// Toggle direzione se stessa colonna
|
||||
productSortDescending = !productSortDescending;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Nuova colonna
|
||||
productSortColumn = column;
|
||||
// Default: nome alfabetico ascendente, resto discendente
|
||||
productSortDescending = column != "name";
|
||||
}
|
||||
ApplyProductFilter();
|
||||
}
|
||||
|
||||
private MarkupString GetProductSortIndicator(string column)
|
||||
{
|
||||
if (productSortColumn != column)
|
||||
return new MarkupString("<i class=\"bi bi-chevron-expand text-muted\" style=\"font-size: 0.7rem;\"></i>");
|
||||
|
||||
return productSortDescending
|
||||
? new MarkupString("<i class=\"bi bi-chevron-down\"></i>")
|
||||
: new MarkupString("<i class=\"bi bi-chevron-up\"></i>");
|
||||
}
|
||||
|
||||
// ???????????????????????????????????????????????????????????????????
|
||||
// METODI EDITING LIMITI DEFAULT PRODOTTO
|
||||
// ???????????????????????????????????????????????????????????????????
|
||||
|
||||
private void ToggleEditProduct(ProductStatisticsRecord product)
|
||||
{
|
||||
if (editingProductKey == product.ProductKey)
|
||||
{
|
||||
// Chiudi editor
|
||||
CancelEditProduct();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Apri editor
|
||||
editingProductKey = product.ProductKey;
|
||||
LoadCurrentDefaults(product);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadCurrentDefaults(ProductStatisticsRecord product)
|
||||
{
|
||||
tempUserDefaultMinPrice = product.UserDefaultMinPrice;
|
||||
tempUserDefaultMaxPrice = product.UserDefaultMaxPrice;
|
||||
tempUserDefaultMinResets = product.UserDefaultMinResets;
|
||||
tempUserDefaultMaxResets = product.UserDefaultMaxResets;
|
||||
tempUserDefaultMaxBids = product.UserDefaultMaxBids;
|
||||
tempUserDefaultBidDeadline = product.UserDefaultBidBeforeDeadlineMs;
|
||||
}
|
||||
|
||||
private void CopyRecommendedToTemp(ProductStatisticsRecord product)
|
||||
{
|
||||
tempUserDefaultMinPrice = product.RecommendedMinPrice;
|
||||
tempUserDefaultMaxPrice = product.RecommendedMaxPrice;
|
||||
tempUserDefaultMinResets = product.RecommendedMinResets;
|
||||
tempUserDefaultMaxResets = product.RecommendedMaxResets;
|
||||
tempUserDefaultMaxBids = product.RecommendedMaxBids;
|
||||
// Bid deadline rimane quello dell'utente o default 200ms
|
||||
if (!tempUserDefaultBidDeadline.HasValue)
|
||||
tempUserDefaultBidDeadline = 200;
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void CancelEditProduct()
|
||||
{
|
||||
editingProductKey = null;
|
||||
tempUserDefaultMinPrice = null;
|
||||
tempUserDefaultMaxPrice = null;
|
||||
tempUserDefaultMinResets = null;
|
||||
tempUserDefaultMaxResets = null;
|
||||
tempUserDefaultMaxBids = null;
|
||||
tempUserDefaultBidDeadline = null;
|
||||
}
|
||||
|
||||
private async Task SaveProductDefaults(ProductStatisticsRecord product)
|
||||
{
|
||||
try
|
||||
{
|
||||
isSavingDefaults = true;
|
||||
StateHasChanged();
|
||||
|
||||
// Aggiorna nel database
|
||||
await DatabaseService.UpdateProductUserDefaultsAsync(
|
||||
product.ProductKey,
|
||||
tempUserDefaultMinPrice,
|
||||
tempUserDefaultMaxPrice,
|
||||
tempUserDefaultMinResets,
|
||||
tempUserDefaultMaxResets,
|
||||
tempUserDefaultMaxBids,
|
||||
tempUserDefaultBidDeadline
|
||||
);
|
||||
|
||||
// Aggiorna l'oggetto locale
|
||||
product.UserDefaultMinPrice = tempUserDefaultMinPrice;
|
||||
product.UserDefaultMaxPrice = tempUserDefaultMaxPrice;
|
||||
product.UserDefaultMinResets = tempUserDefaultMinResets;
|
||||
product.UserDefaultMaxResets = tempUserDefaultMaxResets;
|
||||
product.UserDefaultMaxBids = tempUserDefaultMaxBids;
|
||||
product.UserDefaultBidBeforeDeadlineMs = tempUserDefaultBidDeadline;
|
||||
|
||||
await JSRuntime.InvokeVoidAsync("alert",
|
||||
$"? Limiti default salvati per '{product.ProductName}'!\n\n" +
|
||||
$"Min: €{tempUserDefaultMinPrice:F2} - Max: €{tempUserDefaultMaxPrice:F2}\n" +
|
||||
$"Reset: {tempUserDefaultMinResets} - {tempUserDefaultMaxResets}\n" +
|
||||
$"Max Puntate: {tempUserDefaultMaxBids}\n" +
|
||||
$"Anticipo: {tempUserDefaultBidDeadline}ms");
|
||||
|
||||
CancelEditProduct();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("alert", $"Errore salvataggio: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
isSavingDefaults = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyDefaultsToAllAuctions(ProductStatisticsRecord product)
|
||||
{
|
||||
try
|
||||
{
|
||||
var matchingAuctions = AppState.Auctions
|
||||
.Where(a => ProductStatisticsService.GenerateProductKey(a.Name) == product.ProductKey)
|
||||
.ToList();
|
||||
|
||||
if (!matchingAuctions.Any())
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("alert", $"Nessuna asta trovata per '{product.ProductName}'");
|
||||
return;
|
||||
}
|
||||
|
||||
var confirmed = await JSRuntime.InvokeAsync<bool>("confirm",
|
||||
$"Applicare i limiti default a {matchingAuctions.Count} aste di '{product.ProductName}'?\n\n" +
|
||||
$"Min: €{product.UserDefaultMinPrice:F2} - Max: €{product.UserDefaultMaxPrice:F2}\n" +
|
||||
$"Reset: {product.UserDefaultMinResets} - {product.UserDefaultMaxResets}\n" +
|
||||
$"Max Puntate: {product.UserDefaultMaxBids}\n" +
|
||||
$"Anticipo: {product.UserDefaultBidBeforeDeadlineMs}ms");
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
// Applica i limiti
|
||||
foreach (var auction in matchingAuctions)
|
||||
{
|
||||
if (product.UserDefaultMinPrice.HasValue)
|
||||
auction.MinPrice = product.UserDefaultMinPrice.Value;
|
||||
if (product.UserDefaultMaxPrice.HasValue)
|
||||
auction.MaxPrice = product.UserDefaultMaxPrice.Value;
|
||||
if (product.UserDefaultMinResets.HasValue)
|
||||
auction.MinResets = product.UserDefaultMinResets.Value;
|
||||
if (product.UserDefaultMaxResets.HasValue)
|
||||
auction.MaxResets = product.UserDefaultMaxResets.Value;
|
||||
if (product.UserDefaultMaxBids.HasValue)
|
||||
auction.MaxClicks = product.UserDefaultMaxBids.Value;
|
||||
if (product.UserDefaultBidBeforeDeadlineMs.HasValue)
|
||||
auction.BidBeforeDeadlineMs = product.UserDefaultBidBeforeDeadlineMs.Value;
|
||||
}
|
||||
|
||||
// Salva
|
||||
AutoBidder.Utilities.PersistenceManager.SaveAuctions(AppState.Auctions.ToList());
|
||||
|
||||
await JSRuntime.InvokeVoidAsync("alert",
|
||||
$"? Limiti applicati a {matchingAuctions.Count} aste di '{product.ProductName}'!");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasUserDefaults(ProductStatisticsRecord product)
|
||||
{
|
||||
return product.UserDefaultMinPrice.HasValue
|
||||
&& product.UserDefaultMaxPrice.HasValue;
|
||||
}
|
||||
}
|
||||
|
||||
<style>
|
||||
@@ -631,7 +1290,25 @@ private AuctionResultExtended? selectedAuctionDetail;
|
||||
background-color: rgba(0,123,255,0.1) !important;
|
||||
}
|
||||
|
||||
.product-row {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.product-row:hover {
|
||||
background-color: rgba(25,135,84,0.1) !important;
|
||||
}
|
||||
|
||||
.table-success-subtle {
|
||||
background-color: rgba(25, 135, 84, 0.1);
|
||||
}
|
||||
|
||||
.table-info {
|
||||
background-color: rgba(13, 202, 240, 0.2) !important;
|
||||
}
|
||||
|
||||
.font-monospace {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -428,6 +428,22 @@ namespace AutoBidder.Services
|
||||
auction.AddLog($"[ASTA TERMINATA] {statusMsg}");
|
||||
OnLog?.Invoke($"[FINE] [{auction.AuctionId}] Asta {statusMsg} - Polling fermato");
|
||||
|
||||
// 💡 SUGGERIMENTO: Se persa e non abbiamo mai provato a puntare, potrebbe essere un problema di timing
|
||||
if (!won && auction.SessionBidCount == 0)
|
||||
{
|
||||
var settings = Utilities.SettingsManager.Load();
|
||||
int offsetMs = auction.BidBeforeDeadlineMs > 0
|
||||
? auction.BidBeforeDeadlineMs
|
||||
: settings.DefaultBidBeforeDeadlineMs;
|
||||
|
||||
// Se l'offset è <= 1000ms, il polling (~1s) potrebbe non catturare il momento giusto
|
||||
if (offsetMs <= 1000)
|
||||
{
|
||||
auction.AddLog($"[💡 SUGGERIMENTO] Asta persa senza mai puntare. Con offset={offsetMs}ms e polling~1s, " +
|
||||
$"potresti non vedere mai il timer scendere sotto {offsetMs}ms. Considera di aumentare l'offset a 1500-2000ms nelle impostazioni.");
|
||||
}
|
||||
}
|
||||
|
||||
auction.BidHistory.Add(new BidHistory
|
||||
{
|
||||
Timestamp = DateTime.UtcNow,
|
||||
@@ -531,7 +547,7 @@ namespace AutoBidder.Services
|
||||
{
|
||||
var settings = SettingsManager.Load();
|
||||
|
||||
// Offset: millisecondi prima della scadenza
|
||||
// Offset: millisecondi prima della scadenza (configurato dall'utente)
|
||||
int offsetMs = auction.BidBeforeDeadlineMs > 0
|
||||
? auction.BidBeforeDeadlineMs
|
||||
: settings.DefaultBidBeforeDeadlineMs;
|
||||
@@ -542,42 +558,53 @@ namespace AutoBidder.Services
|
||||
// Skip se già vincitore o timer scaduto
|
||||
if (state.IsMyBid || timerMs <= 0) return;
|
||||
|
||||
// ?? STIMA TEMPO RIMANENTE
|
||||
// L'API dà timer in secondi interi (1000, 2000, ecc.)
|
||||
// Quando cambia, salvo il timestamp. Poi stimo localmente.
|
||||
|
||||
bool timerChanged = Math.Abs(auction.LastRawTimer - timerMs) > 500;
|
||||
|
||||
if (timerChanged || !auction.LastDeadlineUpdateUtc.HasValue)
|
||||
{
|
||||
auction.LastRawTimer = timerMs;
|
||||
auction.LastDeadlineUpdateUtc = DateTime.UtcNow;
|
||||
auction.BidScheduled = false;
|
||||
}
|
||||
|
||||
// Calcola tempo stimato rimanente
|
||||
var elapsed = (DateTime.UtcNow - auction.LastDeadlineUpdateUtc.Value).TotalMilliseconds;
|
||||
double estimatedRemaining = timerMs - elapsed;
|
||||
|
||||
// Log timing solo se abilitato
|
||||
if (settings.LogTiming)
|
||||
{
|
||||
auction.AddLog($"[TIMING] API={timerMs:F0}ms, Elapsed={elapsed:F0}ms, Stima={estimatedRemaining:F0}ms");
|
||||
auction.AddLog($"[TIMING] API={timerMs:F0}ms, Offset={offsetMs}ms");
|
||||
}
|
||||
|
||||
// ?? È il momento di puntare?
|
||||
if (estimatedRemaining > offsetMs) return; // Troppo presto
|
||||
if (estimatedRemaining < -200) return; // Troppo tardi
|
||||
// Punta quando il timer API è <= offset configurato dall'utente
|
||||
// NESSUNA modifica automatica - l'utente decide il timing
|
||||
if (timerMs > offsetMs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Protezione doppia puntata
|
||||
if (auction.BidScheduled) return;
|
||||
// Timer <= offset = È IL MOMENTO DI PUNTARE!
|
||||
auction.AddLog($"[BID WINDOW] Timer={timerMs:F0}ms <= Offset={offsetMs}ms - Verifica condizioni...");
|
||||
|
||||
// Cooldown 1 secondo
|
||||
if (auction.LastClickAt.HasValue && (DateTime.UtcNow - auction.LastClickAt.Value).TotalMilliseconds < 1000) return;
|
||||
// Resetta BidScheduled se il timer è AUMENTATO (qualcun altro ha puntato = nuovo ciclo)
|
||||
if (timerMs > auction.LastScheduledTimerMs + 500)
|
||||
{
|
||||
auction.BidScheduled = false;
|
||||
}
|
||||
|
||||
// Resetta anche se è passato troppo tempo dall'ultima puntata (nuovo ciclo)
|
||||
if (auction.LastClickAt.HasValue &&
|
||||
(DateTime.UtcNow - auction.LastClickAt.Value).TotalSeconds > 10)
|
||||
{
|
||||
auction.BidScheduled = false;
|
||||
}
|
||||
|
||||
// Protezione doppia puntata SOLO per lo stesso ciclo di timer
|
||||
if (auction.BidScheduled && Math.Abs(auction.LastScheduledTimerMs - timerMs) < 100)
|
||||
{
|
||||
auction.AddLog($"[SKIP] Puntata già schedulata per timer~={timerMs:F0}ms in questo ciclo");
|
||||
return;
|
||||
}
|
||||
|
||||
// Cooldown 1 secondo tra puntate
|
||||
if (auction.LastClickAt.HasValue && (DateTime.UtcNow - auction.LastClickAt.Value).TotalMilliseconds < 1000)
|
||||
{
|
||||
auction.AddLog($"[COOLDOWN] Attesa cooldown puntata precedente");
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔴 CONTROLLI FONDAMENTALI (prezzo, reset, limiti, puntate residue)
|
||||
if (!ShouldBid(auction, state))
|
||||
{
|
||||
// I motivi vengono ora loggati sempre dentro ShouldBid
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -587,19 +614,25 @@ namespace AutoBidder.Services
|
||||
|
||||
if (!decision.ShouldBid)
|
||||
{
|
||||
// 🔥 FIX: Logga SEMPRE il motivo del blocco strategia, non solo se LogStrategyDecisions è attivo
|
||||
// Questo aiuta a capire perché si perdono le aste
|
||||
auction.AddLog($"[STRATEGY] {decision.Reason}");
|
||||
|
||||
// Log aggiuntivo solo se debug strategie attivo
|
||||
if (settings.LogStrategyDecisions)
|
||||
{
|
||||
auction.AddLog($"[STRATEGY] {decision.Reason}");
|
||||
OnLog?.Invoke($"[{auction.Name}] STRATEGY blocked: {decision.Reason}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ?? PUNTA!
|
||||
auction.BidScheduled = true;
|
||||
auction.LastScheduledTimerMs = timerMs;
|
||||
|
||||
if (settings.LogBids)
|
||||
{
|
||||
auction.AddLog($"[BID] Puntata a ~{estimatedRemaining:F0}ms dalla scadenza");
|
||||
auction.AddLog($"[BID] Puntata con timer API={timerMs:F0}ms");
|
||||
}
|
||||
|
||||
await ExecuteBid(auction, state, token);
|
||||
@@ -649,12 +682,15 @@ namespace AutoBidder.Services
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
auction.AddLog($"[BID OK] Latenza: {result.LatencyMs}ms -> EUR{result.NewPrice:F2}");
|
||||
// Log dettagliato con info ping per analisi timing
|
||||
var pollingPing = auction.PollingLatencyMs;
|
||||
auction.AddLog($"[BID OK] Latenza puntata: {result.LatencyMs}ms | Ping polling: {pollingPing}ms | Totale stimato: {result.LatencyMs + pollingPing}ms");
|
||||
OnLog?.Invoke($"[OK] Puntata riuscita su {auction.Name} ({auction.AuctionId}): {result.LatencyMs}ms");
|
||||
}
|
||||
else
|
||||
{
|
||||
auction.AddLog($"[BID FAIL] {result.Error}");
|
||||
var pollingPing = auction.PollingLatencyMs;
|
||||
auction.AddLog($"[BID FAIL] {result.Error} | Ping: {pollingPing}ms");
|
||||
OnLog?.Invoke($"[FAIL] Puntata fallita su {auction.Name} ({auction.AuctionId}): {result.Error}");
|
||||
}
|
||||
|
||||
@@ -667,7 +703,7 @@ namespace AutoBidder.Services
|
||||
Timer = state.Timer,
|
||||
LatencyMs = result.LatencyMs,
|
||||
Success = result.Success,
|
||||
Notes = result.Success ? $"EUR{result.NewPrice:F2}" : (result.Error ?? "Errore sconosciuto")
|
||||
Notes = result.Success ? $"OK" : (result.Error ?? "Errore sconosciuto")
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -698,6 +734,7 @@ namespace AutoBidder.Services
|
||||
if (auction.CalculatedValue.SavingsPercentage.HasValue &&
|
||||
auction.CalculatedValue.SavingsPercentage.Value < settings.MinSavingsPercentage)
|
||||
{
|
||||
// 🔥 Logga SEMPRE - è un blocco frequente e importante
|
||||
auction.AddLog($"[VALUE] Puntata bloccata: risparmio {auction.CalculatedValue.SavingsPercentage.Value:F1}% < {settings.MinSavingsPercentage:F1}% richiesto");
|
||||
return false;
|
||||
}
|
||||
@@ -705,46 +742,49 @@ namespace AutoBidder.Services
|
||||
|
||||
if (settings.LogTiming && settings.ValueCheckEnabled)
|
||||
{
|
||||
auction.AddLog($"[DEBUG] ? Controllo convenienza OK");
|
||||
auction.AddLog($"[DEBUG] ✓ Controllo convenienza OK");
|
||||
}
|
||||
|
||||
// ?? CONTROLLO ANTI-COLLISIONE: Rileva aste troppo "affollate"
|
||||
// Se negli ultimi 10 secondi ci sono state 3+ puntate di utenti diversi, evita
|
||||
var recentBidsThreshold = 10; // secondi
|
||||
var maxActiveBidders = 3; // se 3+ bidder attivi, potrebbe essere troppo affollata
|
||||
|
||||
try
|
||||
// ?? CONTROLLO ANTI-COLLISIONE (OPZIONALE): Rileva aste troppo "affollate"
|
||||
// DISABILITATO DI DEFAULT - può far perdere aste competitive!
|
||||
if (settings.HardcodedAntiCollisionEnabled)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var recentBids = auction.RecentBids
|
||||
.Where(b => now - b.Timestamp <= recentBidsThreshold)
|
||||
.ToList();
|
||||
var recentBidsThreshold = 10; // secondi
|
||||
var maxActiveBidders = 3; // se 3+ bidder attivi, potrebbe essere troppo affollata
|
||||
|
||||
var activeBidders = recentBids
|
||||
.Select(b => b.Username)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Count();
|
||||
|
||||
if (settings.LogTiming)
|
||||
try
|
||||
{
|
||||
auction.AddLog($"[DEBUG] Bidder attivi ultimi {recentBidsThreshold}s: {activeBidders}/{maxActiveBidders}");
|
||||
}
|
||||
|
||||
if (activeBidders >= maxActiveBidders)
|
||||
{
|
||||
// Controlla se l'ultimo bidder sono io - se sì, posso continuare
|
||||
var session = _apiClient.GetSession();
|
||||
var lastBid = recentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault();
|
||||
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var recentBids = auction.RecentBids
|
||||
.Where(b => now - b.Timestamp <= recentBidsThreshold)
|
||||
.ToList();
|
||||
|
||||
if (lastBid != null &&
|
||||
!lastBid.Username.Equals(session?.Username, StringComparison.OrdinalIgnoreCase))
|
||||
var activeBidders = recentBids
|
||||
.Select(b => b.Username)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Count();
|
||||
|
||||
if (settings.LogTiming)
|
||||
{
|
||||
auction.AddLog($"[COMPETITION] Asta affollata: {activeBidders} bidder attivi negli ultimi {recentBidsThreshold}s - SKIP");
|
||||
return false;
|
||||
auction.AddLog($"[DEBUG] Bidder attivi ultimi {recentBidsThreshold}s: {activeBidders}/{maxActiveBidders}");
|
||||
}
|
||||
|
||||
if (activeBidders >= maxActiveBidders)
|
||||
{
|
||||
// Controlla se l'ultimo bidder sono io - se sì, posso continuare
|
||||
var session = _apiClient.GetSession();
|
||||
var lastBid = recentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault();
|
||||
|
||||
if (lastBid != null &&
|
||||
!lastBid.Username.Equals(session?.Username, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
auction.AddLog($"[COMPETITION] Asta affollata: {activeBidders} bidder attivi negli ultimi {recentBidsThreshold}s - SKIP");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { /* Ignora errori nel controllo competizione */ }
|
||||
}
|
||||
catch { /* Ignora errori nel controllo competizione */ }
|
||||
|
||||
if (settings.LogTiming)
|
||||
{
|
||||
@@ -757,13 +797,14 @@ namespace AutoBidder.Services
|
||||
var session = _apiClient.GetSession();
|
||||
if (session != null && session.RemainingBids <= settings.MinimumRemainingBids)
|
||||
{
|
||||
// 🔥 Logga SEMPRE - è un blocco importante
|
||||
auction.AddLog($"[LIMIT] Puntata bloccata: puntate residue ({session.RemainingBids}) <= limite ({settings.MinimumRemainingBids})");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (settings.LogTiming && session != null)
|
||||
{
|
||||
auction.AddLog($"[DEBUG] ? Puntate residue OK ({session.RemainingBids} > {settings.MinimumRemainingBids})");
|
||||
auction.AddLog($"[DEBUG] ✓ Puntate residue OK ({session.RemainingBids} > {settings.MinimumRemainingBids})");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -780,12 +821,14 @@ namespace AutoBidder.Services
|
||||
// ?? CONTROLLO 3: MinPrice/MaxPrice
|
||||
if (auction.MinPrice > 0 && state.Price < auction.MinPrice)
|
||||
{
|
||||
// 🔥 Logga SEMPRE questo blocco - è critico per capire perché non punta
|
||||
auction.AddLog($"[PRICE] Prezzo troppo basso: €{state.Price:F2} < Min €{auction.MinPrice:F2}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (auction.MaxPrice > 0 && state.Price > auction.MaxPrice)
|
||||
{
|
||||
// 🔥 Logga SEMPRE questo blocco - è critico
|
||||
auction.AddLog($"[PRICE] Prezzo troppo alto: €{state.Price:F2} > Max €{auction.MaxPrice:F2}");
|
||||
return false;
|
||||
}
|
||||
@@ -801,12 +844,14 @@ namespace AutoBidder.Services
|
||||
// ?? CONTROLLO 4: MinResets/MaxResets
|
||||
if (auction.MinResets > 0 && auction.ResetCount < auction.MinResets)
|
||||
{
|
||||
// 🔥 Logga SEMPRE - è un motivo comune di aste perse
|
||||
auction.AddLog($"[RESET] Reset troppo bassi: {auction.ResetCount} < Min {auction.MinResets}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (auction.MaxResets > 0 && auction.ResetCount >= auction.MaxResets)
|
||||
{
|
||||
// 🔥 Logga SEMPRE - è un motivo comune di aste perse
|
||||
auction.AddLog($"[RESET] Reset massimi raggiunti: {auction.ResetCount} >= Max {auction.MaxResets}");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -732,6 +732,21 @@ namespace AutoBidder.Services
|
||||
await using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}),
|
||||
|
||||
new Migration(15, "Add user-defined default limits to ProductStatistics", async (conn) => {
|
||||
var sql = @"
|
||||
-- Aggiungi colonne per limiti definiti dall'utente (separati dai calcolati)
|
||||
ALTER TABLE ProductStatistics ADD COLUMN UserDefaultMinPrice REAL;
|
||||
ALTER TABLE ProductStatistics ADD COLUMN UserDefaultMaxPrice REAL;
|
||||
ALTER TABLE ProductStatistics ADD COLUMN UserDefaultMinResets INTEGER;
|
||||
ALTER TABLE ProductStatistics ADD COLUMN UserDefaultMaxResets INTEGER;
|
||||
ALTER TABLE ProductStatistics ADD COLUMN UserDefaultMaxBids INTEGER;
|
||||
ALTER TABLE ProductStatistics ADD COLUMN UserDefaultBidBeforeDeadlineMs INTEGER;
|
||||
";
|
||||
await using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
})
|
||||
};
|
||||
|
||||
@@ -1407,12 +1422,14 @@ namespace AutoBidder.Services
|
||||
AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
|
||||
AvgResets, MinResets, MaxResets,
|
||||
RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
|
||||
UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs,
|
||||
HourlyStatsJson, LastUpdated)
|
||||
VALUES (@productKey, @productName, @totalAuctions, @wonAuctions, @lostAuctions,
|
||||
@avgFinalPrice, @minFinalPrice, @maxFinalPrice,
|
||||
@avgBidsToWin, @minBidsToWin, @maxBidsToWin,
|
||||
@avgResets, @minResets, @maxResets,
|
||||
@recMinPrice, @recMaxPrice, @recMinResets, @recMaxResets, @recMaxBids,
|
||||
@userMinPrice, @userMaxPrice, @userMinResets, @userMaxResets, @userMaxBids, @userBidDeadline,
|
||||
@hourlyJson, @lastUpdated)
|
||||
ON CONFLICT(ProductKey) DO UPDATE SET
|
||||
ProductName = @productName,
|
||||
@@ -1433,6 +1450,12 @@ namespace AutoBidder.Services
|
||||
RecommendedMinResets = @recMinResets,
|
||||
RecommendedMaxResets = @recMaxResets,
|
||||
RecommendedMaxBids = @recMaxBids,
|
||||
UserDefaultMinPrice = COALESCE(@userMinPrice, UserDefaultMinPrice),
|
||||
UserDefaultMaxPrice = COALESCE(@userMaxPrice, UserDefaultMaxPrice),
|
||||
UserDefaultMinResets = COALESCE(@userMinResets, UserDefaultMinResets),
|
||||
UserDefaultMaxResets = COALESCE(@userMaxResets, UserDefaultMaxResets),
|
||||
UserDefaultMaxBids = COALESCE(@userMaxBids, UserDefaultMaxBids),
|
||||
UserDefaultBidBeforeDeadlineMs = COALESCE(@userBidDeadline, UserDefaultBidBeforeDeadlineMs),
|
||||
HourlyStatsJson = @hourlyJson,
|
||||
LastUpdated = @lastUpdated;
|
||||
";
|
||||
@@ -1457,6 +1480,12 @@ namespace AutoBidder.Services
|
||||
new SqliteParameter("@recMinResets", (object?)stats.RecommendedMinResets ?? DBNull.Value),
|
||||
new SqliteParameter("@recMaxResets", (object?)stats.RecommendedMaxResets ?? DBNull.Value),
|
||||
new SqliteParameter("@recMaxBids", (object?)stats.RecommendedMaxBids ?? DBNull.Value),
|
||||
new SqliteParameter("@userMinPrice", (object?)stats.UserDefaultMinPrice ?? DBNull.Value),
|
||||
new SqliteParameter("@userMaxPrice", (object?)stats.UserDefaultMaxPrice ?? DBNull.Value),
|
||||
new SqliteParameter("@userMinResets", (object?)stats.UserDefaultMinResets ?? DBNull.Value),
|
||||
new SqliteParameter("@userMaxResets", (object?)stats.UserDefaultMaxResets ?? DBNull.Value),
|
||||
new SqliteParameter("@userMaxBids", (object?)stats.UserDefaultMaxBids ?? DBNull.Value),
|
||||
new SqliteParameter("@userBidDeadline", (object?)stats.UserDefaultBidBeforeDeadlineMs ?? DBNull.Value),
|
||||
new SqliteParameter("@hourlyJson", (object?)stats.HourlyStatsJson ?? DBNull.Value),
|
||||
new SqliteParameter("@lastUpdated", DateTime.UtcNow.ToString("O"))
|
||||
);
|
||||
@@ -1473,6 +1502,7 @@ namespace AutoBidder.Services
|
||||
AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
|
||||
AvgResets, MinResets, MaxResets,
|
||||
RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
|
||||
UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs,
|
||||
HourlyStatsJson, LastUpdated
|
||||
FROM ProductStatistics
|
||||
WHERE ProductKey = @productKey;
|
||||
@@ -1507,8 +1537,14 @@ namespace AutoBidder.Services
|
||||
RecommendedMinResets = reader.IsDBNull(16) ? null : reader.GetInt32(16),
|
||||
RecommendedMaxResets = reader.IsDBNull(17) ? null : reader.GetInt32(17),
|
||||
RecommendedMaxBids = reader.IsDBNull(18) ? null : reader.GetInt32(18),
|
||||
HourlyStatsJson = reader.IsDBNull(19) ? null : reader.GetString(19),
|
||||
LastUpdated = reader.GetString(20)
|
||||
UserDefaultMinPrice = reader.IsDBNull(19) ? null : reader.GetDouble(19),
|
||||
UserDefaultMaxPrice = reader.IsDBNull(20) ? null : reader.GetDouble(20),
|
||||
UserDefaultMinResets = reader.IsDBNull(21) ? null : reader.GetInt32(21),
|
||||
UserDefaultMaxResets = reader.IsDBNull(22) ? null : reader.GetInt32(22),
|
||||
UserDefaultMaxBids = reader.IsDBNull(23) ? null : reader.GetInt32(23),
|
||||
UserDefaultBidBeforeDeadlineMs = reader.IsDBNull(24) ? null : reader.GetInt32(24),
|
||||
HourlyStatsJson = reader.IsDBNull(25) ? null : reader.GetString(25),
|
||||
LastUpdated = reader.GetString(26)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1558,6 +1594,7 @@ namespace AutoBidder.Services
|
||||
AvgBidsToWin, MinBidsToWin, MaxBidsToWin,
|
||||
AvgResets, MinResets, MaxResets,
|
||||
RecommendedMinPrice, RecommendedMaxPrice, RecommendedMinResets, RecommendedMaxResets, RecommendedMaxBids,
|
||||
UserDefaultMinPrice, UserDefaultMaxPrice, UserDefaultMinResets, UserDefaultMaxResets, UserDefaultMaxBids, UserDefaultBidBeforeDeadlineMs,
|
||||
HourlyStatsJson, LastUpdated
|
||||
FROM ProductStatistics
|
||||
ORDER BY TotalAuctions DESC;
|
||||
@@ -1593,13 +1630,66 @@ namespace AutoBidder.Services
|
||||
RecommendedMinResets = reader.IsDBNull(16) ? null : reader.GetInt32(16),
|
||||
RecommendedMaxResets = reader.IsDBNull(17) ? null : reader.GetInt32(17),
|
||||
RecommendedMaxBids = reader.IsDBNull(18) ? null : reader.GetInt32(18),
|
||||
HourlyStatsJson = reader.IsDBNull(19) ? null : reader.GetString(19),
|
||||
LastUpdated = reader.GetString(20)
|
||||
UserDefaultMinPrice = reader.IsDBNull(19) ? null : reader.GetDouble(19),
|
||||
UserDefaultMaxPrice = reader.IsDBNull(20) ? null : reader.GetDouble(20),
|
||||
UserDefaultMinResets = reader.IsDBNull(21) ? null : reader.GetInt32(21),
|
||||
UserDefaultMaxResets = reader.IsDBNull(22) ? null : reader.GetInt32(22),
|
||||
UserDefaultMaxBids = reader.IsDBNull(23) ? null : reader.GetInt32(23),
|
||||
UserDefaultBidBeforeDeadlineMs = reader.IsDBNull(24) ? null : reader.GetInt32(24),
|
||||
HourlyStatsJson = reader.IsDBNull(25) ? null : reader.GetString(25),
|
||||
LastUpdated = reader.GetString(26)
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Elimina un prodotto dalle statistiche per ProductKey
|
||||
/// </summary>
|
||||
public async Task<int> DeleteProductStatisticsAsync(string productKey)
|
||||
{
|
||||
var sql = @"
|
||||
DELETE FROM ProductStatistics
|
||||
WHERE ProductKey = @productKey;
|
||||
";
|
||||
|
||||
return await ExecuteNonQueryAsync(sql,
|
||||
new SqliteParameter("@productKey", productKey)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggiorna i valori di default definiti dall'utente per un prodotto
|
||||
/// </summary>
|
||||
public async Task UpdateProductUserDefaultsAsync(string productKey,
|
||||
double? minPrice, double? maxPrice,
|
||||
int? minResets, int? maxResets,
|
||||
int? maxBids, int? bidBeforeDeadlineMs)
|
||||
{
|
||||
var sql = @"
|
||||
UPDATE ProductStatistics
|
||||
SET UserDefaultMinPrice = @minPrice,
|
||||
UserDefaultMaxPrice = @maxPrice,
|
||||
UserDefaultMinResets = @minResets,
|
||||
UserDefaultMaxResets = @maxResets,
|
||||
UserDefaultMaxBids = @maxBids,
|
||||
UserDefaultBidBeforeDeadlineMs = @bidDeadline,
|
||||
LastUpdated = @lastUpdated
|
||||
WHERE ProductKey = @productKey;
|
||||
";
|
||||
|
||||
await ExecuteNonQueryAsync(sql,
|
||||
new SqliteParameter("@productKey", productKey),
|
||||
new SqliteParameter("@minPrice", (object?)minPrice ?? DBNull.Value),
|
||||
new SqliteParameter("@maxPrice", (object?)maxPrice ?? DBNull.Value),
|
||||
new SqliteParameter("@minResets", (object?)minResets ?? DBNull.Value),
|
||||
new SqliteParameter("@maxResets", (object?)maxResets ?? DBNull.Value),
|
||||
new SqliteParameter("@maxBids", (object?)maxBids ?? DBNull.Value),
|
||||
new SqliteParameter("@bidDeadline", (object?)bidBeforeDeadlineMs ?? DBNull.Value),
|
||||
new SqliteParameter("@lastUpdated", DateTime.UtcNow.ToString("O"))
|
||||
);
|
||||
}
|
||||
|
||||
private AuctionResultExtended ReadAuctionResultExtended(Microsoft.Data.Sqlite.SqliteDataReader reader)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
@inject AuctionMonitor AuctionMonitor
|
||||
@implements IDisposable
|
||||
|
||||
<div class="nav-sidebar">
|
||||
<div class="nav-header">
|
||||
@@ -36,11 +38,32 @@
|
||||
</div>
|
||||
|
||||
<div class="nav-footer">
|
||||
<!-- Info Sessione Utente -->
|
||||
@if (!string.IsNullOrEmpty(sessionUsername))
|
||||
{
|
||||
<div class="session-stats">
|
||||
<div class="session-stat">
|
||||
<i class="bi bi-hand-index-thumb-fill"></i>
|
||||
<div class="stat-content">
|
||||
<span class="stat-label">Puntate</span>
|
||||
<span class="stat-value @GetBidsClass()">@sessionRemainingBids</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-stat">
|
||||
<i class="bi bi-wallet2"></i>
|
||||
<div class="stat-content">
|
||||
<span class="stat-label">Credito</span>
|
||||
<span class="stat-value text-success">€@sessionShopCredit.ToString("F2")</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<div class="user-badge">
|
||||
<div class="user-badge @(string.IsNullOrEmpty(sessionUsername) ? "disconnected" : "connected")">
|
||||
<i class="bi bi-person-circle"></i>
|
||||
<span>@context.User.Identity?.Name</span>
|
||||
<span>@(string.IsNullOrEmpty(sessionUsername) ? "Non connesso" : sessionUsername)</span>
|
||||
</div>
|
||||
<a href="/Account/Logout" class="nav-menu-item logout-item">
|
||||
<i class="bi bi-box-arrow-right"></i>
|
||||
@@ -52,6 +75,52 @@
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string? sessionUsername;
|
||||
private int sessionRemainingBids;
|
||||
private double sessionShopCredit;
|
||||
private System.Threading.Timer? refreshTimer;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
LoadSessionInfo();
|
||||
|
||||
// Refresh ogni 5 secondi
|
||||
refreshTimer = new System.Threading.Timer(async _ =>
|
||||
{
|
||||
LoadSessionInfo();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
private void LoadSessionInfo()
|
||||
{
|
||||
try
|
||||
{
|
||||
var session = AuctionMonitor.GetSession();
|
||||
if (session != null)
|
||||
{
|
||||
sessionUsername = session.Username;
|
||||
sessionRemainingBids = session.RemainingBids;
|
||||
sessionShopCredit = session.ShopCredit;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private string GetBidsClass()
|
||||
{
|
||||
if (sessionRemainingBids <= 10) return "text-danger";
|
||||
if (sessionRemainingBids <= 50) return "text-warning";
|
||||
return "text-success";
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
refreshTimer?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
<style>
|
||||
.nav-sidebar {
|
||||
display: flex;
|
||||
@@ -150,6 +219,52 @@
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.session-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.session-stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
padding: 0.375rem 0;
|
||||
}
|
||||
|
||||
.session-stat i {
|
||||
font-size: 0.875rem;
|
||||
width: 1.25rem;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.session-stat .stat-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.session-stat .stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.session-stat .stat-value {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.session-stat .text-success { color: #22c55e; }
|
||||
.session-stat .text-warning { color: #f59e0b; }
|
||||
.session-stat .text-danger { color: #ef4444; }
|
||||
|
||||
.user-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -162,6 +277,15 @@
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.user-badge.connected {
|
||||
border-left: 3px solid #22c55e;
|
||||
}
|
||||
|
||||
.user-badge.disconnected {
|
||||
border-left: 3px solid #ef4444;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.user-badge i {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
@@ -147,6 +147,13 @@ namespace AutoBidder.Utilities
|
||||
/// </summary>
|
||||
public bool LogErrors { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Applica automaticamente i limiti salvati nel prodotto quando si aggiunge una nuova asta.
|
||||
/// Se TRUE e il prodotto ha valori di default salvati, li applica automaticamente.
|
||||
/// Default: true (consigliato per coerenza)
|
||||
/// </summary>
|
||||
public bool AutoApplyProductDefaults { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Log stato asta (terminata, reset, ecc.) [STATUS]
|
||||
/// Default: true
|
||||
@@ -203,6 +210,14 @@ namespace AutoBidder.Utilities
|
||||
/// </summary>
|
||||
public double MinSavingsPercentage { get; set; } = -5.0;
|
||||
|
||||
/// <summary>
|
||||
/// Abilita il controllo anti-collisione hardcoded.
|
||||
/// Se attivo, blocca le puntate quando ci sono 3+ bidder attivi negli ultimi 10 secondi.
|
||||
/// ATTENZIONE: Questo controllo può far perdere aste competitive!
|
||||
/// Default: false (DISABILITATO - non blocca mai)
|
||||
/// </summary>
|
||||
public bool HardcodedAntiCollisionEnabled { get; set; } = false;
|
||||
|
||||
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
|
||||
// RILEVAMENTO COMPETIZIONE E HEAT METRIC
|
||||
// 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
|
||||
|
||||
@@ -1,5 +1,423 @@
|
||||
/* === MODERN PAGE STYLES (append to app-wpf.css) === */
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
TOOLBAR COMPATTA CON PULSANTI E CONTEGGI
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
.toolbar-compact {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: linear-gradient(135deg, rgba(20, 20, 30, 0.8) 0%, rgba(30, 30, 45, 0.8) 100%);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
margin-bottom: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-group-actions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.action-btn.success {
|
||||
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.success:hover {
|
||||
box-shadow: 0 0 10px rgba(34, 197, 94, 0.5);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.action-btn.warning {
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.action-btn.warning:hover {
|
||||
box-shadow: 0 0 10px rgba(245, 158, 11, 0.5);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.action-btn.secondary {
|
||||
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-btn.secondary:hover {
|
||||
box-shadow: 0 0 10px rgba(107, 114, 128, 0.5);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Indicatori Stato */
|
||||
.status-indicators {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0 0.5rem;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.status-pill:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.status-pill i {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.status-pill.total { color: #a5b4fc; border-color: rgba(99, 102, 241, 0.3); }
|
||||
.status-pill.active { color: #4ade80; border-color: rgba(34, 197, 94, 0.3); }
|
||||
.status-pill.paused { color: #fbbf24; border-color: rgba(251, 191, 36, 0.3); }
|
||||
.status-pill.stopped { color: #9ca3af; border-color: rgba(156, 163, 175, 0.3); }
|
||||
.status-pill.won { color: #fbbf24; border-color: rgba(251, 191, 36, 0.3); }
|
||||
.status-pill.lost { color: #f87171; border-color: rgba(248, 113, 113, 0.3); }
|
||||
|
||||
/* Pulsanti Gestione */
|
||||
.btn-group-manage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.manage-separator {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
.manage-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.manage-btn:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.manage-btn.primary {
|
||||
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.manage-btn.primary:hover:not(:disabled) {
|
||||
box-shadow: 0 0 8px rgba(99, 102, 241, 0.5);
|
||||
}
|
||||
|
||||
.manage-btn.danger {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.manage-btn.danger:hover:not(:disabled) {
|
||||
box-shadow: 0 0 8px rgba(239, 68, 68, 0.5);
|
||||
}
|
||||
|
||||
.manage-btn.danger-fill {
|
||||
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.manage-btn.danger-fill:hover:not(:disabled) {
|
||||
box-shadow: 0 0 10px rgba(220, 38, 38, 0.6);
|
||||
}
|
||||
|
||||
.manage-btn.outline-success {
|
||||
border-color: rgba(34, 197, 94, 0.4);
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.manage-btn.outline-success:hover:not(:disabled) {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
}
|
||||
|
||||
.manage-btn.outline-warning {
|
||||
border-color: rgba(245, 158, 11, 0.4);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.manage-btn.outline-warning:hover:not(:disabled) {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
}
|
||||
|
||||
.manage-btn.outline-secondary {
|
||||
border-color: rgba(156, 163, 175, 0.4);
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.manage-btn.outline-secondary:hover:not(:disabled) {
|
||||
background: rgba(156, 163, 175, 0.15);
|
||||
}
|
||||
|
||||
.manage-btn.outline-gold {
|
||||
border-color: rgba(251, 191, 36, 0.4);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.manage-btn.outline-gold:hover:not(:disabled) {
|
||||
background: rgba(251, 191, 36, 0.15);
|
||||
}
|
||||
|
||||
.manage-btn.outline-danger {
|
||||
border-color: rgba(239, 68, 68, 0.4);
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.manage-btn.outline-danger:hover:not(:disabled) {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
LAYOUT CON SPLITTER TRASCINABILI
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Container principale - occupa tutto lo spazio disponibile */
|
||||
.auction-monitor-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Area contenuto principale */
|
||||
.main-content-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* Riga superiore (Aste + Log) */
|
||||
.top-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
min-height: 150px;
|
||||
overflow: hidden;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* Riga inferiore (Dettagli) */
|
||||
.bottom-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 80px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Pannello generico */
|
||||
.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Pannello Aste */
|
||||
.panel-auctions {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
/* Pannello Log */
|
||||
.panel-log {
|
||||
flex: 0 0 280px;
|
||||
min-width: 150px;
|
||||
max-width: 450px;
|
||||
}
|
||||
|
||||
/* Pannello Dettagli */
|
||||
.panel-details {
|
||||
flex: 1;
|
||||
min-height: 80px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Gutter/Splitter */
|
||||
.gutter {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
flex-shrink: 0;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.gutter:hover {
|
||||
background: rgba(99, 102, 241, 0.25);
|
||||
}
|
||||
|
||||
.gutter:active {
|
||||
background: rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
|
||||
.gutter-vertical {
|
||||
width: 6px;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.gutter-horizontal {
|
||||
height: 6px;
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
/* Header pannello */
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.4rem 0.6rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel-header i {
|
||||
margin-right: 0.4rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Contenuto dettagli */
|
||||
.auction-details-content {
|
||||
padding: 0.5rem;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.details-header {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
padding: 0.25rem 0 0.5rem 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* Placeholder dettagli */
|
||||
.details-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.details-placeholder i {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.375rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.details-placeholder p {
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Colonna Ping */
|
||||
.col-ping {
|
||||
width: 55px;
|
||||
min-width: 55px;
|
||||
text-align: center;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 992px) {
|
||||
.top-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.gutter-vertical {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.panel-log {
|
||||
flex: 0 0 150px;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.toolbar-compact {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-group-actions,
|
||||
.status-indicators,
|
||||
.btn-group-manage {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.status-indicators {
|
||||
border: none;
|
||||
padding: 0.5rem 0;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.btn-group-manage {
|
||||
margin-left: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* ✅ NUOVO: Stili per selezione riga e colonna puntate */
|
||||
.table-hover tbody tr {
|
||||
cursor: pointer;
|
||||
@@ -712,3 +1130,52 @@
|
||||
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%) !important;
|
||||
color: #e5e5e5 !important;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
BANNER STATISTICHE ASTE - RIMOSSO (sostituito da toolbar compatta)
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Legacy support - nascosto */
|
||||
.auctions-stats-banner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
TABELLA COMPATTA
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
.table-compact {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.table-compact th {
|
||||
padding: 0.4rem 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table-compact td {
|
||||
padding: 0.35rem 0.5rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.table-compact .col-stato {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.table-compact .col-azioni {
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
/* Pulsanti extra small */
|
||||
.btn-xs {
|
||||
padding: 0.15rem 0.35rem;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.btn-xs i {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user