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 Button _btnTrovaPrt = new(); private readonly Button _btnPulisci = 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; private string? _ultimaCartellaRaw; 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); var lblIstruzioni3 = new Label { AutoSize = true, ForeColor = SystemColors.GrayText, Margin = new Padding(0, 12, 0, 0), Text = "4. Gli assiemi referenziano altri PRT e sotto-assiemi al loro interno: premi " + "qui per risolvere l'albero componenti (ricorsivamente) e copiare tutto ciò che " + "viene trovato nell'indice dentro 'raw'.", }; contenuto.Controls.Add(lblIstruzioni3); _btnTrovaPrt.Text = "Trova componenti referenziati"; _btnTrovaPrt.Size = new Size(220, 32); _btnTrovaPrt.Margin = new Padding(0, 8, 0, 0); _btnTrovaPrt.Enabled = false; _btnTrovaPrt.Click += (_, _) => TrovaPrtReferenziati(); contenuto.Controls.Add(_btnTrovaPrt); var lblManutenzione = new Label { AutoSize = true, ForeColor = SystemColors.GrayText, Margin = new Padding(0, 16, 0, 0), Text = "Manutenzione: svuota le cartelle 'raw' e 'step' nella root prima di un " + "nuovo giro (non tocca l'indice, solo le copie locali).", }; contenuto.Controls.Add(lblManutenzione); _btnPulisci.Text = "Pulisci raw e step"; _btnPulisci.Size = new Size(200, 30); _btnPulisci.Margin = new Padding(0, 6, 0, 0); _btnPulisci.Click += (_, _) => PulisciRawEStep(); contenuto.Controls.Add(_btnPulisci); return new GroupBox { Text = "2-4. Importa Distinta Base, crea la cartella raw e risolvi i componenti referenziati", 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); _ultimaCartellaRaw = cartellaRaw; _btnTrovaPrt.Enabled = true; ImpostaStatoImport( $"{percorsiTrovati.Count} componenti con percorso CAD trovato su {_righeCorrenti.Count}. " + $"{copiati} file copiati in {cartellaRaw}", Color.DarkGreen); } /// Svuota in un colpo solo le cartelle "raw" e "step" nella root (stessa cartella /// base del file distinta importato), previa un'unica conferma. Non tocca l'indice CAD: solo /// le copie locali generate dalla pipeline. private void PulisciRawEStep() { string radice; if (_ultimaCartellaRaw is not null) { radice = Path.GetDirectoryName(_ultimaCartellaRaw)!; } else { // Non abbiamo ancora una cartella nota in questa sessione (es. app appena riaperta): // si chiede all'utente di indicare la root invece di tenere il pulsante disabilitato. using var dialog = new FolderBrowserDialog { Description = "Seleziona la cartella root (contiene 'raw' e 'step')" }; if (dialog.ShowDialog(this) != DialogResult.OK) return; radice = dialog.SelectedPath; } var cartelle = new[] { Path.Combine(radice, "raw"), Path.Combine(radice, "step") } .Where(Directory.Exists) .ToList(); if (cartelle.Count == 0) { ImpostaStatoImport($"Nessuna cartella 'raw' o 'step' trovata in {radice}.", Color.DarkRed); return; } var conferma = MessageBox.Show(this, $"Eliminare tutti i file in:\n{string.Join("\n", cartelle)}\n\nL'operazione non è reversibile.", "Pulisci raw e step", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (conferma != DialogResult.Yes) return; int eliminati = 0; foreach (var cartella in cartelle) { foreach (var file in Directory.GetFiles(cartella)) { try { File.Delete(file); eliminati++; } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { // ponytail: file in uso (es. aperto in Creo) -> si salta, non blocca gli altri } } } ImpostaStatoImport($"{eliminati} file eliminati da {string.Join(" e ", cartelle)}.", Color.DarkGreen); } /// Risolve ricorsivamente l'albero dei componenti a partire dagli .asm in "raw": /// per ogni assieme legge i codici referenziati al suo interno, li cerca nell'indice, copia /// in "raw" quelli trovati e, se sono a loro volta assiemi, li accoda per essere scansionati /// finché non restano solo PRT (foglie) o codici non presenti nell'indice. private void TrovaPrtReferenziati() { if (_ultimaCartellaRaw is null || !Directory.Exists(_ultimaCartellaRaw)) return; if (_dbPath is null) { ImpostaStatoImport("Nessun database caricato: impossibile risolvere i componenti referenziati.", Color.DarkRed); return; } var daScansionare = new Queue( Directory.GetFiles(_ultimaCartellaRaw).Where(f => Path.GetFileName(f).Contains(".asm."))); var codiciVisti = new HashSet(daScansionare.Select(f => Path.GetFileName(f).Split('.')[0])); var trovati = new List<(string Codice, string Tipo, string Percorso)>(); int codiciNonTrovati = 0; using (var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly")) { connection.Open(); using var command = connection.CreateCommand(); command.CommandText = "SELECT tipo_file, percorso_completo FROM files_ultima_versione " + "WHERE codice = @codice AND tipo_file IN ('prt', 'asm')"; var pCodice = command.Parameters.Add("@codice", Microsoft.Data.Sqlite.SqliteType.Text); while (daScansionare.Count > 0) { var asm = daScansionare.Dequeue(); foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm)) { if (!codiciVisti.Add(codice)) continue; pCodice.Value = codice; var componenti = new List<(string Tipo, string Percorso)>(); using (var reader = command.ExecuteReader()) { while (reader.Read()) { componenti.Add((reader.GetString(0), reader.GetString(1))); } } if (componenti.Count == 0) { codiciNonTrovati++; continue; } foreach (var (tipo, percorso) in componenti) { CopiaFileTrovati(new[] { percorso }, _ultimaCartellaRaw); trovati.Add((codice, tipo, percorso)); if (tipo == "asm") { daScansionare.Enqueue(Path.Combine(_ultimaCartellaRaw, Path.GetFileName(percorso))); } } } } } int numPrt = trovati.Count(r => r.Tipo == "prt"); int numAsm = trovati.Count(r => r.Tipo == "asm"); ImpostaStatoImport( $"Componenti referenziati: {numPrt} PRT e {numAsm} sotto-assiemi trovati e copiati in raw, " + $"{codiciNonTrovati} codici referenziati non trovati nell'indice.", Color.DarkGreen); using var dialog = new PrtMancantiForm(trovati, codiciNonTrovati, _ultimaCartellaRaw); dialog.ShowDialog(this); } /// Copia i file trovati nella cartella "raw" indicata, creandola se necessario. /// Ritorna il numero di file copiati con successo. internal 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); } } }