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; }
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||||
<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>
|
||||||
<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>
|
||||||
<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 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>
|
||||||
<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>
|
||||||
<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>
|
||||||
<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>
|
||||||
<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>
|
||||||
<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>
|
||||||
<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>
|
||||||
<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>
|
||||||
<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>
|
||||||
<small class="fw-bold">@product.ProductName</small>
|
<div class="d-flex align-items-center gap-2">
|
||||||
<br/>
|
<button class="btn btn-sm btn-outline-secondary"
|
||||||
<small class="text-muted">@product.TotalAuctions totali (@product.WonAuctions vinte)</small>
|
@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>
|
||||||
<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,46 +742,49 @@ 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!
|
||||||
var recentBidsThreshold = 10; // secondi
|
if (settings.HardcodedAntiCollisionEnabled)
|
||||||
var maxActiveBidders = 3; // se 3+ bidder attivi, potrebbe essere troppo affollata
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
var recentBidsThreshold = 10; // secondi
|
||||||
var recentBids = auction.RecentBids
|
var maxActiveBidders = 3; // se 3+ bidder attivi, potrebbe essere troppo affollata
|
||||||
.Where(b => now - b.Timestamp <= recentBidsThreshold)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var activeBidders = recentBids
|
try
|
||||||
.Select(b => b.Username)
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.Count();
|
|
||||||
|
|
||||||
if (settings.LogTiming)
|
|
||||||
{
|
{
|
||||||
auction.AddLog($"[DEBUG] Bidder attivi ultimi {recentBidsThreshold}s: {activeBidders}/{maxActiveBidders}");
|
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
}
|
var recentBids = auction.RecentBids
|
||||||
|
.Where(b => now - b.Timestamp <= recentBidsThreshold)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
if (activeBidders >= maxActiveBidders)
|
var activeBidders = recentBids
|
||||||
{
|
.Select(b => b.Username)
|
||||||
// Controlla se l'ultimo bidder sono io - se sì, posso continuare
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
var session = _apiClient.GetSession();
|
.Count();
|
||||||
var lastBid = recentBids.OrderByDescending(b => b.Timestamp).FirstOrDefault();
|
|
||||||
|
|
||||||
if (lastBid != null &&
|
if (settings.LogTiming)
|
||||||
!lastBid.Username.Equals(session?.Username, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
auction.AddLog($"[COMPETITION] Asta affollata: {activeBidders} bidder attivi negli ultimi {recentBidsThreshold}s - SKIP");
|
auction.AddLog($"[DEBUG] Bidder attivi ultimi {recentBidsThreshold}s: {activeBidders}/{maxActiveBidders}");
|
||||||
return false;
|
}
|
||||||
|
|
||||||
|
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)
|
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