Add wizard UI, general-assembly resolution, and test suite

Restructure Form1 into a sequential wizard (database -> import -> raw ->
recursive component resolution -> completeness/STEP report), fix
case-sensitivity bugs in codice/tipo_file lookups and a word-boundary gap
in the assembly scanner that missed _D100-style sub-assembly references,
and treat the distinta's general assembly as an ordinary CSV row (derived
from the DBP_<codice>_<timestamp> filename) instead of special-casing it
throughout. Also adds CadSearchGui.Tests (xUnit) covering the scanner,
filename parsing, and the case-insensitive DB lookup behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5 committed 2026-09-16 16:25:52 +02:00
1 parent 146ce78eae
commit 52483177f0
16 files changed
+1144 -249

No files matched your search

+4
View File
@@ -29,3 +29,7 @@ publish/
/raw/ /raw/
/step/ /step/
*.dxc *.dxc
# Report BOM generati manualmente da Creo per debug/validazione: mai una dipendenza
# della pipeline (che deve funzionare senza Creo), solo un aiuto occasionale.
*.bom.*
+34 -17
View File
@@ -5,9 +5,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Cos'è questo progetto ## Cos'è questo progetto
`CadSearchGui` è un'app Windows Forms (.NET 10, WinForms) che indicizza e permette di `CadSearchGui` è un'app Windows Forms (.NET 10, WinForms) che indicizza e permette di
cercare i file CAD Pro-ENGINEER/Creo contenuti in sei cartelle di un disco/share cercare i file CAD Pro-ENGINEER/Creo contenuti in tutte le cartelle di primo livello
di rete (`__Accessori`, `__B.end`, `__Corpi`, `__Fondi`, `Unificati`, `Commerciali` sotto una radice, di un disco/share di rete (radice di default `X:\`), salvando i metadati in un DB
di default `X:\`), salvando i metadati in un DB SQLite locale (`cad_index.db`). SQLite locale (`cad_index.db`).
## Comandi ## Comandi
@@ -24,7 +24,12 @@ Pubblicazione come eseguibile standalone (single-file, self-contained, Windows x
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o publish dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o publish
``` ```
Non ci sono test automatizzati nel repo. `CadSearchGui.Tests/` (xUnit) contiene unit e integration test — vedi
`dotnet test` da dentro quella cartella. Riferimento al progetto principale
via `ProjectReference` + `InternalsVisibleTo` (in `CadSearchGui.csproj`), così
i test possono chiamare direttamente `Indexer.ParseFilename` e
`AssemblyComponentScanner.EstraiCodiciReferenziati` senza esporli come API
pubblica.
## Architettura ## Architettura
@@ -33,13 +38,32 @@ passato esisteva anche una CLI separata e uno script Python di indicizzazione,
entrambi rimossi: ora l'unico programma è questo, con GUI e indicizzazione integrate). entrambi rimossi: ora l'unico programma è questo, con GUI e indicizzazione integrate).
- **Program.cs** — entry point WinForms standard, avvia `Form1`. - **Program.cs** — entry point WinForms standard, avvia `Form1`.
- **Form1.cs** / **Form1.Designer.cs** — finestra principale. La UI è costruita - **Form1.cs** / **Form1.Designer.cs** — finestra principale, costruita come wizard: una
interamente in codice in `Form1.BuildUi()` (nessun designer visuale usato); i sola fase alla volta occupa tutta la finestra (`_panelFasi[]`/`NomiFasi`), si avanza
controlli sono campi privati (`_txtCerca`, `_cmbTipo`, `_cmbCartella`, `_chkUltima`, con "Avanti" solo quando la fase corrente è pronta (`AvantiConsentito`). La UI è
`_grid`, ecc.). `Form1.Designer.cs` contiene solo lo scheletro minimo costruita interamente in codice in `Form1.BuildUi()` (nessun designer visuale usato).
(`InitializeComponent`/`Dispose`) generato dal template. `Form1.Designer.cs` contiene solo lo scheletro minimo (`InitializeComponent`/`Dispose`)
generato dal template. Le fasi, in ordine:
1. **Database** — carica/aggiorna l'indice, mostra data ultimo aggiornamento; qui vive
anche "Pulisci raw e step" (manutenzione, non fa parte del flusso a valle).
2. **Importa Distinta Base** — `DistintaBaseImporter.ConvertiInCsv` genera il CSV dei
componenti; se il nome del file segue la convenzione del gestionale
(`DBP_<codice>_<timestamp>`), l'assieme generale viene aggiunto come riga in più nel
CSV stesso (il foglio elenca solo i componenti sotto l'assieme, non l'assieme in sé).
Da qui in avanti l'assieme generale è un componente come tutti gli altri, nessun
caso speciale nel resto del codice.
3. **Crea cartella raw** — cerca nell'indice il percorso CAD di ogni componente del CSV
(`TrovaPercorsiCad`) e lo copia in `raw/` (`CopiaFileTrovati`).
4. **Trova componenti referenziati** — legge euristicamente i metadati binari dei `.asm`
già in `raw/` (`AssemblyComponentScanner`) per risolvere ricorsivamente i
sotto-assiemi/parti che contengono (`RisolviAlberoComponenti`), copiandoli anch'essi
in `raw/`.
5. **Verifica completezza e STEP** — "Verifica completezza assiemi" (ricorsivo sui
percorsi originali in indice, non su `raw/`) e "Report STEP realizzati" (confronto
con `step/`).
- **Indexer.cs** — logica di scansione del disco e scrittura/aggiornamento del DB - **Indexer.cs** — logica di scansione del disco e scrittura/aggiornamento del DB
SQLite (`Indexer.ScanAsync`). Cammina le 6 cartelle radice (`Indexer.CartelleRadice`) SQLite (`Indexer.ScanAsync`). Cammina tutte le cartelle di primo livello trovate sotto
la radice (scoperte dinamicamente con `Directory.GetDirectories`, non un elenco fisso)
con un walk manuale a stack (`Walk`, più veloce di `EnumerateFiles(..., AllDirectories)` con un walk manuale a stack (`Walk`, più veloce di `EnumerateFiles(..., AllDirectories)`
su share di rete con centinaia di migliaia di file), estrae da ogni nome file il su share di rete con centinaia di migliaia di file), estrae da ogni nome file il
pattern Pro-E `<codice>.<tipo>.<versione>` via regex, e fa un upsert incrementale pattern Pro-E `<codice>.<tipo>.<versione>` via regex, e fa un upsert incrementale
@@ -65,13 +89,6 @@ file non più presenti su disco a fine scansione).
Vista `files_ultima_versione`: stessa struttura, filtrata alla sola versione più Vista `files_ultima_versione`: stessa struttura, filtrata alla sola versione più
recente per codice+tipo. recente per codice+tipo.
### Comportamento di ricerca di default
La ricerca di default (nessun filtro esplicito dall'utente) mostra solo i tipi
`prt`/`asm` e solo l'ultima versione — vedi `OpzionePrtAsm`/`TipiPredefiniti` in
`Form1.cs`. L'utente può comunque scegliere "Tutti i tipi" o un tipo specifico dal
combo, e togliere la spunta "Solo ultima versione".
### Percorso del DB ### Percorso del DB
All'avvio la app cerca `cad_index.db` accanto all'eseguibile o nella working All'avvio la app cerca `cad_index.db` accanto all'eseguibile o nella working
@@ -0,0 +1,60 @@
using CadSearchGui;
namespace CadSearchGui.Tests;
public class AssemblyComponentScannerTests
{
private static string ScriviAsmFinto(string testo)
{
string percorso = Path.GetTempFileName();
File.WriteAllBytes(percorso, System.Text.Encoding.Latin1.GetBytes(testo));
return percorso;
}
[Fact]
public void Scarta_codici_con_una_sola_occorrenza()
{
// "11112222" compare 2 volte (riferimento vero), "99998888" una sola (rumore).
string percorso = ScriviAsmFinto("blah 11112222 blah 99998888 blah 11112222 fine");
try
{
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso);
Assert.Contains("11112222", codici);
Assert.DoesNotContain("99998888", codici);
}
finally { File.Delete(percorso); }
}
[Fact]
public void Riconosce_sotto_assiemi_con_suffisso_stile_D100()
{
// Visto su 9244228573.asm: i sotto-assiemi hanno nomi come <codice>_D100/_D200/_D300.
string percorso = ScriviAsmFinto("x 9244228573_D100 y 9244228573_D100 z 9244228573_D200 9244228573_D200");
try
{
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso);
Assert.Contains("9244228573_D100", codici);
Assert.Contains("9244228573_D200", codici);
}
finally { File.Delete(percorso); }
}
[Fact]
public void Non_estrae_nulla_da_una_sequenza_di_cifre_troppo_lunga()
{
// Una sequenza di 15 cifre di fila non contiene nessun confine di parola valido
// per un blocco di 8-12 cifre: non deve produrre falsi positivi troncati.
string percorso = ScriviAsmFinto("123456789012345 123456789012345");
try
{
var codici = AssemblyComponentScanner.EstraiCodiciReferenziati(percorso);
Assert.Empty(codici);
}
finally { File.Delete(percorso); }
}
[Fact]
public void File_inesistente_lancia_eccezione_IO() =>
Assert.Throws<DirectoryNotFoundException>(() =>
AssemblyComponentScanner.EstraiCodiciReferenziati(@"C:\non\esiste\pippo.asm.1"));
}
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.12" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CadSearchGui\CadSearchGui.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>
@@ -0,0 +1,124 @@
using CadSearchGui;
using Microsoft.Data.Sqlite;
namespace CadSearchGui.Tests;
/// <summary>Ricrea lo schema reale del DB (stessa costante SQL usata da Indexer.ScanAsync) e
/// verifica, con dati inseriti a mano, i due comportamenti che in passato hanno causato bug
/// silenziosi: il confronto case-insensitive su codice/tipo_file e la vista
/// files_ultima_versione che tiene solo l'ultima versione per cartella_radice+codice+tipo.</summary>
public class CaseInsensitiveLookupIntegrationTests : IDisposable
{
private readonly string _dbPath = Path.Combine(Path.GetTempPath(), $"cadsearch_test_{Guid.NewGuid():N}.db");
private readonly SqliteConnection _connection;
public CaseInsensitiveLookupIntegrationTests()
{
_connection = new SqliteConnection($"Data Source={_dbPath}");
_connection.Open();
using var schemaCmd = _connection.CreateCommand();
schemaCmd.CommandText = Indexer.Schema;
schemaCmd.ExecuteNonQuery();
}
public void Dispose()
{
_connection.Dispose();
SqliteConnection.ClearPool(_connection);
File.Delete(_dbPath);
}
private void InserisciFile(string percorso, string cartella, string codice, string tipo, int versione, string dataModifica)
{
using var cmd = _connection.CreateCommand();
cmd.CommandText = """
INSERT INTO files (percorso_completo, percorso_relativo, nome_file, cartella_radice,
famiglia, codice, tipo_file, versione, dimensione_byte, data_modifica, scansionato_il)
VALUES (@p, @p, @p, @cartella, NULL, @codice, @tipo, @versione, 1, @modifica, @modifica)
""";
cmd.Parameters.AddWithValue("@p", percorso);
cmd.Parameters.AddWithValue("@cartella", cartella);
cmd.Parameters.AddWithValue("@codice", codice);
cmd.Parameters.AddWithValue("@tipo", tipo);
cmd.Parameters.AddWithValue("@versione", versione);
cmd.Parameters.AddWithValue("@modifica", dataModifica);
cmd.ExecuteNonQuery();
}
/// <summary>Stessa query (LOWER su entrambi i lati) usata da Form1 per risolvere un codice
/// referenziato in un .asm: la scansione euristica del binario può restituire maiuscole
/// diverse da quelle del file reale su disco (es. "_D100" estratto vs "_d100" su X:\).</summary>
private List<(string Tipo, string Percorso)> CercaCasoInsensitive(string codiceCercato)
{
using var cmd = _connection.CreateCommand();
cmd.CommandText = "SELECT tipo_file, percorso_completo FROM files_ultima_versione "
+ "WHERE LOWER(codice) = LOWER(@codice) AND LOWER(tipo_file) IN ('prt', 'asm') "
+ "ORDER BY tipo_file, data_modifica DESC";
cmd.Parameters.AddWithValue("@codice", codiceCercato);
using var reader = cmd.ExecuteReader();
var risultati = new List<(string, string)>();
while (reader.Read())
risultati.Add((reader.GetString(0), reader.GetString(1)));
return risultati;
}
[Fact]
public void Trova_il_file_anche_se_la_maiuscola_del_codice_non_corrisponde()
{
InserisciFile(@"X:\__Corpi\442\M_B\9244228573_d100.asm.16", "__Corpi", "9244228573_d100", "asm", 16, "2018-08-22T14:51:19Z");
var risultati = CercaCasoInsensitive("9244228573_D100");
Assert.Single(risultati);
Assert.Equal("asm", risultati[0].Tipo);
}
[Fact]
public void Trova_il_file_anche_se_il_tipo_e_maiuscolo()
{
InserisciFile(@"X:\Commerciali\Elettrici\1504240807.PRT.1", "Commerciali", "1504240807", "PRT", 1, "2020-01-01T00:00:00Z");
var risultati = CercaCasoInsensitive("1504240807");
Assert.Single(risultati);
Assert.Equal("PRT", risultati[0].Tipo);
}
[Fact]
public void Tra_due_cartelle_radice_diverse_tiene_solo_la_versione_piu_recente()
{
// Stesso codice+tipo duplicato su due cartelle radice diverse (capitato con temp_paris
// vs __Corpi): la vista files_ultima_versione deve restituire il più recente per
// cartella, ma la query applicativa a valle deve poi tenerne solo uno in assoluto
// (comportamento verificato qui a livello di dati grezzi restituiti dalla vista).
InserisciFile(@"X:\temp_paris\gabbia45_modelli_copia\9244228573.asm.1", "temp_paris", "9244228573", "asm", 1, "2014-06-25T07:38:13Z");
InserisciFile(@"X:\__Corpi\442\M_B\9244228573.asm.41", "__Corpi", "9244228573", "asm", 41, "2019-05-08T13:27:23Z");
var risultati = CercaCasoInsensitive("9244228573");
// La vista non deduplica tra cartelle radice diverse: la query applicativa ordina per
// data_modifica DESC e tiene solo la prima per tipo, cioè quella più recente.
Assert.Equal(2, risultati.Count);
Assert.Equal(@"X:\__Corpi\442\M_B\9244228573.asm.41", risultati[0].Percorso);
}
[Fact]
public void La_vista_ultima_versione_scarta_le_versioni_precedenti_nella_stessa_cartella()
{
InserisciFile(@"X:\__Corpi\442\M_B\9244228573.asm.40", "__Corpi", "9244228573", "asm", 40, "2018-08-22T14:51:27Z");
InserisciFile(@"X:\__Corpi\442\M_B\9244228573.asm.41", "__Corpi", "9244228573", "asm", 41, "2019-05-08T13:27:23Z");
var risultati = CercaCasoInsensitive("9244228573");
Assert.Single(risultati);
Assert.Contains(".asm.41", risultati[0].Percorso);
}
[Fact]
public void Nessun_risultato_per_un_codice_non_indicizzato()
{
InserisciFile(@"X:\__Corpi\442\M_B\9244228573.asm.41", "__Corpi", "9244228573", "asm", 41, "2019-05-08T13:27:23Z");
Assert.Empty(CercaCasoInsensitive("0000000000"));
}
}
@@ -0,0 +1,69 @@
using CadSearchGui;
namespace CadSearchGui.Tests;
public class DistintaBaseImporterTests
{
private const string SpreadsheetMlMinimo = """
<?xml version="1.0"?>
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet">
<Worksheet ss:Name="Distinta Base" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">
<Table>
<Row><Cell ss:Index="2"><Data>Componente</Data></Cell><Cell><Data>Descrizione</Data></Cell></Row>
<Row><Cell ss:Index="2"><Data>1234567890</Data></Cell><Cell><Data>Vite</Data></Cell></Row>
</Table>
</Worksheet>
</Workbook>
""";
private static string ScriviFileDistinta(string nomeFile, string contenuto)
{
string percorso = Path.Combine(Path.GetTempPath(), $"cadsearch_test_{Guid.NewGuid():N}", nomeFile);
Directory.CreateDirectory(Path.GetDirectoryName(percorso)!);
File.WriteAllText(percorso, contenuto);
return percorso;
}
[Fact]
public void Aggiunge_assieme_generale_dedotto_dal_nome_file_DBP()
{
string percorso = ScriviFileDistinta("DBP_9244228573_20260916144238.xls", SpreadsheetMlMinimo);
try
{
var (_, righe) = DistintaBaseImporter.ConvertiInCsv(percorso);
Assert.Contains(righe, r => r.Componente == "9244228573" && r.Descrizione == "Assieme generale");
Assert.Contains(righe, r => r.Componente == "1234567890");
}
finally { Directory.Delete(Path.GetDirectoryName(percorso)!, recursive: true); }
}
[Fact]
public void Non_duplica_lassieme_generale_se_gia_presente_tra_i_componenti()
{
// Caso limite: il codice del nome file coincide con un componente già elencato nel foglio.
string contenutoConGeneraleGiaPresente = SpreadsheetMlMinimo.Replace("1234567890", "9244228573");
string percorso = ScriviFileDistinta("DBP_9244228573_20260916144238.xls", contenutoConGeneraleGiaPresente);
try
{
var (_, righe) = DistintaBaseImporter.ConvertiInCsv(percorso);
Assert.Single(righe, r => r.Componente == "9244228573");
}
finally { Directory.Delete(Path.GetDirectoryName(percorso)!, recursive: true); }
}
[Fact]
public void Nessun_assieme_generale_aggiunto_se_il_nome_file_non_segue_il_pattern_DBP()
{
string percorso = ScriviFileDistinta("estrazione_qualsiasi.xls", SpreadsheetMlMinimo);
try
{
var (_, righe) = DistintaBaseImporter.ConvertiInCsv(percorso);
Assert.DoesNotContain(righe, r => r.Descrizione == "Assieme generale");
Assert.Single(righe);
}
finally { Directory.Delete(Path.GetDirectoryName(percorso)!, recursive: true); }
}
}
@@ -0,0 +1,31 @@
using CadSearchGui;
namespace CadSearchGui.Tests;
public class IndexerParseFilenameTests
{
[Theory]
[InlineData("9244228573.asm.41", "9244228573", "asm", 41)]
[InlineData("9244228573_D100.asm.16", "9244228573_D100", "asm", 16)]
[InlineData("1504240807.PRT.1", "1504240807", "PRT", 1)]
public void Estrae_codice_tipo_e_versione_dal_nome_file(
string nomeFile, string codiceAtteso, string tipoAtteso, int versioneAttesa)
{
var (codice, tipo, versione) = Indexer.ParseFilename(nomeFile);
Assert.Equal(codiceAtteso, codice);
Assert.Equal(tipoAtteso, tipo);
Assert.Equal(versioneAttesa, versione);
}
[Theory]
[InlineData("9244228573.edz")]
[InlineData("readme.txt")]
[InlineData("senza_estensioni")]
public void File_senza_pattern_codice_tipo_versione_finisce_in_altro(string nomeFile)
{
var (codice, tipo, versione) = Indexer.ParseFilename(nomeFile);
Assert.Equal(nomeFile, codice);
Assert.Equal("altro", tipo);
Assert.Null(versione);
}
}
+10 -4
View File
@@ -11,19 +11,25 @@ namespace CadSearchGui;
/// vedi Indexer.FilenameRegex) e si lascia poi al chiamante il compito di verificarli /// vedi Indexer.FilenameRegex) e si lascia poi al chiamante il compito di verificarli
/// contro l'indice DB, che scarta automaticamente i falsi positivi (numeri che non /// contro l'indice DB, che scarta automaticamente i falsi positivi (numeri che non
/// corrispondono a nessun codice reale). /// corrispondono a nessun codice reale).
///
/// Verificato su un caso reale confrontando con un report BOM generato da Creo: i
/// riferimenti veri compaiono più volte nel file (una volta per feature/placement),
/// mentre il rumore binario casuale che assomiglia a un codice compare quasi sempre
/// una sola volta. Si scartano quindi i candidati con una sola occorrenza.
/// </summary> /// </summary>
static class AssemblyComponentScanner static class AssemblyComponentScanner
{ {
private static readonly Regex CodiceRegex = new(@"\b\d{8,12}[A-Za-z]{0,2}\b", RegexOptions.Compiled); private static readonly Regex CodiceRegex = new(@"\b\d{8,12}(?:_[A-Za-z]\d{2,4}|[A-Za-z]{0,2})\b", RegexOptions.Compiled);
private const int OccorrenzeMinime = 2;
public static HashSet<string> EstraiCodiciReferenziati(string percorsoAsm) public static HashSet<string> EstraiCodiciReferenziati(string percorsoAsm)
{ {
var testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm)); var testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm));
var codici = new HashSet<string>(); var occorrenze = new Dictionary<string, int>();
foreach (Match m in CodiceRegex.Matches(testo)) foreach (Match m in CodiceRegex.Matches(testo))
{ {
codici.Add(m.Value); occorrenze[m.Value] = occorrenze.GetValueOrDefault(m.Value) + 1;
} }
return codici; return occorrenze.Where(kv => kv.Value >= OccorrenzeMinime).Select(kv => kv.Key).ToHashSet();
} }
} }
+6
View File
@@ -14,4 +14,10 @@
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.0" /> <PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>CadSearchGui.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project> </Project>
+14
View File
@@ -14,6 +14,12 @@ static class DistintaBaseImporter
private static readonly XNamespace Ss = "urn:schemas-microsoft-com:office:spreadsheet"; private static readonly XNamespace Ss = "urn:schemas-microsoft-com:office:spreadsheet";
private const string NomeFoglio = "Distinta Base"; private const string NomeFoglio = "Distinta Base";
// Il gestionale esporta la distinta come "DBP_<codice assieme generale>_<timestamp>.xls":
// il foglio elenca solo i componenti SOTTO l'assieme, non l'assieme generale in sé, quindi
// va aggiunto a parte al CSV leggendo questo codice dal nome del file di input.
private static readonly System.Text.RegularExpressions.Regex NomeFileDistintaRegex =
new(@"^DBP_(?<codice>\d+)_\d+$", System.Text.RegularExpressions.RegexOptions.Compiled);
static DistintaBaseImporter() static DistintaBaseImporter()
{ {
Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
@@ -29,6 +35,14 @@ static class DistintaBaseImporter
.Where(r => r.Componente.Length > 0 || r.Descrizione.Length > 0) .Where(r => r.Componente.Length > 0 || r.Descrizione.Length > 0)
.ToList(); .ToList();
var matchGenerale = NomeFileDistintaRegex.Match(Path.GetFileNameWithoutExtension(percorsoInput));
if (matchGenerale.Success)
{
string codiceGenerale = matchGenerale.Groups["codice"].Value;
if (!righe.Any(r => string.Equals(r.Componente, codiceGenerale, StringComparison.OrdinalIgnoreCase)))
righe.Insert(0, (codiceGenerale, "Assieme generale"));
}
var percorsoCsv = Path.Combine( var percorsoCsv = Path.Combine(
Path.GetDirectoryName(Path.GetFullPath(percorsoInput))!, Path.GetDirectoryName(Path.GetFullPath(percorsoInput))!,
Path.GetFileNameWithoutExtension(percorsoInput) + ".csv"); Path.GetFileNameWithoutExtension(percorsoInput) + ".csv");
+592 -211
View File
@@ -9,13 +9,34 @@ public partial class Form1 : Form
private readonly Button _btnAggiorna = new(); private readonly Button _btnAggiorna = new();
private readonly Button _btnCreaRaw = new(); private readonly Button _btnCreaRaw = new();
private readonly Button _btnTrovaPrt = new(); private readonly Button _btnTrovaPrt = new();
private readonly Button _btnReport = new();
private readonly Button _btnCompletezza = new();
private readonly Button _btnPulisci = new(); private readonly Button _btnPulisci = new();
private readonly Label _lblStatus = new(); private readonly Label _lblStatus = new();
private readonly Label _lblDb = new(); private readonly Label _lblDb = new();
private readonly Label _lblUltimoAggiornamento = new();
private readonly DataGridView _gridImport = new(); private readonly DataGridView _gridImport = new();
private readonly DataGridView _gridRaw = new();
private readonly Label _lblImportStatus = new(); private readonly Label _lblImportStatus = new();
private readonly ToolTip _tipImportStatus = new(); private readonly ToolTip _tipImportStatus = new();
// Wizard: una fase alla volta occupa tutta la finestra, si avanza solo quando quella
// corrente è "pronta" (vedi AvantiConsentito). NomiFasi/_panelFasi sono paralleli per indice.
private static readonly string[] NomiFasi =
{
"1. Database",
"2. Importa Distinta Base",
"3. Crea cartella raw",
"4. Trova componenti referenziati",
"5. Verifica completezza e STEP",
};
private readonly Panel[] _panelFasi = new Panel[NomiFasi.Length];
private readonly Panel _faseContainer = new() { Dock = DockStyle.Fill };
private readonly Label _lblFase = new();
private readonly Button _btnIndietro = new();
private readonly Button _btnAvanti = new();
private int _faseAttuale;
private static readonly string[] TipiPredefiniti = { "prt", "asm" }; private static readonly string[] TipiPredefiniti = { "prt", "asm" };
private const string RadiceDiscoDefault = @"X:\"; private const string RadiceDiscoDefault = @"X:\";
@@ -42,23 +63,210 @@ public partial class Form1 : Form
MinimumSize = new Size(760, 480); MinimumSize = new Size(760, 480);
Font = new Font("Segoe UI", 9.5f); Font = new Font("Segoe UI", 9.5f);
var contenutoTop = new TableLayoutPanel var headerPanel = new Panel { Dock = DockStyle.Top, Height = 40, BackColor = SystemColors.ControlLight };
_lblFase.Dock = DockStyle.Fill;
_lblFase.TextAlign = ContentAlignment.MiddleLeft;
_lblFase.Padding = new Padding(12, 0, 0, 0);
_lblFase.Font = new Font(Font, FontStyle.Bold);
headerPanel.Controls.Add(_lblFase);
var navPanel = new FlowLayoutPanel
{
Dock = DockStyle.Bottom,
Height = 48,
FlowDirection = FlowDirection.RightToLeft,
Padding = new Padding(12, 8, 12, 8),
};
_btnAvanti.Text = "Avanti >";
_btnAvanti.Size = new Size(110, 32);
_btnAvanti.FlatStyle = FlatStyle.Flat;
_btnAvanti.FlatAppearance.BorderSize = 0;
_btnAvanti.BackColor = ColorePrimario;
_btnAvanti.ForeColor = Color.White;
_btnAvanti.Font = new Font(Font, FontStyle.Bold);
_btnAvanti.Click += (_, _) => VaiAllaFase(_faseAttuale + 1);
navPanel.Controls.Add(_btnAvanti);
_btnIndietro.Text = "< Indietro";
_btnIndietro.Size = new Size(110, 32);
_btnIndietro.Margin = new Padding(8, 0, 0, 0);
_btnIndietro.Click += (_, _) => VaiAllaFase(_faseAttuale - 1);
navPanel.Controls.Add(_btnIndietro);
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);
_panelFasi[0] = BuildFaseDatabase();
_panelFasi[1] = BuildFaseImporta();
_panelFasi[2] = BuildFaseCreaRaw();
_panelFasi[3] = BuildFaseTrovaComponenti();
_panelFasi[4] = BuildFaseVerifica();
foreach (var panel in _panelFasi)
{
panel.Dock = DockStyle.Fill;
panel.Visible = false;
_faseContainer.Controls.Add(panel);
}
Controls.Add(_faseContainer);
Controls.Add(navPanel);
Controls.Add(statusPanel);
Controls.Add(headerPanel);
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]);
};
VaiAllaFase(0);
}
/// <summary>Nasconde la fase corrente e mostra quella richiesta (se nei limiti e se la fase
/// corrente permette di avanzare); aggiorna titolo e stato dei pulsanti Indietro/Avanti.</summary>
private void VaiAllaFase(int fase)
{
if (fase < 0 || fase >= NomiFasi.Length)
return;
if (fase > _faseAttuale && !AvantiConsentito(_faseAttuale))
return;
_panelFasi[_faseAttuale].Visible = false;
_faseAttuale = fase;
_panelFasi[_faseAttuale].Visible = true;
_lblFase.Text = NomiFasi[_faseAttuale];
_btnIndietro.Visible = _faseAttuale > 0;
_btnAvanti.Visible = _faseAttuale < NomiFasi.Length - 1;
AggiornaStatoAvanti();
}
/// <summary>Condizione per cui la fase indicata è "conclusa" e si può passare alla successiva.</summary>
private bool AvantiConsentito(int fase) => fase switch
{
0 => _dbPath is not null,
1 => _righeCorrenti is { Count: > 0 },
2 => _ultimaCartellaRaw is not null,
3 => true, // risoluzione ricorsiva opzionale: non tutti gli assiemi hanno sotto-componenti
_ => true,
};
private void AggiornaStatoAvanti() => _btnAvanti.Enabled = AvantiConsentito(_faseAttuale);
private Panel BuildFaseDatabase()
{
var contenuto = new TableLayoutPanel
{ {
Dock = DockStyle.Top, Dock = DockStyle.Top,
AutoSize = true, AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink, AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 1, ColumnCount = 1,
Padding = new Padding(12, 12, 12, 0), Padding = new Padding(16),
}; };
contenutoTop.Controls.Add(BuildGruppoDatabase());
contenutoTop.Controls.Add(BuildGruppoImporta());
_lblImportStatus.Dock = DockStyle.Top; var lblIstruzioni = new Label
{
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 0, 0, 12),
Text = "L'indice CAD viene caricato automaticamente all'avvio, se già presente. "
+ "Usa 'Apri DB...' per selezionarne un altro, oppure 'Aggiorna indice' per "
+ "scansionare di nuovo il disco (X:\\).",
};
contenuto.Controls.Add(lblIstruzioni);
_lblDb.AutoSize = true;
_lblDb.AutoEllipsis = true;
_lblDb.MaximumSize = new Size(900, 0);
_lblDb.ForeColor = SystemColors.GrayText;
_lblDb.Margin = new Padding(0, 0, 0, 4);
contenuto.Controls.Add(_lblDb);
_lblUltimoAggiornamento.AutoSize = true;
_lblUltimoAggiornamento.ForeColor = SystemColors.GrayText;
_lblUltimoAggiornamento.Margin = new Padding(0, 0, 0, 12);
contenuto.Controls.Add(_lblUltimoAggiornamento);
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);
var lblManutenzione = new Label
{
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 20, 0, 8),
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.Click += (_, _) => PulisciRawEStep();
contenuto.Controls.Add(_btnPulisci);
var panel = new Panel();
panel.Controls.Add(contenuto);
return panel;
}
private Panel BuildFaseImporta()
{
var contenuto = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 1,
Padding = new Padding(16),
};
var lblIstruzioni = new Label
{
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 0, 0, 8),
Text = "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(lblIstruzioni);
var btnSfoglia = new Button
{
Text = "Scegli file distinta...",
Size = new Size(220, 32),
Margin = new Padding(0, 0, 0, 8),
};
btnSfoglia.Click += (_, _) => ScegliFileDistinta();
contenuto.Controls.Add(btnSfoglia);
_lblImportStatus.AutoSize = false; _lblImportStatus.AutoSize = false;
_lblImportStatus.AutoEllipsis = true; _lblImportStatus.AutoEllipsis = true;
_lblImportStatus.Height = 28; _lblImportStatus.Height = 24;
_lblImportStatus.Padding = new Padding(12, 6, 12, 6); _lblImportStatus.Width = 900;
_lblImportStatus.ForeColor = SystemColors.GrayText; _lblImportStatus.ForeColor = SystemColors.GrayText;
contenuto.Controls.Add(_lblImportStatus);
_gridImport.Dock = DockStyle.Fill; _gridImport.Dock = DockStyle.Fill;
_gridImport.ReadOnly = true; _gridImport.ReadOnly = true;
@@ -80,36 +288,16 @@ public partial class Form1 : Form
_gridImport.DefaultCellStyle.Padding = new Padding(4, 0, 4, 0); _gridImport.DefaultCellStyle.Padding = new Padding(4, 0, 4, 0);
_gridImport.Columns.Add("componente", "Componente"); _gridImport.Columns.Add("componente", "Componente");
_gridImport.Columns.Add("descrizione", "Descrizione"); _gridImport.Columns.Add("descrizione", "Descrizione");
_gridImport.Columns.Add("percorso", "Percorso CAD"); _gridImport.Columns["componente"]!.FillWeight = 30;
_gridImport.Columns["componente"]!.FillWeight = 20; _gridImport.Columns["descrizione"]!.FillWeight = 70;
_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 }; var panel = new Panel();
_lblStatus.Dock = DockStyle.Fill; panel.Controls.Add(_gridImport);
_lblStatus.TextAlign = ContentAlignment.MiddleLeft; panel.Controls.Add(contenuto);
_lblStatus.Padding = new Padding(12, 0, 0, 0); return panel;
_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() private Panel BuildFaseCreaRaw()
{ {
var contenuto = new TableLayoutPanel var contenuto = new TableLayoutPanel
{ {
@@ -117,85 +305,22 @@ public partial class Form1 : Form
AutoSize = true, AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink, AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 1, ColumnCount = 1,
Padding = new Padding(16),
}; };
_lblDb.AutoSize = true; var lblIstruzioni = new Label
_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, AutoSize = true,
ForeColor = SystemColors.GrayText, ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 0, 0, 0), Margin = new Padding(0, 0, 0, 8),
Text = "2. Trascina qui il file .xls/.xml esportato dal gestionale (foglio 'Distinta " Text = "Cerca nell'indice il percorso CAD di ogni componente importato e copia i "
+ "Base'), oppure scegli il file col pulsante sotto: genera un CSV con Componente " + "file trovati in una cartella 'raw' accanto al file di distinta originale.",
+ "e Descrizione, in anteprima nella griglia.",
}; };
contenuto.Controls.Add(lblIstruzioni1); contenuto.Controls.Add(lblIstruzioni);
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.Text = "Crea cartella raw";
_btnCreaRaw.Size = new Size(220, 36); _btnCreaRaw.Size = new Size(220, 36);
_btnCreaRaw.Margin = new Padding(0, 8, 0, 0); _btnCreaRaw.Margin = new Padding(0, 0, 0, 12);
_btnCreaRaw.Enabled = false;
_btnCreaRaw.FlatStyle = FlatStyle.Flat; _btnCreaRaw.FlatStyle = FlatStyle.Flat;
_btnCreaRaw.FlatAppearance.BorderSize = 0; _btnCreaRaw.FlatAppearance.BorderSize = 0;
_btnCreaRaw.BackColor = ColorePrimario; _btnCreaRaw.BackColor = ColorePrimario;
@@ -204,49 +329,115 @@ public partial class Form1 : Form
_btnCreaRaw.Click += (_, _) => CreaCartellaRaw(); _btnCreaRaw.Click += (_, _) => CreaCartellaRaw();
contenuto.Controls.Add(_btnCreaRaw); contenuto.Controls.Add(_btnCreaRaw);
var lblIstruzioni3 = new Label _gridRaw.Dock = DockStyle.Fill;
_gridRaw.ReadOnly = true;
_gridRaw.BorderStyle = BorderStyle.None;
_gridRaw.BackgroundColor = SystemColors.Window;
_gridRaw.RowHeadersVisible = false;
_gridRaw.AllowUserToAddRows = false;
_gridRaw.AllowUserToDeleteRows = false;
_gridRaw.AllowUserToResizeRows = false;
_gridRaw.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
_gridRaw.MultiSelect = false;
_gridRaw.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
_gridRaw.ColumnHeadersHeight = 32;
_gridRaw.ColumnHeadersDefaultCellStyle.Font = new Font(Font, FontStyle.Bold);
_gridRaw.RowTemplate.Height = 26;
_gridRaw.AlternatingRowsDefaultCellStyle.BackColor = Color.FromArgb(245, 247, 250);
_gridRaw.DefaultCellStyle.SelectionBackColor = ColorePrimario;
_gridRaw.DefaultCellStyle.Padding = new Padding(4, 0, 4, 0);
_gridRaw.Columns.Add("componente", "Componente");
_gridRaw.Columns.Add("percorso", "Percorso CAD");
_gridRaw.Columns["componente"]!.FillWeight = 25;
_gridRaw.Columns["percorso"]!.FillWeight = 75;
_gridRaw.CellDoubleClick += (_, _) => ApriCartellaFileSelezionato();
var panel = new Panel();
panel.Controls.Add(_gridRaw);
panel.Controls.Add(contenuto);
return panel;
}
private Panel BuildFaseTrovaComponenti()
{
var contenuto = new TableLayoutPanel
{ {
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, Dock = DockStyle.Top,
AutoSize = true, AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink, AutoSizeMode = AutoSizeMode.GrowAndShrink,
Padding = new Padding(12, 8, 12, 12), ColumnCount = 1,
Controls = { contenuto }, Padding = new Padding(16),
}; };
var lblIstruzioni = new Label
{
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 0, 0, 12),
Text = "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'. Passo opzionale: se la distinta contiene "
+ "solo parti singole puoi anche passare direttamente alla fase successiva.",
};
contenuto.Controls.Add(lblIstruzioni);
_btnTrovaPrt.Text = "Trova componenti referenziati";
_btnTrovaPrt.Size = new Size(220, 32);
_btnTrovaPrt.Click += (_, _) => TrovaPrtReferenziati();
contenuto.Controls.Add(_btnTrovaPrt);
var panel = new Panel();
panel.Controls.Add(contenuto);
return panel;
}
private Panel BuildFaseVerifica()
{
var contenuto = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 1,
Padding = new Padding(16),
};
var lblIstruzioniCompletezza = new Label
{
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 0, 0, 8),
Text = "Per ogni assieme della distinta, verifica se tutti i suoi componenti "
+ "(ricorsivamente fino ai PRT) sono nell'indice. Nota: la scansione dei .asm "
+ "è euristica, quindi qualche falso 'incompleto' è possibile.",
};
contenuto.Controls.Add(lblIstruzioniCompletezza);
_btnCompletezza.Text = "Verifica completezza assiemi";
_btnCompletezza.Size = new Size(220, 32);
_btnCompletezza.Margin = new Padding(0, 0, 0, 20);
_btnCompletezza.Click += (_, _) => VerificaCompletezzaAssiemi();
contenuto.Controls.Add(_btnCompletezza);
var lblIstruzioniStep = new Label
{
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 0, 0, 8),
Text = "Dopo aver esportato gli STEP con Creo, premi qui per confrontare i "
+ "componenti della distinta con i file .stp presenti in 'step' e vedere quali "
+ "sono già stati realizzati e quali mancano ancora.",
};
contenuto.Controls.Add(lblIstruzioniStep);
_btnReport.Text = "Report STEP realizzati";
_btnReport.Size = new Size(220, 32);
_btnReport.Click += (_, _) => GeneraReportStep();
contenuto.Controls.Add(_btnReport);
var panel = new Panel();
panel.Controls.Add(contenuto);
return panel;
} }
private void ScegliFileDistinta() private void ScegliFileDistinta()
@@ -271,9 +462,10 @@ public partial class Form1 : Form
_gridImport.Rows.Clear(); _gridImport.Rows.Clear();
foreach (var (componente, descrizione) in righe) foreach (var (componente, descrizione) in righe)
{ {
_gridImport.Rows.Add(componente, descrizione, ""); _gridImport.Rows.Add(componente, descrizione);
} }
_btnCreaRaw.Enabled = righe.Count > 0; _gridRaw.Rows.Clear();
_ultimaCartellaRaw = null;
ImpostaStatoImport($"CSV generato ({righe.Count} righe): {csv}", Color.DarkGreen); ImpostaStatoImport($"CSV generato ({righe.Count} righe): {csv}", Color.DarkGreen);
} }
@@ -281,10 +473,10 @@ public partial class Form1 : Form
{ {
_percorsoInputCorrente = null; _percorsoInputCorrente = null;
_righeCorrenti = null; _righeCorrenti = null;
_btnCreaRaw.Enabled = false;
_gridImport.Rows.Clear(); _gridImport.Rows.Clear();
ImpostaStatoImport($"Errore durante la conversione: {ex.Message}", Color.DarkRed); ImpostaStatoImport($"Errore durante la conversione: {ex.Message}", Color.DarkRed);
} }
AggiornaStatoAvanti();
} }
/// <summary>Imposta testo/colore dello stato import e il tooltip con il testo completo /// <summary>Imposta testo/colore dello stato import e il tooltip con il testo completo
@@ -301,25 +493,61 @@ public partial class Form1 : Form
if (_percorsoInputCorrente is null || _righeCorrenti is null) if (_percorsoInputCorrente is null || _righeCorrenti is null)
return; return;
// L'assieme generale è già una riga di _righeCorrenti (aggiunta da
// DistintaBaseImporter.ConvertiInCsv leggendo il nome del file distinta), quindi viene
// trovato e copiato in raw come qualsiasi altro componente, senza bisogno di un caso speciale.
var percorsiTrovati = TrovaPercorsiCad(_righeCorrenti.Select(r => r.Componente)); var percorsiTrovati = TrovaPercorsiCad(_righeCorrenti.Select(r => r.Componente));
for (int i = 0; i < _righeCorrenti.Count; i++) _gridRaw.Rows.Clear();
foreach (var (componente, _) in _righeCorrenti)
{ {
string percorsoCad = percorsiTrovati.TryGetValue(_righeCorrenti[i].Componente, out var lista) string percorsoCad = percorsiTrovati.TryGetValue(componente, out var lista)
? string.Join("; ", lista) ? string.Join("; ", lista)
: ""; : "";
_gridImport.Rows[i].Cells[2].Value = percorsoCad; _gridRaw.Rows.Add(componente, percorsoCad);
} }
var cartellaRaw = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(_percorsoInputCorrente))!, "raw"); var cartellaRaw = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(_percorsoInputCorrente))!, "raw");
int copiati = CopiaFileTrovati(percorsiTrovati.Values.SelectMany(p => p).Distinct(), cartellaRaw); int copiati = CopiaFileTrovati(percorsiTrovati.Values.SelectMany(p => p).Distinct(), cartellaRaw);
_ultimaCartellaRaw = cartellaRaw; _ultimaCartellaRaw = cartellaRaw;
_btnTrovaPrt.Enabled = true;
ImpostaStatoImport( ImpostaStatoImport(
$"{percorsiTrovati.Count} componenti con percorso CAD trovato su {_righeCorrenti.Count}. " $"{percorsiTrovati.Count} componenti con percorso CAD trovato su {_righeCorrenti.Count}. "
+ $"{copiati} file copiati in {cartellaRaw}", + $"{copiati} file copiati in {cartellaRaw}",
Color.DarkGreen); Color.DarkGreen);
AggiornaStatoAvanti();
}
/// <summary>Confronta i componenti della distinta importata con i file .stp presenti nella
/// cartella "step" (esportati manualmente con Creo Distributed Batch), per dire quali sono
/// già stati realizzati e quali mancano ancora.</summary>
private void GeneraReportStep()
{
if (_percorsoInputCorrente is null || _righeCorrenti is null)
return;
var cartellaStep = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(_percorsoInputCorrente))!, "step");
if (!Directory.Exists(cartellaStep))
{
ImpostaStatoImport($"Cartella 'step' non trovata in {cartellaStep}.", Color.DarkRed);
return;
}
var codiciConStep = new HashSet<string>(
Directory.GetFiles(cartellaStep, "*.stp").Select(Path.GetFileNameWithoutExtension)!);
var righeReport = _righeCorrenti
.Select(r => (r.Componente, r.Descrizione, Realizzato: codiciConStep.Contains(r.Componente)))
.ToList();
int realizzati = righeReport.Count(r => r.Realizzato);
ImpostaStatoImport(
$"Report STEP: {realizzati} componenti realizzati su {righeReport.Count} "
+ $"({righeReport.Count - realizzati} mancanti).",
Color.DarkGreen);
using var dialog = new ReportStepForm(righeReport, cartellaStep);
dialog.ShowDialog(this);
} }
/// <summary>Svuota in un colpo solo le cartelle "raw" e "step" nella root (stessa cartella /// <summary>Svuota in un colpo solo le cartelle "raw" e "step" nella root (stessa cartella
@@ -377,10 +605,77 @@ public partial class Form1 : Form
ImpostaStatoImport($"{eliminati} file eliminati da {string.Join(" e ", cartelle)}.", Color.DarkGreen); ImpostaStatoImport($"{eliminati} file eliminati da {string.Join(" e ", cartelle)}.", Color.DarkGreen);
} }
/// <summary>Risolve ricorsivamente l'albero dei componenti a partire dagli .asm in "raw": /// <summary>Risolve ricorsivamente l'albero dei componenti a partire dai file .asm indicati
/// per ogni assieme legge i codici referenziati al suo interno, li cerca nell'indice, copia /// (già copiati in "raw"): per ogni assieme legge i codici referenziati al suo interno, li
/// in "raw" quelli trovati e, se sono a loro volta assiemi, li accoda per essere scansionati /// cerca nell'indice, copia in "raw" quelli trovati e, se sono a loro volta assiemi, li
/// finché non restano solo PRT (foglie) o codici non presenti nell'indice.</summary> /// accoda per essere scansionati finché non restano solo PRT (foglie) o codici non presenti
/// nell'indice. Usata dal bottone "Trova componenti referenziati".</summary>
private (List<(string Codice, string Tipo, string Percorso)> Trovati, List<string> NonTrovati) RisolviAlberoComponenti(
IEnumerable<string> asmIniziali)
{
var daScansionare = new Queue<string>(asmIniziali);
var codiciVisti = new HashSet<string>(
daScansionare.Select(f => Path.GetFileName(f).Split('.')[0]), StringComparer.OrdinalIgnoreCase);
var trovati = new List<(string Codice, string Tipo, string Percorso)>();
var codiciNonTrovati = new List<string>();
if (daScansionare.Count == 0 || _dbPath is null)
return (trovati, codiciNonTrovati);
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 LOWER(codice) = LOWER(@codice) AND LOWER(tipo_file) IN ('prt', 'asm') "
+ "ORDER BY tipo_file, data_modifica DESC";
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;
// Lo stesso codice+tipo può esistere in più cartelle radice (duplicati sulla
// share): ordinato per data_modifica DESC, si tiene solo il più recente per tipo.
var componenti = new List<(string Tipo, string Percorso)>();
var tipiVisti = new HashSet<string>();
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
string tipoLower = reader.GetString(0).ToLowerInvariant();
if (tipiVisti.Add(tipoLower))
{
componenti.Add((tipoLower, reader.GetString(1)));
}
}
}
if (componenti.Count == 0)
{
codiciNonTrovati.Add(codice);
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)));
}
}
}
}
return (trovati, codiciNonTrovati);
}
private void TrovaPrtReferenziati() private void TrovaPrtReferenziati()
{ {
if (_ultimaCartellaRaw is null || !Directory.Exists(_ultimaCartellaRaw)) if (_ultimaCartellaRaw is null || !Directory.Exists(_ultimaCartellaRaw))
@@ -392,69 +687,141 @@ public partial class Form1 : Form
return; return;
} }
var daScansionare = new Queue<string>( // L'assieme generale è già in raw come qualsiasi altro assieme importato (vedi
Directory.GetFiles(_ultimaCartellaRaw).Where(f => Path.GetFileName(f).Contains(".asm."))); // CreaCartellaRaw), quindi rientra qui senza bisogno di un caso speciale.
var codiciVisti = new HashSet<string>(daScansionare.Select(f => Path.GetFileName(f).Split('.')[0])); var asmIniziali = Directory.GetFiles(_ultimaCartellaRaw).Where(f => Path.GetFileName(f).Contains(".asm."));
var (trovati, codiciNonTrovati) = RisolviAlberoComponenti(asmIniziali);
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 numPrt = trovati.Count(r => r.Tipo == "prt");
int numAsm = trovati.Count(r => r.Tipo == "asm"); int numAsm = trovati.Count(r => r.Tipo == "asm");
ImpostaStatoImport( ImpostaStatoImport(
$"Componenti referenziati: {numPrt} PRT e {numAsm} sotto-assiemi trovati e copiati in raw, " $"Componenti referenziati: {numPrt} PRT e {numAsm} sotto-assiemi trovati e copiati in raw, "
+ $"{codiciNonTrovati} codici referenziati non trovati nell'indice.", + $"{codiciNonTrovati.Count} codici referenziati non trovati nell'indice.",
Color.DarkGreen); Color.DarkGreen);
using var dialog = new PrtMancantiForm(trovati, codiciNonTrovati, _ultimaCartellaRaw); using var dialog = new PrtMancantiForm(trovati, codiciNonTrovati, _ultimaCartellaRaw);
dialog.ShowDialog(this); dialog.ShowDialog(this);
} }
/// <summary>Per ogni assieme della distinta, verifica ricorsivamente (leggendo i .asm dai
/// percorsi originali sulla share) se tutti i componenti fino ai PRT sono nell'indice.
/// I risultati di ogni file/codice vengono in cache e condivisi tra gli assiemi controllati
/// nella stessa passata, per non rileggere più volte gli stessi sotto-componenti.</summary>
private void VerificaCompletezzaAssiemi()
{
if (_righeCorrenti is null)
return;
if (_dbPath is null)
{
ImpostaStatoImport("Nessun database caricato: impossibile verificare la completezza.", Color.DarkRed);
return;
}
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 LOWER(codice) = LOWER(@codice) AND LOWER(tipo_file) IN ('prt', 'asm') "
+ "ORDER BY tipo_file, data_modifica DESC";
var pCodice = command.Parameters.Add("@codice", Microsoft.Data.Sqlite.SqliteType.Text);
var cacheComponenti = new Dictionary<string, List<(string Tipo, string Percorso)>>();
var cacheCodiciFile = new Dictionary<string, HashSet<string>>();
List<(string Tipo, string Percorso)> CercaComponenti(string codice)
{
if (cacheComponenti.TryGetValue(codice, out var giaTrovati))
return giaTrovati;
pCodice.Value = codice;
var risultato = new List<(string Tipo, string Percorso)>();
var tipiVisti = new HashSet<string>();
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
string tipo = reader.GetString(0).ToLowerInvariant();
if (tipiVisti.Add(tipo))
{
risultato.Add((tipo, reader.GetString(1)));
}
}
}
cacheComponenti[codice] = risultato;
return risultato;
}
bool AssiemeCompleto(string percorsoAsmIniziale)
{
var daVisitare = new Queue<string>();
daVisitare.Enqueue(percorsoAsmIniziale);
var fileVisitati = new HashSet<string> { percorsoAsmIniziale };
var codiciVisti = new HashSet<string> { Path.GetFileName(percorsoAsmIniziale).Split('.')[0] };
while (daVisitare.Count > 0)
{
var file = daVisitare.Dequeue();
if (!cacheCodiciFile.TryGetValue(file, out var codiciReferenziati))
{
try
{
codiciReferenziati = AssemblyComponentScanner.EstraiCodiciReferenziati(file);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
return false; // file irraggiungibile (share offline) -> non verificabile, per prudenza "incompleto"
}
cacheCodiciFile[file] = codiciReferenziati;
}
foreach (var codice in codiciReferenziati)
{
if (!codiciVisti.Add(codice))
continue;
var componenti = CercaComponenti(codice);
if (componenti.Count == 0)
return false;
foreach (var (tipo, percorso) in componenti)
{
if (tipo == "asm" && fileVisitati.Add(percorso))
{
daVisitare.Enqueue(percorso);
}
}
}
}
return true;
}
// L'assieme generale è già una riga di _righeCorrenti (vedi DistintaBaseImporter), quindi
// la sua completezza viene verificata insieme a quella di tutti gli altri componenti.
var righeReport = new List<(string Componente, string Descrizione, bool? Completo)>();
foreach (var (componente, descrizione) in _righeCorrenti)
{
var componenti = CercaComponenti(componente);
string? percorsoAsm = componenti.Where(c => c.Tipo == "asm").Select(c => c.Percorso).FirstOrDefault();
bool? completo = componenti.Count == 0 ? null
: percorsoAsm is null ? true // solo prt trovato: nessuna sotto-struttura da verificare
: AssiemeCompleto(percorsoAsm);
righeReport.Add((componente, descrizione, completo));
}
int completi = righeReport.Count(r => r.Completo == true);
int incompleti = righeReport.Count(r => r.Completo == false);
int nonTrovati = righeReport.Count(r => r.Completo is null);
ImpostaStatoImport(
$"Completezza assiemi: {completi} completi, {incompleti} incompleti, "
+ $"{nonTrovati} non trovati nell'indice.",
Color.DarkGreen);
using var dialogCompletezza = new ReportCompletezzaForm(righeReport);
dialogCompletezza.ShowDialog(this);
}
/// <summary>Copia i file trovati nella cartella "raw" indicata, creandola se necessario. /// <summary>Copia i file trovati nella cartella "raw" indicata, creandola se necessario.
/// Ritorna il numero di file copiati con successo.</summary> /// Ritorna il numero di file copiati con successo.</summary>
internal static int CopiaFileTrovati(IEnumerable<string> percorsi, string cartellaRaw) internal static int CopiaFileTrovati(IEnumerable<string> percorsi, string cartellaRaw)
@@ -490,9 +857,9 @@ public partial class Form1 : Form
var nomiParametri = TipiPredefiniti.Select((_, i) => $"@tipo{i}"); var nomiParametri = TipiPredefiniti.Select((_, i) => $"@tipo{i}");
using var command = connection.CreateCommand(); using var command = connection.CreateCommand();
command.CommandText = $""" command.CommandText = $"""
SELECT percorso_completo FROM files_ultima_versione SELECT tipo_file, percorso_completo FROM files_ultima_versione
WHERE codice = @codice AND tipo_file IN ({string.Join(",", nomiParametri)}) WHERE LOWER(codice) = LOWER(@codice) AND LOWER(tipo_file) IN ({string.Join(",", nomiParametri)})
ORDER BY tipo_file ORDER BY tipo_file, data_modifica DESC
"""; """;
var pCodice = command.Parameters.Add("@codice", Microsoft.Data.Sqlite.SqliteType.Text); var pCodice = command.Parameters.Add("@codice", Microsoft.Data.Sqlite.SqliteType.Text);
for (int i = 0; i < TipiPredefiniti.Length; i++) for (int i = 0; i < TipiPredefiniti.Length; i++)
@@ -505,9 +872,15 @@ public partial class Form1 : Form
pCodice.Value = codice; pCodice.Value = codice;
using var reader = command.ExecuteReader(); using var reader = command.ExecuteReader();
var percorsi = new List<string>(); var percorsi = new List<string>();
// Lo stesso codice+tipo può esistere in più cartelle radice (duplicati sulla share):
// essendo ordinato per data_modifica DESC, il primo per tipo è il più recente, gli altri si scartano.
var tipiVisti = new HashSet<string>();
while (reader.Read()) while (reader.Read())
{ {
percorsi.Add(reader.GetString(0)); if (tipiVisti.Add(reader.GetString(0).ToLowerInvariant()))
{
percorsi.Add(reader.GetString(1));
}
} }
if (percorsi.Count > 0) if (percorsi.Count > 0)
risultato[codice] = percorsi; risultato[codice] = percorsi;
@@ -533,6 +906,8 @@ public partial class Form1 : Form
{ {
_lblStatus.Text = "Database non trovato. Usa 'Apri DB...' per selezionarlo oppure 'Aggiorna indice' per crearlo."; _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."; _lblDb.Text = "Nessun database caricato. Premi 'Aggiorna indice' per crearlo da X:\\, oppure 'Apri DB...' se ne hai gi\u00e0 uno.";
_lblUltimoAggiornamento.Text = "";
AggiornaStatoAvanti();
return; return;
} }
@@ -560,9 +935,14 @@ public partial class Form1 : Form
connection.Open(); connection.Open();
long totale = (long)(new SqliteCommand("SELECT COUNT(*) FROM files", connection).ExecuteScalar() ?? 0L); long totale = (long)(new SqliteCommand("SELECT COUNT(*) FROM files", connection).ExecuteScalar() ?? 0L);
object? ultimaScansioneRaw = new SqliteCommand("SELECT MAX(scansionato_il) FROM files", connection).ExecuteScalar();
_dbPath = path; _dbPath = path;
_lblDb.Text = $"Database: {path} ({totale:N0} file indicizzati)"; _lblDb.Text = $"Database: {path} ({totale:N0} file indicizzati)";
_lblUltimoAggiornamento.Text = ultimaScansioneRaw is string testoData
&& DateTime.TryParse(testoData, out var ultimaScansione)
? $"Ultimo aggiornamento indice: {ultimaScansione.ToLocalTime():dd/MM/yyyy HH:mm}"
: "Ultimo aggiornamento indice: sconosciuto";
_lblStatus.Text = "Pronto."; _lblStatus.Text = "Pronto.";
SalvaUltimoDbUsato(path); SalvaUltimoDbUsato(path);
} }
@@ -571,6 +951,7 @@ public partial class Form1 : Form
MessageBox.Show(this, $"Impossibile aprire il database:\n{ex.Message}", "Errore", MessageBox.Show(this, $"Impossibile aprire il database:\n{ex.Message}", "Errore",
MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
AggiornaStatoAvanti();
} }
/// <summary>Ricorda l'ultimo DB caricato con successo, così al prossimo avvio si /// <summary>Ricorda l'ultimo DB caricato con successo, così al prossimo avvio si
@@ -656,11 +1037,11 @@ public partial class Form1 : Form
private void ApriCartellaFileSelezionato() private void ApriCartellaFileSelezionato()
{ {
if (_gridImport.CurrentRow is null) if (_gridRaw.CurrentRow is null)
{ {
return; return;
} }
string? percorsi = _gridImport.CurrentRow.Cells[2].Value as string; string? percorsi = _gridRaw.CurrentRow.Cells[1].Value as string;
if (string.IsNullOrEmpty(percorsi)) if (string.IsNullOrEmpty(percorsi))
{ {
return; return;
+8 -6
View File
@@ -18,12 +18,10 @@ public sealed record ScanProgress(
public static class Indexer public static class Indexer
{ {
public static readonly string[] CartelleRadice = { "__Accessori", "__B.end", "__Corpi", "__Fondi", "Unificati", "Commerciali" };
private static readonly Regex FilenameRegex = private static readonly Regex FilenameRegex =
new(@"^(?<codice>.+)\.(?<tipo>[A-Za-z0-9]+)\.(?<versione>\d+)$", RegexOptions.Compiled); new(@"^(?<codice>.+)\.(?<tipo>[A-Za-z0-9]+)\.(?<versione>\d+)$", RegexOptions.Compiled);
private const string Schema = """ internal const string Schema = """
CREATE TABLE IF NOT EXISTS files ( CREATE TABLE IF NOT EXISTS files (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
percorso_completo TEXT UNIQUE NOT NULL, percorso_completo TEXT UNIQUE NOT NULL,
@@ -67,8 +65,12 @@ public static class Indexer
schemaCmd.ExecuteNonQuery(); schemaCmd.ExecuteNonQuery();
} }
// Indicizza tutte le cartelle di primo livello sotto la radice (non un elenco fisso),
// così una nuova cartella creata sulla share entra nell'indice senza modificare il codice.
var cartelleRadice = Directory.GetDirectories(root).Select(p => Path.GetFileName(p)!).ToArray();
var previsti = new Dictionary<string, long>(); var previsti = new Dictionary<string, long>();
foreach (var cartella in CartelleRadice) foreach (var cartella in cartelleRadice)
{ {
using var cmd = connection.CreateCommand(); using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT COUNT(*) FROM files WHERE cartella_radice = @c"; cmd.CommandText = "SELECT COUNT(*) FROM files WHERE cartella_radice = @c";
@@ -80,7 +82,7 @@ public static class Indexer
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
long fattiTotali = 0; long fattiTotali = 0;
foreach (var cartella in CartelleRadice) foreach (var cartella in cartelleRadice)
{ {
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
@@ -233,7 +235,7 @@ public static class Indexer
cmd.Parameters["@modifica"].Value = modifica; cmd.Parameters["@modifica"].Value = modifica;
} }
private static (string codice, string tipo, int? versione) ParseFilename(string nomeFile) internal static (string codice, string tipo, int? versione) ParseFilename(string nomeFile)
{ {
var match = FilenameRegex.Match(nomeFile); var match = FilenameRegex.Match(nomeFile);
if (match.Success) if (match.Success)
+10 -4
View File
@@ -6,7 +6,7 @@ namespace CadSearchGui;
/// sotto-assiemi trovati nell'indice e già copiati in "raw" a partire dagli assiemi iniziali.</summary> /// sotto-assiemi trovati nell'indice e già copiati in "raw" a partire dagli assiemi iniziali.</summary>
public class PrtMancantiForm : Form public class PrtMancantiForm : Form
{ {
public PrtMancantiForm(List<(string Codice, string Tipo, string Percorso)> righe, int codiciNonTrovati, string cartellaRaw) public PrtMancantiForm(List<(string Codice, string Tipo, string Percorso)> righe, List<string> codiciNonTrovati, string cartellaRaw)
{ {
int numPrt = righe.Count(r => r.Tipo == "prt"); int numPrt = righe.Count(r => r.Tipo == "prt");
int numAsm = righe.Count(r => r.Tipo == "asm"); int numAsm = righe.Count(r => r.Tipo == "asm");
@@ -23,9 +23,10 @@ public class PrtMancantiForm : Form
Height = 44, Height = 44,
Padding = new Padding(12, 8, 12, 8), Padding = new Padding(12, 8, 12, 8),
Text = $"Copiati in \"{cartellaRaw}\": {numPrt} PRT e {numAsm} sotto-assiemi trovati nell'indice." Text = $"Copiati in \"{cartellaRaw}\": {numPrt} PRT e {numAsm} sotto-assiemi trovati nell'indice."
+ (codiciNonTrovati > 0 + (codiciNonTrovati.Count > 0
? $" {codiciNonTrovati} codici referenziati non corrispondono a nessun file indicizzato " ? $" {codiciNonTrovati.Count} codici referenziati non corrispondono a nessun file indicizzato "
+ "(probabilmente disegni, o falsi positivi della scansione)." + "(righe 'non trovato' sotto: possono essere disegni, componenti davvero mancanti, "
+ "o falsi positivi della scansione)."
: ""), : ""),
}; };
@@ -49,6 +50,11 @@ public class PrtMancantiForm : Form
{ {
grid.Rows.Add(codice, tipo, percorso); grid.Rows.Add(codice, tipo, percorso);
} }
foreach (var codice in codiciNonTrovati.OrderBy(c => c))
{
int i = grid.Rows.Add(codice, "non trovato", "");
grid.Rows[i].DefaultCellStyle.ForeColor = Color.DarkRed;
}
var btnApriCartella = new Button { Text = "Apri cartella raw", Dock = DockStyle.Bottom, Height = 34 }; var btnApriCartella = new Button { Text = "Apri cartella raw", Dock = DockStyle.Bottom, Height = 34 };
btnApriCartella.Click += (_, _) => btnApriCartella.Click += (_, _) =>
+62
View File
@@ -0,0 +1,62 @@
namespace CadSearchGui;
/// <summary>Mostra, per ogni assieme della distinta base, se tutti i suoi componenti
/// (ricorsivamente fino ai PRT) sono presenti nell'indice (v) o mancano (x).</summary>
public class ReportCompletezzaForm : Form
{
public ReportCompletezzaForm(List<(string Componente, string Descrizione, bool? Completo)> righe)
{
int completi = righe.Count(r => r.Completo == true);
int incompleti = righe.Count(r => r.Completo == false);
int nonTrovati = righe.Count(r => r.Completo is null);
Text = "Completezza assiemi";
ClientSize = new Size(720, 440);
StartPosition = FormStartPosition.CenterParent;
Font = new Font("Segoe UI", 9.5f);
var lblInfo = new Label
{
Dock = DockStyle.Top,
AutoSize = false,
Height = 48,
Padding = new Padding(12, 8, 12, 8),
Text = $"{completi} completi, {incompleti} incompleti, {nonTrovati} non trovati nell'indice "
+ $"su {righe.Count} componenti. Nota: la scansione dei .asm è euristica, qualche "
+ "falso 'incompleto' è possibile.",
};
var grid = new DataGridView
{
Dock = DockStyle.Fill,
ReadOnly = true,
AllowUserToAddRows = false,
AllowUserToDeleteRows = false,
RowHeadersVisible = false,
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
};
grid.Columns.Add("componente", "Componente");
grid.Columns.Add("descrizione", "Descrizione");
grid.Columns.Add("completo", "Completo");
grid.Columns["componente"]!.FillWeight = 20;
grid.Columns["descrizione"]!.FillWeight = 55;
grid.Columns["completo"]!.FillWeight = 25;
foreach (var (componente, descrizione, completo) in righe.OrderBy(r => r.Completo).ThenBy(r => r.Componente))
{
string testo = completo switch { true => "✓", false => "✗", null => "?" };
int i = grid.Rows.Add(componente, descrizione, testo);
grid.Rows[i].DefaultCellStyle.ForeColor = completo switch
{
true => Color.DarkGreen,
false => Color.DarkRed,
null => Color.Gray,
};
grid.Rows[i].Cells["completo"].Style.Alignment = DataGridViewContentAlignment.MiddleCenter;
}
Controls.Add(grid);
Controls.Add(lblInfo);
}
}
+66
View File
@@ -0,0 +1,66 @@
using System.Diagnostics;
namespace CadSearchGui;
/// <summary>Mostra, per ogni componente della distinta base, se lo STEP corrispondente
/// è già stato esportato (file .stp presente in "step") o manca ancora.</summary>
public class ReportStepForm : Form
{
public ReportStepForm(List<(string Componente, string Descrizione, bool Realizzato)> righe, string cartellaStep)
{
int realizzati = righe.Count(r => r.Realizzato);
Text = "Report STEP realizzati";
ClientSize = new Size(720, 440);
StartPosition = FormStartPosition.CenterParent;
Font = new Font("Segoe UI", 9.5f);
var lblInfo = new Label
{
Dock = DockStyle.Top,
AutoSize = false,
Height = 32,
Padding = new Padding(12, 8, 12, 8),
Text = $"{realizzati} componenti realizzati su {righe.Count} ({righe.Count - realizzati} mancanti), "
+ $"confrontati con \"{cartellaStep}\".",
};
var grid = new DataGridView
{
Dock = DockStyle.Fill,
ReadOnly = true,
AllowUserToAddRows = false,
AllowUserToDeleteRows = false,
RowHeadersVisible = false,
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
};
grid.Columns.Add("componente", "Componente");
grid.Columns.Add("descrizione", "Descrizione");
grid.Columns.Add("step", "STEP");
grid.Columns["componente"]!.FillWeight = 20;
grid.Columns["descrizione"]!.FillWeight = 55;
grid.Columns["step"]!.FillWeight = 25;
foreach (var (componente, descrizione, realizzato) in righe.OrderBy(r => r.Realizzato).ThenBy(r => r.Componente))
{
int i = grid.Rows.Add(componente, descrizione, realizzato ? "Realizzato" : "Mancante");
grid.Rows[i].DefaultCellStyle.ForeColor = realizzato ? Color.DarkGreen : Color.DarkRed;
}
var btnApriCartella = new Button { Text = "Apri cartella step", Dock = DockStyle.Bottom, Height = 34 };
btnApriCartella.Click += (_, _) =>
{
try { Process.Start("explorer.exe", cartellaStep); }
catch (Exception ex)
{
MessageBox.Show(this, $"Impossibile aprire Esplora risorse:\n{ex.Message}", "Errore",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
};
Controls.Add(grid);
Controls.Add(lblInfo);
Controls.Add(btnApriCartella);
}
}
+28 -7
View File
@@ -10,14 +10,25 @@ CAD di ogni componente.
1. **Indice CAD** — all'avvio carica automaticamente `cad_index.db` (se 1. **Indice CAD** — all'avvio carica automaticamente `cad_index.db` (se
presente accanto all'eseguibile). Se manca, si può selezionare un DB presente accanto all'eseguibile). Se manca, si può selezionare un DB
esistente con **Apri DB...** o generarne uno nuovo con **Aggiorna esistente con **Apri DB...** o generarne uno nuovo con **Aggiorna
indice**, che scansiona il disco (di default `X:\`, cartelle indice**, che scansiona il disco (di default `X:\`, tutte le cartelle di primo
`__Accessori`, `__B.end`, `__Corpi`, `__Fondi`, `Unificati`, `Commerciali`) e indicizza tutti i file. livello che trova) e indicizza tutti i file.
2. **Importa Distinta Base** — trascina (o scegli da file) l'estrazione 2. **Importa Distinta Base** — trascina (o scegli da file) l'estrazione
`.xls`/`.xml` del gestionale (foglio "Distinta Base", colonne `.xls`/`.xml` del gestionale (foglio "Distinta Base", colonne
Componente/Descrizione): viene generato un CSV con anteprima in griglia. Componente/Descrizione): viene generato un CSV con anteprima in griglia.
Se il file segue la convenzione di nome `DBP_<codice>_<timestamp>` del
gestionale, l'assieme generale dell'estrazione viene aggiunto come riga
in più nel CSV (il foglio elenca solo i componenti sotto l'assieme, non
l'assieme stesso).
3. **Crea cartella raw** — cerca nell'indice il percorso CAD di ogni 3. **Crea cartella raw** — cerca nell'indice il percorso CAD di ogni
componente importato e copia i file trovati in una cartella `raw/` componente importato (assieme generale incluso) e copia i file trovati
accanto al file di distinta originale. in una cartella `raw/` accanto al file di distinta originale.
4. **Trova componenti referenziati** — legge i metadati dei file `.asm` in
`raw/` per trovare ricorsivamente i sotto-assiemi/parti che li
compongono, copiandoli a loro volta in `raw/`.
5. **Verifica completezza assiemi** / **Report STEP realizzati** — confronta
la distinta con l'indice (per capire se manca qualche sotto-componente)
e con la cartella `step/` (per capire quali STEP sono già stati
realizzati).
## Requisiti ## Requisiti
@@ -75,7 +86,17 @@ indicizzazione integrate):
- `ProgressForm.cs` — dialog di avanzamento durante l'aggiornamento indice. - `ProgressForm.cs` — dialog di avanzamento durante l'aggiornamento indice.
Il database generato (`cad_index.db`), i file Excel/CSV della distinta Il database generato (`cad_index.db`), i file Excel/CSV della distinta
trascinati nella root e la cartella `raw/` sono rigenerabili e non sono trascinati nella root e le cartelle `raw/`/`step/` sono rigenerabili e non
tracciati in git (vedi `.gitignore`). sono tracciati in git (vedi `.gitignore`).
Non ci sono test automatizzati nel repo. ## Test
`CadSearchGui.Tests/` contiene unit test (regex di estrazione dei componenti
dai `.asm`, parsing del nome file Pro-E) e integration test (schema/vista SQL
reale, con focus sui bug di case-sensitivity/duplicati già risolti). Da
eseguire:
```powershell
cd CadSearchGui.Tests
dotnet test
```