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:
1 parent
146ce78eae
commit
52483177f0
16 files changed
+1144
-249
No files matched your search
@@ -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.*
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,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
@@ -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;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 += (_, _) =>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
```
|
||||||
Reference in new issue
Block a user