diff --git a/.gitignore b/.gitignore index 461d4c3..4183425 100644 --- a/.gitignore +++ b/.gitignore @@ -20,8 +20,11 @@ publish/ # Stato locale degli strumenti agentici .claude/ -# File Excel/XML della Distinta Base (esportati dal gestionale) e il CSV generato +# File Excel/XML della Distinta Base (esportati dal gestionale), il CSV generato +# e la cartella con le copie dei file CAD trovati /*.xls /*.xlsx /*.xls.xml /*.csv +/raw/ +/step/ diff --git a/CadSearchGui/Form1.cs b/CadSearchGui/Form1.cs index 99cb14c..d34e7a6 100644 --- a/CadSearchGui/Form1.cs +++ b/CadSearchGui/Form1.cs @@ -7,15 +7,19 @@ 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() { @@ -24,84 +28,72 @@ public partial class Form1 : Form 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 topPanel = new TableLayoutPanel + var contenutoTop = new TableLayoutPanel { Dock = DockStyle.Top, - Height = 108, - ColumnCount = 3, - RowCount = 3, - Padding = new Padding(8), + AutoSize = true, + AutoSizeMode = AutoSizeMode.GrowAndShrink, + ColumnCount = 1, + Padding = new Padding(12, 12, 12, 0), }; - topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); - topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 130)); - topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 130)); - - var lblIstruzioni = new Label - { - Dock = DockStyle.Fill, - AutoSize = false, - Text = "Trascina qui il file .xls/.xml esportato dal gestionale (foglio 'Distinta " - + "Base'), oppure scegli il file col pulsante 'Scegli file...'. Viene generato " - + "un CSV con Componente/Descrizione e, per ogni componente, il percorso del " - + "file CAD corrispondente trovato nell'indice.", - }; - topPanel.SetColumnSpan(lblIstruzioni, 3); - topPanel.Controls.Add(lblIstruzioni, 0, 0); - - var btnSfoglia = new Button { Text = "Scegli file distinta...", Dock = DockStyle.Fill }; - btnSfoglia.Click += (_, _) => ScegliFileDistinta(); - topPanel.Controls.Add(btnSfoglia, 0, 1); - - _btnApriDb.Text = "Apri DB..."; - _btnApriDb.Dock = DockStyle.Fill; - _btnApriDb.Click += (_, _) => ScegliDatabase(); - topPanel.Controls.Add(_btnApriDb, 1, 1); - - _btnAggiorna.Text = "Aggiorna indice"; - _btnAggiorna.Dock = DockStyle.Fill; - _btnAggiorna.Click += (_, _) => AggiornaIndice(); - topPanel.Controls.Add(_btnAggiorna, 2, 1); - - _lblDb.Dock = DockStyle.Fill; - _lblDb.AutoSize = false; - _lblDb.ForeColor = SystemColors.GrayText; - _lblDb.TextAlign = ContentAlignment.MiddleLeft; - topPanel.SetColumnSpan(_lblDb, 3); - topPanel.Controls.Add(_lblDb, 0, 2); + contenutoTop.Controls.Add(BuildGruppoDatabase()); + contenutoTop.Controls.Add(BuildGruppoImporta()); _lblImportStatus.Dock = DockStyle.Top; - _lblImportStatus.Height = 24; - _lblImportStatus.Padding = new Padding(8, 4, 8, 4); + _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 }; + var statusPanel = new Panel { Dock = DockStyle.Bottom, Height = 28, BackColor = SystemColors.ControlLight }; _lblStatus.Dock = DockStyle.Fill; _lblStatus.TextAlign = ContentAlignment.MiddleLeft; - _lblStatus.Padding = new Padding(8, 0, 0, 0); + _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(topPanel); + Controls.Add(contenutoTop); DragEnter += (_, e) => e.Effect = e.Data?.GetDataPresent(DataFormats.FileDrop) == true @@ -114,6 +106,112 @@ public partial class Form1 : Form }; } + 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 @@ -130,31 +228,82 @@ public partial class Form1 : Form try { var (csv, righe) = DistintaBaseImporter.ConvertiInCsv(percorso); - var percorsiTrovati = TrovaPercorsiCad(righe.Select(r => r.Componente)); + _percorsoInputCorrente = percorso; + _righeCorrenti = righe; _gridImport.Rows.Clear(); - int trovati = 0; foreach (var (componente, descrizione) in righe) { - string percorsoCad = percorsiTrovati.TryGetValue(componente, out var lista) - ? string.Join("; ", lista) - : ""; - if (percorsoCad.Length > 0) - trovati++; - _gridImport.Rows.Add(componente, descrizione, percorsoCad); + _gridImport.Rows.Add(componente, descrizione, ""); } + _btnCreaRaw.Enabled = righe.Count > 0; - _lblImportStatus.ForeColor = Color.DarkGreen; - _lblImportStatus.Text = $"CSV generato ({righe.Count} righe, {trovati} con percorso CAD trovato): {csv}"; + ImpostaStatoImport($"CSV generato ({righe.Count} righe): {csv}", Color.DarkGreen); } catch (Exception ex) { + _percorsoInputCorrente = null; + _righeCorrenti = null; + _btnCreaRaw.Enabled = false; _gridImport.Rows.Clear(); - _lblImportStatus.ForeColor = Color.DarkRed; - _lblImportStatus.Text = $"Errore durante la conversione: {ex.Message}"; + 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) @@ -195,19 +344,23 @@ public partial class Form1 : Form 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(File.Exists); + _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."; - _lblDb.Text = "Nessun database caricato."; + _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; } @@ -239,6 +392,7 @@ public partial class Form1 : Form _dbPath = path; _lblDb.Text = $"Database: {path} ({totale:N0} file indicizzati)"; _lblStatus.Text = "Pronto."; + SalvaUltimoDbUsato(path); } catch (Exception ex) { @@ -247,6 +401,33 @@ public partial class Form1 : Form } } + /// 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"); diff --git a/README.md b/README.md new file mode 100644 index 0000000..60c6c42 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# CadSearchGui + +App Windows Forms (.NET 10) per indicizzare, cercare e recuperare i file CAD +Pro-ENGINEER/Creo di un disco/share di rete, e per trasformare l'estrazione +"Distinta Base" del gestionale (file `.xls`/`.xml`) in un CSV con il percorso +CAD di ogni componente. + +## Funzionalità + +1. **Indice CAD** — all'avvio carica automaticamente `cad_index.db` (se + presente accanto all'eseguibile). Se manca, si può selezionare un DB + esistente con **Apri DB...** o generarne uno nuovo con **Aggiorna + indice**, che scansiona il disco (di default `X:\`, cartelle + `__Accessori`, `__B.end`, `__Corpi`, `__Fondi`) e indicizza tutti i file. +2. **Importa Distinta Base** — trascina (o scegli da file) l'estrazione + `.xls`/`.xml` del gestionale (foglio "Distinta Base", colonne + Componente/Descrizione): viene generato un CSV con anteprima in griglia. +3. **Crea cartella raw** — cerca nell'indice il percorso CAD di ogni + componente importato e copia i file trovati in una cartella `raw/` + accanto al file di distinta originale. + +## Requisiti + +- Windows +- [.NET 10 SDK](https://dotnet.microsoft.com/download) (per build/run da sorgente) +- Accesso alla share di rete con i file CAD (`X:\` di default) per + l'indicizzazione + +## Build + +```powershell +cd CadSearchGui +dotnet build -c Release +``` + +## Avvio + +Da sorgente: + +```powershell +cd CadSearchGui +dotnet run -c Release +``` + +Oppure l'eseguibile già compilato: + +```powershell +cd CadSearchGui\bin\Release\net10.0-windows +.\CadSearchGui.exe +``` + +## Pubblicazione (eseguibile standalone) + +Genera un `.exe` self-contained per Windows x64, senza bisogno del runtime +.NET installato sulla macchina di destinazione: + +```powershell +cd CadSearchGui +dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o publish +``` + +L'eseguibile risultante è in `CadSearchGui\publish\CadSearchGui.exe`. + +## Struttura del progetto + +Tutto il codice è in `CadSearchGui/` (unico progetto nella solution, GUI e +indicizzazione integrate): + +- `Program.cs` — entry point. +- `Form1.cs` — finestra principale (indice CAD, import Distinta Base, griglia + risultati); UI costruita interamente in codice, nessun designer visuale. +- `Indexer.cs` — scansione del disco e scrittura/aggiornamento del DB SQLite. +- `DistintaBaseImporter.cs` — parsing del file Distinta Base (SpreadsheetML + XML o Excel binario/OpenXML tramite ExcelDataReader) e generazione CSV. +- `ProgressForm.cs` — dialog di avanzamento durante l'aggiornamento indice. + +Il database generato (`cad_index.db`), i file Excel/CSV della distinta +trascinati nella root e la cartella `raw/` sono rigenerabili e non sono +tracciati in git (vedi `.gitignore`). + +Non ci sono test automatizzati nel repo.