* Aggiornamento alla versione 4.0.0 * Refactoring architetturale: introdotte partial classes e UserControls modulari per migliorare manutenibilità e leggibilità. * Aggiunti nuovi UserControls: `AuctionMonitorControl`, `BrowserControl`, `SettingsControl`, `StatisticsControl`. * Introdotto supporto per WebView2 per il browser integrato. * Migliorata gestione delle aste: aggiunta/rimozione tramite URL o ID, configurazione predefinita. * Nuove funzionalità di esportazione: supporto CSV, JSON, XML con opzioni configurabili. * Logging avanzato: codifica colore per severità e auto-scroll. * Tema scuro moderno e miglioramenti UI/UX: sidebar di navigazione, griglie virtualizzate, icone emoji. * Persistenza dati: salvataggio automatico di aste e impostazioni in file JSON. * Documentazione aggiornata: `README.md`, `CHANGELOG.md` e nuovi file di supporto. * Miglioramenti alla sicurezza: cookie di sessione salvati in modo sicuro con DPAPI. * Preparazione per future estensioni: placeholder per funzionalità avanzate e struttura modulare.
272 lines
15 KiB
C#
272 lines
15 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
using System.Xml.Linq;
|
|
using AutoBidder.Models;
|
|
using AutoBidder.Utilities;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace AutoBidder
|
|
{
|
|
/// <summary>
|
|
/// Statistics and closed auctions event handlers
|
|
/// NOTA: Funzionalità statistiche temporaneamente disabilitate - in sviluppo
|
|
/// </summary>
|
|
public partial class MainWindow
|
|
{
|
|
private void ExportStatsButton_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
MessageBox.Show(this, "Funzionalità statistiche in sviluppo", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
}
|
|
|
|
private async void LoadClosedAuctionsButton_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
MessageBox.Show(this, "Funzionalità statistiche in sviluppo", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
|
|
/* CODICE TEMPORANEAMENTE DISABILITATO - Statistiche in sviluppo
|
|
try
|
|
{
|
|
StatsStatusText.Text = "Avvio caricamento statistiche...";
|
|
var settings = Utilities.SettingsManager.Load();
|
|
if (settings == null || string.IsNullOrWhiteSpace(settings.ExportPath) || !Directory.Exists(settings.ExportPath))
|
|
{
|
|
MessageBox.Show(this, "Percorso export non configurato o non valido. Configuralo nelle impostazioni.", "Carica Statistiche", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
StatsStatusText.Text = "Percorso export non valido";
|
|
return;
|
|
}
|
|
|
|
ExportProgressBar.Visibility = Visibility.Visible;
|
|
ExportProgressText.Visibility = Visibility.Visible;
|
|
ExportProgressText.Text = "Caricamento statistiche...";
|
|
|
|
var folder = settings.ExportPath!;
|
|
var files = Directory.GetFiles(folder, "auction_*.*");
|
|
if (files.Length == 0)
|
|
{
|
|
MessageBox.Show(this, "Nessun file di aste trovato nella cartella di export.", "Carica Statistiche", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
StatsStatusText.Text = "Nessun file trovato";
|
|
ExportProgressBar.Visibility = Visibility.Collapsed;
|
|
ExportProgressText.Visibility = Visibility.Collapsed;
|
|
return;
|
|
}
|
|
|
|
var aggregated = new Dictionary<string, List<ClosedAuctionRecord>>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
await Task.Run(() =>
|
|
{
|
|
foreach (var f in files)
|
|
{
|
|
try
|
|
{
|
|
var ext = Path.GetExtension(f).ToLowerInvariant();
|
|
if (ext == ".json")
|
|
{
|
|
var txt = File.ReadAllText(f, Encoding.UTF8);
|
|
try
|
|
{
|
|
var rec = System.Text.Json.JsonSerializer.Deserialize<ClosedAuctionRecord>(txt);
|
|
if (rec != null)
|
|
{
|
|
var key = (rec.ProductName ?? ExtractProductFromFilename(f) ?? "<unknown>").Trim();
|
|
if (!aggregated.ContainsKey(key)) aggregated[key] = new List<ClosedAuctionRecord>();
|
|
aggregated[key].Add(rec);
|
|
continue;
|
|
}
|
|
}
|
|
catch { }
|
|
|
|
try
|
|
{
|
|
var arr = System.Text.Json.JsonSerializer.Deserialize<List<ClosedAuctionRecord>>(txt);
|
|
if (arr != null)
|
|
{
|
|
foreach (var r in arr)
|
|
{
|
|
var key = (r.ProductName ?? ExtractProductFromFilename(f) ?? "<unknown>").Trim();
|
|
if (!aggregated.ContainsKey(key)) aggregated[key] = new List<ClosedAuctionRecord>();
|
|
aggregated[key].Add(r);
|
|
}
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
else if (ext == ".xml")
|
|
{
|
|
try
|
|
{
|
|
var doc = XDocument.Load(f);
|
|
var auctionElems = doc.Descendants("Auction");
|
|
if (!auctionElems.Any()) auctionElems = doc.Descendants("AuctionExport");
|
|
foreach (var n in auctionElems)
|
|
{
|
|
var name = n.Descendants("Name").FirstOrDefault()?.Value ?? n.Descendants("ProductName").FirstOrDefault()?.Value;
|
|
double d = 0; double.TryParse(n.Descendants("FinalPrice").FirstOrDefault()?.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out d);
|
|
int bids = 0; int.TryParse(n.Descendants("TotalBids").FirstOrDefault()?.Value, out bids);
|
|
var winner = n.Descendants("Winner").FirstOrDefault()?.Value ?? string.Empty;
|
|
var url = n.Descendants("OriginalUrl").FirstOrDefault()?.Value ?? string.Empty;
|
|
var rec = new ClosedAuctionRecord { ProductName = name, FinalPrice = d == 0 ? null : (double?)d, Winner = winner, BidsUsed = bids, AuctionUrl = url };
|
|
var key = (rec.ProductName ?? ExtractProductFromFilename(f) ?? "<unknown>").Trim();
|
|
if (!aggregated.ContainsKey(key)) aggregated[key] = new List<ClosedAuctionRecord>();
|
|
aggregated[key].Add(rec);
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
else // CSV or text
|
|
{
|
|
try
|
|
{
|
|
var lines = File.ReadAllLines(f, Encoding.UTF8);
|
|
string product = ExtractProductFromFilename(f) ?? "<unknown>";
|
|
double? price = null; int? bids = null; string winner = string.Empty; string url = string.Empty;
|
|
foreach (var l in lines)
|
|
{
|
|
var line = l.Trim();
|
|
if (line.StartsWith("Name,") || line.StartsWith("ProductName,"))
|
|
{
|
|
var parts = line.Split(',', 2);
|
|
if (parts.Length == 2) product = parts[1].Trim('"');
|
|
}
|
|
else if (line.StartsWith("FinalPrice", StringComparison.OrdinalIgnoreCase) || line.StartsWith("Price,"))
|
|
{
|
|
var parts = line.Split(',', 2);
|
|
if (parts.Length == 2 && double.TryParse(parts[1].Trim('"').Replace('€', ' ').Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out var p)) price = p;
|
|
}
|
|
else if (line.StartsWith("TotalBids", StringComparison.OrdinalIgnoreCase) || line.StartsWith("BidsUsed", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var parts = line.Split(',', 2);
|
|
if (parts.Length == 2 && int.TryParse(parts[1].Trim('"'), out var b)) bids = b;
|
|
}
|
|
else if (line.StartsWith("Winner", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var parts = line.Split(',', 2);
|
|
if (parts.Length == 2) winner = parts[1].Trim('"');
|
|
}
|
|
else if (line.StartsWith("OriginalUrl", StringComparison.OrdinalIgnoreCase) || line.StartsWith("AuctionUrl", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var parts = line.Split(',', 2);
|
|
if (parts.Length == 2) url = parts[1].Trim('"');
|
|
}
|
|
}
|
|
var rec = new ClosedAuctionRecord { ProductName = product, FinalPrice = price, BidsUsed = bids, Winner = winner, AuctionUrl = url };
|
|
var key = (rec.ProductName ?? ExtractProductFromFilename(f) ?? "<unknown>").Trim();
|
|
if (!aggregated.ContainsKey(key)) aggregated[key] = new List<ClosedAuctionRecord>();
|
|
aggregated[key].Add(rec);
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
});
|
|
|
|
var stats = new List<object>();
|
|
foreach (var kv in aggregated)
|
|
{
|
|
var list = kv.Value.Where(x => x.FinalPrice.HasValue || x.BidsUsed.HasValue).ToList();
|
|
if (list.Count == 0) continue;
|
|
var avgPrice = list.Where(x => x.FinalPrice.HasValue).Select(x => x.FinalPrice!.Value).DefaultIfEmpty(0).Average();
|
|
var avgBids = list.Where(x => x.BidsUsed.HasValue).Select(x => x.BidsUsed!.Value).DefaultIfEmpty(0).Average();
|
|
var winner = list.Where(x => !string.IsNullOrEmpty(x.Winner)).GroupBy(x => x.Winner).OrderByDescending(g => g.Count()).Select(g => g.Key).FirstOrDefault() ?? string.Empty;
|
|
var example = list.FirstOrDefault(x => !string.IsNullOrEmpty(x.AuctionUrl))?.AuctionUrl ?? string.Empty;
|
|
|
|
stats.Add(new
|
|
{
|
|
ProductName = kv.Key,
|
|
FinalPrice = avgPrice,
|
|
Winner = winner,
|
|
BidsUsed = (int)Math.Round(avgBids),
|
|
AuctionUrl = example,
|
|
Count = list.Count,
|
|
AverageFinalPrice = avgPrice,
|
|
AverageBidsUsed = avgBids
|
|
});
|
|
}
|
|
|
|
Dispatcher.Invoke(() =>
|
|
{
|
|
StatsDataGrid.ItemsSource = stats.OrderByDescending(s => (int)s.GetType().GetProperty("Count")!.GetValue(s)).ToList();
|
|
StatsStatusText.Text = $"Caricati {stats.Count} prodotti ({files.Length} file analizzati)";
|
|
ExportProgressBar.Visibility = Visibility.Collapsed;
|
|
ExportProgressText.Visibility = Visibility.Collapsed;
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
StatsStatusText.Text = "Errore caricamento statistiche";
|
|
Log($"[ERRORE] Carica statistiche: {ex.Message}", LogLevel.Error);
|
|
MessageBox.Show(this, "Errore durante caricamento statistiche: " + ex.Message, "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
ExportProgressBar.Visibility = Visibility.Collapsed;
|
|
ExportProgressText.Visibility = Visibility.Collapsed;
|
|
}
|
|
*/
|
|
}
|
|
|
|
private static string? ExtractProductFromFilename(string path)
|
|
{
|
|
try
|
|
{
|
|
var name = Path.GetFileNameWithoutExtension(path);
|
|
var m = Regex.Match(name, @"auction_(.+)");
|
|
if (m.Success)
|
|
{
|
|
var v = m.Groups[1].Value;
|
|
if (Regex.IsMatch(v, "^\\d+$")) return null;
|
|
return v.Replace('_', ' ');
|
|
}
|
|
return null;
|
|
}
|
|
catch { return null; }
|
|
}
|
|
|
|
private async void ApplyInsightsButton_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
MessageBox.Show(this, "Funzionalità statistiche in sviluppo", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
|
|
/* CODICE TEMPORANEAMENTE DISABILITATO
|
|
try
|
|
{
|
|
if (_selectedAuction == null)
|
|
{
|
|
MessageBox.Show(this, "Seleziona un'asta prima di applicare le raccomandazioni.", "Applica Raccomandazioni", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
return;
|
|
}
|
|
|
|
var optionsBuilder = new Microsoft.EntityFrameworkCore.DbContextOptionsBuilder<Data.StatisticsContext>();
|
|
var dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "stats.db");
|
|
optionsBuilder.UseSqlite($"Data Source={dbPath}");
|
|
using var ctx = new Data.StatisticsContext(optionsBuilder.Options);
|
|
var svc = new Services.StatsService(ctx);
|
|
|
|
var (recBids, recPrice) = await svc.GetRecommendationAsync(_selectedAuction.AuctionInfo.Name, _selectedAuction.AuctionInfo.OriginalUrl);
|
|
_selectedAuction.MaxClicks = Math.Max(_selectedAuction.MaxClicks, recBids);
|
|
_selectedAuction.MaxPrice = Math.Max(_selectedAuction.MaxPrice, recPrice);
|
|
Log($"[OK] Raccomandazioni: MaxClicks={recBids}, MaxPrice={recPrice:F2} applicate a {_selectedAuction.Name}", LogLevel.Success);
|
|
UpdateSelectedAuctionDetails(_selectedAuction);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log($"[ERRORE] ApplyInsights: {ex.Message}", LogLevel.Error);
|
|
MessageBox.Show(this, "Errore applicazione raccomandazioni: " + ex.Message, "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
*/
|
|
}
|
|
|
|
private void FreeBidsStart_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
MessageBox.Show(this, "Funzionalità non ancora implementata", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
}
|
|
|
|
private void FreeBidsStop_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
MessageBox.Show(this, "Funzionalità non ancora implementata", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
}
|
|
}
|
|
}
|