using System.Diagnostics; using Microsoft.Data.Sqlite; namespace CadSearchGui; public partial class Form1 : Form { private readonly Button _btnApriDb = new(); private readonly Button _btnAggiorna = new(); private readonly Button _btnCreaRaw = new(); private readonly Label _lblStatus = new(); private readonly Label _lblDb = new(); private readonly DataGridView _gridImport = new(); private readonly Label _lblImportStatus = new(); private readonly ToolTip _tipImportStatus = new(); private static readonly string[] TipiPredefiniti = { "prt", "asm" }; private const string RadiceDiscoDefault = @"X:\"; private string? _dbPath; private string? _percorsoInputCorrente; private List<(string Componente, string Descrizione)>? _righeCorrenti; public Form1() { InitializeComponent(); BuildUi(); Load += (_, _) => InizializzaDatabase(); } // Palette: blu primario per l'azione principale del flusso ("Crea cartella raw"), // il resto resta ai colori di sistema per restare coerente col tema Windows dell'utente. private static readonly Color ColorePrimario = Color.FromArgb(0, 99, 177); private void BuildUi() { AllowDrop = true; Text = "Distinta Base -> CSV con percorsi CAD"; MinimumSize = new Size(760, 480); Font = new Font("Segoe UI", 9.5f); var contenutoTop = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, ColumnCount = 1, Padding = new Padding(12, 12, 12, 0), }; contenutoTop.Controls.Add(BuildGruppoDatabase()); contenutoTop.Controls.Add(BuildGruppoImporta()); _lblImportStatus.Dock = DockStyle.Top; _lblImportStatus.AutoSize = false; _lblImportStatus.AutoEllipsis = true; _lblImportStatus.Height = 28; _lblImportStatus.Padding = new Padding(12, 6, 12, 6); _lblImportStatus.ForeColor = SystemColors.GrayText; _gridImport.Dock = DockStyle.Fill; _gridImport.ReadOnly = true; _gridImport.BorderStyle = BorderStyle.None; _gridImport.BackgroundColor = SystemColors.Window; _gridImport.RowHeadersVisible = false; _gridImport.AllowUserToAddRows = false; _gridImport.AllowUserToDeleteRows = false; _gridImport.AllowUserToResizeRows = false; _gridImport.SelectionMode = DataGridViewSelectionMode.FullRowSelect; _gridImport.MultiSelect = false; _gridImport.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; _gridImport.ColumnHeadersHeight = 32; _gridImport.ColumnHeadersDefaultCellStyle.Font = new Font(Font, FontStyle.Bold); _gridImport.ColumnHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleLeft; _gridImport.RowTemplate.Height = 26; _gridImport.AlternatingRowsDefaultCellStyle.BackColor = Color.FromArgb(245, 247, 250); _gridImport.DefaultCellStyle.SelectionBackColor = ColorePrimario; _gridImport.DefaultCellStyle.Padding = new Padding(4, 0, 4, 0); _gridImport.Columns.Add("componente", "Componente"); _gridImport.Columns.Add("descrizione", "Descrizione"); _gridImport.Columns.Add("percorso", "Percorso CAD"); _gridImport.Columns["componente"]!.FillWeight = 20; _gridImport.Columns["descrizione"]!.FillWeight = 40; _gridImport.Columns["percorso"]!.FillWeight = 40; _gridImport.CellDoubleClick += (_, _) => ApriCartellaFileSelezionato(); var statusPanel = new Panel { Dock = DockStyle.Bottom, Height = 28, BackColor = SystemColors.ControlLight }; _lblStatus.Dock = DockStyle.Fill; _lblStatus.TextAlign = ContentAlignment.MiddleLeft; _lblStatus.Padding = new Padding(12, 0, 0, 0); _lblStatus.Text = "Pronto."; statusPanel.Controls.Add(_lblStatus); Controls.Add(_gridImport); Controls.Add(_lblImportStatus); Controls.Add(statusPanel); Controls.Add(contenutoTop); DragEnter += (_, e) => e.Effect = e.Data?.GetDataPresent(DataFormats.FileDrop) == true ? DragDropEffects.Copy : DragDropEffects.None; DragDrop += (_, e) => { if (e.Data?.GetData(DataFormats.FileDrop) is string[] { Length: > 0 } file) ImportaDistinta(file[0]); }; } private GroupBox BuildGruppoDatabase() { var contenuto = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, ColumnCount = 1, }; _lblDb.AutoSize = true; _lblDb.AutoEllipsis = true; _lblDb.MaximumSize = new Size(900, 0); _lblDb.ForeColor = SystemColors.GrayText; _lblDb.Margin = new Padding(0, 0, 0, 8); contenuto.Controls.Add(_lblDb); var bottoni = new FlowLayoutPanel { AutoSize = true, FlowDirection = FlowDirection.LeftToRight }; _btnApriDb.Text = "Apri DB..."; _btnApriDb.Size = new Size(120, 32); _btnApriDb.Margin = new Padding(0, 0, 8, 0); _btnApriDb.Click += (_, _) => ScegliDatabase(); bottoni.Controls.Add(_btnApriDb); _btnAggiorna.Text = "Aggiorna indice"; _btnAggiorna.Size = new Size(120, 32); _btnAggiorna.Click += (_, _) => AggiornaIndice(); bottoni.Controls.Add(_btnAggiorna); contenuto.Controls.Add(bottoni); return new GroupBox { Text = "1. Indice CAD (caricato automaticamente all'avvio)", Dock = DockStyle.Top, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, Padding = new Padding(12, 8, 12, 12), Margin = new Padding(0, 0, 0, 10), Controls = { contenuto }, }; } private GroupBox BuildGruppoImporta() { var contenuto = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, ColumnCount = 1, }; var lblIstruzioni1 = new Label { AutoSize = true, ForeColor = SystemColors.GrayText, Margin = new Padding(0, 0, 0, 0), Text = "2. Trascina qui il file .xls/.xml esportato dal gestionale (foglio 'Distinta " + "Base'), oppure scegli il file col pulsante sotto: genera un CSV con Componente " + "e Descrizione, in anteprima nella griglia.", }; contenuto.Controls.Add(lblIstruzioni1); var btnSfoglia = new Button { Text = "Scegli file distinta...", Size = new Size(220, 32), Margin = new Padding(0, 8, 0, 16), }; btnSfoglia.Click += (_, _) => ScegliFileDistinta(); contenuto.Controls.Add(btnSfoglia); var lblIstruzioni2 = new Label { AutoSize = true, ForeColor = SystemColors.GrayText, Text = "3. Premi qui sotto per cercare il percorso CAD di ogni componente " + "nell'indice e copiare i file trovati in una cartella 'raw'.", }; contenuto.Controls.Add(lblIstruzioni2); _btnCreaRaw.Text = "Crea cartella raw"; _btnCreaRaw.Size = new Size(220, 36); _btnCreaRaw.Margin = new Padding(0, 8, 0, 0); _btnCreaRaw.Enabled = false; _btnCreaRaw.FlatStyle = FlatStyle.Flat; _btnCreaRaw.FlatAppearance.BorderSize = 0; _btnCreaRaw.BackColor = ColorePrimario; _btnCreaRaw.ForeColor = Color.White; _btnCreaRaw.Font = new Font(Font, FontStyle.Bold); _btnCreaRaw.Click += (_, _) => CreaCartellaRaw(); contenuto.Controls.Add(_btnCreaRaw); return new GroupBox { Text = "2-3. Importa Distinta Base e crea la cartella raw", Dock = DockStyle.Top, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, Padding = new Padding(12, 8, 12, 12), Controls = { contenuto }, }; } private void ScegliFileDistinta() { using var dialog = new OpenFileDialog { Filter = "File distinta base (*.xls;*.xml)|*.xls;*.xml|Tutti i file|*.*", Title = "Seleziona il file Distinta Base", }; if (dialog.ShowDialog(this) == DialogResult.OK) ImportaDistinta(dialog.FileName); } private void ImportaDistinta(string percorso) { try { var (csv, righe) = DistintaBaseImporter.ConvertiInCsv(percorso); _percorsoInputCorrente = percorso; _righeCorrenti = righe; _gridImport.Rows.Clear(); foreach (var (componente, descrizione) in righe) { _gridImport.Rows.Add(componente, descrizione, ""); } _btnCreaRaw.Enabled = righe.Count > 0; ImpostaStatoImport($"CSV generato ({righe.Count} righe): {csv}", Color.DarkGreen); } catch (Exception ex) { _percorsoInputCorrente = null; _righeCorrenti = null; _btnCreaRaw.Enabled = false; _gridImport.Rows.Clear(); ImpostaStatoImport($"Errore durante la conversione: {ex.Message}", Color.DarkRed); } } /// Imposta testo/colore dello stato import e il tooltip con il testo completo /// (la label tronca con "..." quando il percorso non ci sta in una riga). private void ImpostaStatoImport(string testo, Color colore) { _lblImportStatus.ForeColor = colore; _lblImportStatus.Text = testo; _tipImportStatus.SetToolTip(_lblImportStatus, testo); } private void CreaCartellaRaw() { if (_percorsoInputCorrente is null || _righeCorrenti is null) return; var percorsiTrovati = TrovaPercorsiCad(_righeCorrenti.Select(r => r.Componente)); for (int i = 0; i < _righeCorrenti.Count; i++) { string percorsoCad = percorsiTrovati.TryGetValue(_righeCorrenti[i].Componente, out var lista) ? string.Join("; ", lista) : ""; _gridImport.Rows[i].Cells[2].Value = percorsoCad; } var cartellaRaw = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(_percorsoInputCorrente))!, "raw"); int copiati = CopiaFileTrovati(percorsiTrovati.Values.SelectMany(p => p).Distinct(), cartellaRaw); ImpostaStatoImport( $"{percorsiTrovati.Count} componenti con percorso CAD trovato su {_righeCorrenti.Count}. " + $"{copiati} file copiati in {cartellaRaw}", Color.DarkGreen); } /// Copia i file trovati nella cartella "raw" indicata, creandola se necessario. /// Ritorna il numero di file copiati con successo. private static int CopiaFileTrovati(IEnumerable percorsi, string cartellaRaw) { int copiati = 0; foreach (var percorso in percorsi) { try { Directory.CreateDirectory(cartellaRaw); File.Copy(percorso, Path.Combine(cartellaRaw, Path.GetFileName(percorso)), overwrite: true); copiati++; } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { // ponytail: file non raggiungibile (share offline, permessi) -> lo si salta, non blocca gli altri } } return copiati; } /// Cerca, per ogni codice componente, i percorsi dei file CAD (ultima versione, /// solo prt/asm) corrispondenti nell'indice. private Dictionary> TrovaPercorsiCad(IEnumerable codici) { var risultato = new Dictionary>(); if (_dbPath is null) return risultato; using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly"); connection.Open(); var nomiParametri = TipiPredefiniti.Select((_, i) => $"@tipo{i}"); using var command = connection.CreateCommand(); command.CommandText = $""" SELECT percorso_completo FROM files_ultima_versione WHERE codice = @codice AND tipo_file IN ({string.Join(",", nomiParametri)}) ORDER BY tipo_file """; var pCodice = command.Parameters.Add("@codice", Microsoft.Data.Sqlite.SqliteType.Text); for (int i = 0; i < TipiPredefiniti.Length; i++) { command.Parameters.AddWithValue($"@tipo{i}", TipiPredefiniti[i]); } foreach (var codice in codici.Distinct()) { pCodice.Value = codice; using var reader = command.ExecuteReader(); var percorsi = new List(); while (reader.Read()) { percorsi.Add(reader.GetString(0)); } if (percorsi.Count > 0) risultato[codice] = percorsi; } return risultato; } private static readonly string PercorsoImpostazioni = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "CadSearchGui", "ultimo_db.txt"); private void InizializzaDatabase() { var candidati = new[] { LeggiUltimoDbUsato(), Path.Combine(AppContext.BaseDirectory, "cad_index.db"), Path.Combine(Directory.GetCurrentDirectory(), "cad_index.db"), }; _dbPath = candidati.FirstOrDefault(p => p is not null && File.Exists(p)); if (_dbPath is null) { _lblStatus.Text = "Database non trovato. Usa 'Apri DB...' per selezionarlo oppure 'Aggiorna indice' per crearlo."; _lblDb.Text = "Nessun database caricato. Premi 'Aggiorna indice' per crearlo da X:\\, oppure 'Apri DB...' se ne hai gi\u00e0 uno."; return; } CaricaDatabase(_dbPath); } private void ScegliDatabase() { using var dialog = new OpenFileDialog { Filter = "Database SQLite (*.db)|*.db|Tutti i file (*.*)|*.*", Title = "Seleziona cad_index.db", }; if (dialog.ShowDialog(this) == DialogResult.OK) { CaricaDatabase(dialog.FileName); } } private void CaricaDatabase(string path) { try { using var connection = new SqliteConnection($"Data Source={path};Mode=ReadOnly"); connection.Open(); long totale = (long)(new SqliteCommand("SELECT COUNT(*) FROM files", connection).ExecuteScalar() ?? 0L); _dbPath = path; _lblDb.Text = $"Database: {path} ({totale:N0} file indicizzati)"; _lblStatus.Text = "Pronto."; SalvaUltimoDbUsato(path); } catch (Exception ex) { MessageBox.Show(this, $"Impossibile aprire il database:\n{ex.Message}", "Errore", MessageBoxButtons.OK, MessageBoxIcon.Error); } } /// Ricorda l'ultimo DB caricato con successo, così al prossimo avvio si /// ricarica automaticamente anche se l'eseguibile gira da una cartella diversa. private static string? LeggiUltimoDbUsato() { try { return File.Exists(PercorsoImpostazioni) ? File.ReadAllText(PercorsoImpostazioni).Trim() : null; } catch (IOException) { return null; } } private static void SalvaUltimoDbUsato(string path) { try { Directory.CreateDirectory(Path.GetDirectoryName(PercorsoImpostazioni)!); File.WriteAllText(PercorsoImpostazioni, path); } catch (IOException) { // ponytail: non critico se non si riesce a salvare, si torna a chiedere "Apri DB..." la prossima volta } } private async void AggiornaIndice() { string dbPath = _dbPath ?? Path.Combine(AppContext.BaseDirectory, "cad_index.db"); if (!Directory.Exists(RadiceDiscoDefault)) { MessageBox.Show(this, $"Disco non raggiungibile: {RadiceDiscoDefault}", "Errore", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } _btnAggiorna.Enabled = false; _lblStatus.Text = "Aggiornamento indice in corso..."; using var cts = new CancellationTokenSource(); using var dialog = new ProgressForm(); var progress = new Progress(dialog.Aggiorna); dialog.Annullato += (_, _) => cts.Cancel(); var scanTask = Indexer.ScanAsync(RadiceDiscoDefault, dbPath, progress, cts.Token); // Chiude automaticamente il dialog quando lo scan termina (successo, errore o annullamento) _ = scanTask.ContinueWith(_ => { if (dialog.Visible) { dialog.BeginInvoke(dialog.Close); } }, TaskScheduler.Default); dialog.ShowDialog(this); try { await scanTask; _lblStatus.Text = "Indice aggiornato."; CaricaDatabase(dbPath); } catch (OperationCanceledException) { _lblStatus.Text = "Aggiornamento annullato."; } catch (Exception ex) { MessageBox.Show(this, $"Errore durante l'aggiornamento dell'indice:\n{ex.Message}", "Errore", MessageBoxButtons.OK, MessageBoxIcon.Error); _lblStatus.Text = "Aggiornamento fallito."; } finally { _btnAggiorna.Enabled = true; } } private void ApriCartellaFileSelezionato() { if (_gridImport.CurrentRow is null) { return; } string? percorsi = _gridImport.CurrentRow.Cells[2].Value as string; if (string.IsNullOrEmpty(percorsi)) { return; } string primoPercorso = percorsi.Split("; ", 2)[0]; try { Process.Start("explorer.exe", $"/select,\"{primoPercorso}\""); } catch (Exception ex) { MessageBox.Show(this, $"Impossibile aprire Esplora risorse:\n{ex.Message}", "Errore", MessageBoxButtons.OK, MessageBoxIcon.Error); } } }