diff --git a/.gitignore b/.gitignore index 87c9f54..461d4c3 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,9 @@ publish/ # Stato locale degli strumenti agentici .claude/ + +# File Excel/XML della Distinta Base (esportati dal gestionale) e il CSV generato +/*.xls +/*.xlsx +/*.xls.xml +/*.csv diff --git a/CadSearchGui/CadSearchGui.csproj b/CadSearchGui/CadSearchGui.csproj index 4093c4e..77840f6 100644 --- a/CadSearchGui/CadSearchGui.csproj +++ b/CadSearchGui/CadSearchGui.csproj @@ -10,6 +10,8 @@ + + \ No newline at end of file diff --git a/CadSearchGui/DistintaBaseImporter.cs b/CadSearchGui/DistintaBaseImporter.cs new file mode 100644 index 0000000..cb33d1d --- /dev/null +++ b/CadSearchGui/DistintaBaseImporter.cs @@ -0,0 +1,109 @@ +using System.Text; +using System.Xml.Linq; +using ExcelDataReader; + +namespace CadSearchGui; + +/// +/// Converte il file "Distinta Base" esportato dal gestionale (SpreadsheetML +/// XML con estensione .xls/.xml, oppure .xls/.xlsx binario vero) nelle sole +/// colonne Componente/Descrizione in CSV. +/// +static class DistintaBaseImporter +{ + private static readonly XNamespace Ss = "urn:schemas-microsoft-com:office:spreadsheet"; + private const string NomeFoglio = "Distinta Base"; + + static DistintaBaseImporter() + { + Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); + } + + public static (string PercorsoCsv, List<(string Componente, string Descrizione)> Righe) ConvertiInCsv(string percorsoInput) + { + var righeGrezze = EImmagineSpreadsheetMl(percorsoInput) + ? LeggiRigheSpreadsheetMl(percorsoInput) + : LeggiRigheExcelBinario(percorsoInput); + + var righe = righeGrezze.Skip(1) // salta l'intestazione + .Where(r => r.Componente.Length > 0 || r.Descrizione.Length > 0) + .ToList(); + + var percorsoCsv = Path.Combine( + Path.GetDirectoryName(Path.GetFullPath(percorsoInput))!, + Path.GetFileNameWithoutExtension(percorsoInput) + ".csv"); + + using var writer = new StreamWriter(percorsoCsv, false, Encoding.UTF8); + writer.WriteLine("Componente;Descrizione"); + foreach (var (componente, descrizione) in righe) + { + writer.WriteLine($"{EscapeCsv(componente)};{EscapeCsv(descrizione)}"); + } + + return (percorsoCsv, righe); + } + + /// I file XML "SpreadsheetML" iniziano con l'intestazione XML testuale; + /// i file .xls/.xlsx binari veri iniziano invece con una firma binaria (OLE2 o ZIP). + private static bool EImmagineSpreadsheetMl(string percorso) + { + Span primiByte = stackalloc byte[5]; + using var stream = File.OpenRead(percorso); + var letti = stream.Read(primiByte); + return letti >= 5 && Encoding.ASCII.GetString(primiByte) == " LeggiRigheSpreadsheetMl(string percorsoInput) + { + var doc = XDocument.Load(percorsoInput); + var foglio = doc.Descendants(Ss + "Worksheet") + .FirstOrDefault(w => (string?)w.Attribute(Ss + "Name") == NomeFoglio) + ?? throw new InvalidDataException($"Foglio '{NomeFoglio}' non trovato nel file."); + + var righeXml = foglio.Descendants(Ss + "Row").ToList(); + if (righeXml.Count == 0) + throw new InvalidDataException($"Il foglio '{NomeFoglio}' non contiene righe."); + + return righeXml.Select(LeggiColonneBeCSpreadsheetMl).ToList(); + } + + private static (string Componente, string Descrizione) LeggiColonneBeCSpreadsheetMl(XElement riga) + { + string b = "", c = ""; + int colonna = 0; + foreach (var cella in riga.Elements(Ss + "Cell")) + { + // ss:Index salta le celle vuote iniziali: senza attributo la colonna è quella precedente + 1 + var indice = cella.Attribute(Ss + "Index"); + colonna = indice != null ? int.Parse(indice.Value) : colonna + 1; + if (colonna == 2) b = cella.Element(Ss + "Data")?.Value ?? ""; + else if (colonna == 3) c = cella.Element(Ss + "Data")?.Value ?? ""; + } + return (b, c); + } + + private static List<(string Componente, string Descrizione)> LeggiRigheExcelBinario(string percorsoInput) + { + using var stream = File.Open(percorsoInput, FileMode.Open, FileAccess.Read, FileShare.Read); + using var reader = ExcelReaderFactory.CreateReader(stream); + var dataSet = reader.AsDataSet(); + var tabella = dataSet.Tables[NomeFoglio] + ?? throw new InvalidDataException($"Foglio '{NomeFoglio}' non trovato nel file."); + + var righe = new List<(string Componente, string Descrizione)>(); + foreach (System.Data.DataRow riga in tabella.Rows) + { + var b = riga.Table.Columns.Count > 1 ? riga[1]?.ToString() ?? "" : ""; + var c = riga.Table.Columns.Count > 2 ? riga[2]?.ToString() ?? "" : ""; + righe.Add((b, c)); + } + return righe; + } + + private static string EscapeCsv(string valore) + { + if (valore.Contains(';') || valore.Contains('"') || valore.Contains('\n')) + return $"\"{valore.Replace("\"", "\"\"")}\""; + return valore; + } +} diff --git a/CadSearchGui/Form1.cs b/CadSearchGui/Form1.cs index 00638ad..05345c2 100644 --- a/CadSearchGui/Form1.cs +++ b/CadSearchGui/Form1.cs @@ -15,6 +15,8 @@ public partial class Form1 : Form private readonly DataGridView _grid = new(); private readonly Label _lblStatus = new(); private readonly Label _lblDb = new(); + private readonly DataGridView _gridImport = new(); + private readonly Label _lblImportStatus = new(); private const string OpzionePrtAsm = "Solo prt/asm (predefinito)"; private const string OpzioneTuttiTipi = "Tutti i tipi"; @@ -120,9 +122,96 @@ public partial class Form1 : Form _lblStatus.Text = "Pronto."; statusPanel.Controls.Add(_lblStatus); - Controls.Add(_grid); - Controls.Add(statusPanel); - Controls.Add(topPanel); + var tabRicerca = new TabPage("Ricerca CAD"); + tabRicerca.Controls.Add(_grid); + tabRicerca.Controls.Add(statusPanel); + tabRicerca.Controls.Add(topPanel); + + var tabs = new TabControl { Dock = DockStyle.Fill }; + tabs.TabPages.Add(BuildTabImportaDistinta()); + tabs.TabPages.Add(tabRicerca); + + Controls.Add(tabs); + } + + private TabPage BuildTabImportaDistinta() + { + var tab = new TabPage("Importa Distinta Base") { AllowDrop = true }; + + var lblIstruzioni = new Label + { + Dock = DockStyle.Top, + Height = 60, + Padding = new Padding(8), + Text = "Trascina qui il file .xls/.xml esportato dal gestionale (foglio 'Distinta " + + "Base'), oppure scegli il file col pulsante sotto. Verrà generato un CSV con " + + "le colonne Componente e Descrizione, con anteprima qui sotto.", + }; + + var btnSfoglia = new Button { Text = "Scegli file...", Dock = DockStyle.Top, Height = 32 }; + btnSfoglia.Click += (_, _) => ScegliFileDistinta(); + + _lblImportStatus.Dock = DockStyle.Top; + _lblImportStatus.Height = 24; + _lblImportStatus.Padding = new Padding(8, 4, 8, 4); + _lblImportStatus.ForeColor = SystemColors.GrayText; + + _gridImport.Dock = DockStyle.Fill; + _gridImport.ReadOnly = true; + _gridImport.AllowUserToAddRows = false; + _gridImport.AllowUserToDeleteRows = false; + _gridImport.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; + _gridImport.Columns.Add("componente", "Componente"); + _gridImport.Columns.Add("descrizione", "Descrizione"); + + tab.Controls.Add(_gridImport); + tab.Controls.Add(_lblImportStatus); + tab.Controls.Add(btnSfoglia); + tab.Controls.Add(lblIstruzioni); + + tab.DragEnter += (_, e) => + e.Effect = e.Data?.GetDataPresent(DataFormats.FileDrop) == true + ? DragDropEffects.Copy + : DragDropEffects.None; + tab.DragDrop += (_, e) => + { + if (e.Data?.GetData(DataFormats.FileDrop) is string[] { Length: > 0 } file) + ImportaDistinta(file[0]); + }; + + return tab; + } + + 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); + _gridImport.Rows.Clear(); + foreach (var (componente, descrizione) in righe) + { + _gridImport.Rows.Add(componente, descrizione); + } + _lblImportStatus.ForeColor = Color.DarkGreen; + _lblImportStatus.Text = $"CSV generato ({righe.Count} righe): {csv}"; + } + catch (Exception ex) + { + _gridImport.Rows.Clear(); + _lblImportStatus.ForeColor = Color.DarkRed; + _lblImportStatus.Text = $"Errore durante la conversione: {ex.Message}"; + } } private void InizializzaDatabase()