2026-09-16 09:58:55 +02:00
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
|
using Microsoft.Data.Sqlite;
|
|
|
|
|
|
|
2026-09-21 11:18:43 +02:00
|
|
|
|
namespace PrepStep;
|
2026-09-16 09:58:55 +02:00
|
|
|
|
|
|
|
|
|
|
public partial class Form1 : Form
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly Button _btnApriDb = new();
|
|
|
|
|
|
private readonly Button _btnAggiorna = new();
|
2026-09-16 10:59:16 +02:00
|
|
|
|
private readonly Button _btnCreaRaw = new();
|
2026-09-16 11:40:34 +02:00
|
|
|
|
private readonly Button _btnTrovaPrt = new();
|
2026-09-17 09:08:33 +02:00
|
|
|
|
private readonly Button _btnVerifica = new();
|
2026-09-16 16:25:52 +02:00
|
|
|
|
private readonly Button _btnCompletezza = new();
|
2026-09-21 09:54:06 +02:00
|
|
|
|
private readonly ListView _lstAnteprimaAsm = new();
|
2026-09-16 12:05:07 +02:00
|
|
|
|
private readonly Button _btnPulisci = new();
|
2026-09-16 09:58:55 +02:00
|
|
|
|
private readonly Label _lblStatus = new();
|
|
|
|
|
|
private readonly Label _lblDb = new();
|
2026-09-16 16:25:52 +02:00
|
|
|
|
private readonly Label _lblUltimoAggiornamento = new();
|
2026-09-21 11:18:43 +02:00
|
|
|
|
private readonly Label _lblCartellaLavoro = new();
|
|
|
|
|
|
private readonly Button _btnCambiaCartella = new();
|
2026-09-16 10:22:07 +02:00
|
|
|
|
private readonly DataGridView _gridImport = new();
|
2026-09-16 16:25:52 +02:00
|
|
|
|
private readonly DataGridView _gridRaw = new();
|
2026-09-16 10:22:07 +02:00
|
|
|
|
private readonly Label _lblImportStatus = new();
|
2026-09-16 10:59:16 +02:00
|
|
|
|
private readonly ToolTip _tipImportStatus = new();
|
2026-09-16 09:58:55 +02:00
|
|
|
|
|
2026-09-16 16:25:52 +02:00
|
|
|
|
// 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",
|
2026-09-21 11:23:46 +02:00
|
|
|
|
"5. Verifica completezza",
|
2026-09-16 16:25:52 +02:00
|
|
|
|
};
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
2026-09-16 09:58:55 +02:00
|
|
|
|
private static readonly string[] TipiPredefiniti = { "prt", "asm" };
|
|
|
|
|
|
private const string RadiceDiscoDefault = @"X:\";
|
|
|
|
|
|
|
|
|
|
|
|
private string? _dbPath;
|
2026-09-21 11:18:43 +02:00
|
|
|
|
private string? _cartellaLavoro;
|
2026-09-16 10:59:16 +02:00
|
|
|
|
private string? _percorsoInputCorrente;
|
|
|
|
|
|
private List<(string Componente, string Descrizione)>? _righeCorrenti;
|
2026-09-17 09:13:38 +02:00
|
|
|
|
private Dictionary<string, List<string>>? _ultimaVerifica;
|
2026-09-16 11:40:34 +02:00
|
|
|
|
private string? _ultimaCartellaRaw;
|
2026-09-16 09:58:55 +02:00
|
|
|
|
|
|
|
|
|
|
public Form1()
|
|
|
|
|
|
{
|
|
|
|
|
|
InitializeComponent();
|
|
|
|
|
|
BuildUi();
|
2026-09-21 11:18:43 +02:00
|
|
|
|
Load += (_, _) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
InizializzaDatabase();
|
2026-09-21 14:33:18 +02:00
|
|
|
|
InizializzaCartellaLavoro();
|
2026-09-21 11:18:43 +02:00
|
|
|
|
};
|
2026-09-16 09:58:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 10:59:16 +02:00
|
|
|
|
// Palette: blu primario per l'azione principale del flusso ("Crea cartella raw"),
|
|
|
|
|
|
// il resto resta ai colori di sistema per restare coerente col tema Windows dell'utente.
|
|
|
|
|
|
private static readonly Color ColorePrimario = Color.FromArgb(0, 99, 177);
|
|
|
|
|
|
|
2026-09-16 09:58:55 +02:00
|
|
|
|
private void BuildUi()
|
|
|
|
|
|
{
|
2026-09-16 10:28:15 +02:00
|
|
|
|
AllowDrop = true;
|
2026-09-21 11:00:01 +02:00
|
|
|
|
// Versione letta dall'assembly (<Version> nel csproj) invece che duplicata qui a mano.
|
|
|
|
|
|
var versione = typeof(Form1).Assembly.GetName().Version;
|
2026-09-21 11:18:43 +02:00
|
|
|
|
Text = $"Cevolani StepPrep {versione?.ToString(3)}";
|
2026-09-16 10:59:16 +02:00
|
|
|
|
MinimumSize = new Size(760, 480);
|
|
|
|
|
|
Font = new Font("Segoe UI", 9.5f);
|
2026-09-21 11:00:01 +02:00
|
|
|
|
// Stessa icona di ApplicationIcon nel csproj: la si rilegge dall'exe compilato invece
|
|
|
|
|
|
// di duplicare il file .ico come Content, funziona identica in Debug/Release/publish.
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex) when (ex is IOException or ArgumentException)
|
|
|
|
|
|
{
|
|
|
|
|
|
// ponytail: nessuna icona nell'exe (es. lanciato da dotnet run) -> resta quella di default
|
|
|
|
|
|
}
|
2026-09-16 10:28:15 +02:00
|
|
|
|
|
2026-09-16 16:25:52 +02:00
|
|
|
|
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);
|
|
|
|
|
|
|
2026-09-22 10:02:14 +02:00
|
|
|
|
// Barra di navigazione come footer separato dal contenuto: linea sottile in cima e
|
|
|
|
|
|
// sfondo bianco, invece dei bottoni appoggiati direttamente sul contenuto/sulla barra
|
|
|
|
|
|
// di stato senza alcuna divisione visiva.
|
2026-09-21 14:16:32 +02:00
|
|
|
|
// Padding destro = 16, lo stesso usato dai pannelli di ogni fase (Padding(16)): prima
|
|
|
|
|
|
// era 12, quindi "Avanti" sporgeva 4px oltre il margine destro del contenuto sopra.
|
|
|
|
|
|
var navContainer = new Panel { Dock = DockStyle.Bottom, Height = 64, BackColor = Color.White };
|
|
|
|
|
|
var navSeparatore = new Panel
|
|
|
|
|
|
{
|
|
|
|
|
|
Dock = DockStyle.Top,
|
|
|
|
|
|
Height = 1,
|
|
|
|
|
|
BackColor = Color.FromArgb(220, 220, 220),
|
|
|
|
|
|
};
|
2026-09-16 16:25:52 +02:00
|
|
|
|
var navPanel = new FlowLayoutPanel
|
|
|
|
|
|
{
|
2026-09-21 14:16:32 +02:00
|
|
|
|
Dock = DockStyle.Fill,
|
2026-09-16 16:25:52 +02:00
|
|
|
|
FlowDirection = FlowDirection.RightToLeft,
|
2026-09-21 14:16:32 +02:00
|
|
|
|
Padding = new Padding(16, 14, 16, 14),
|
2026-09-16 16:25:52 +02:00
|
|
|
|
};
|
2026-09-21 10:40:32 +02:00
|
|
|
|
_btnAvanti.Text = "Avanti ›";
|
|
|
|
|
|
UiStile.Primario(_btnAvanti);
|
|
|
|
|
|
UiStile.Arrotonda(_btnAvanti);
|
2026-09-16 16:25:52 +02:00
|
|
|
|
_btnAvanti.Click += (_, _) => VaiAllaFase(_faseAttuale + 1);
|
|
|
|
|
|
navPanel.Controls.Add(_btnAvanti);
|
|
|
|
|
|
|
2026-09-21 10:40:32 +02:00
|
|
|
|
_btnIndietro.Text = "‹ Indietro";
|
2026-09-16 16:25:52 +02:00
|
|
|
|
_btnIndietro.Margin = new Padding(8, 0, 0, 0);
|
2026-09-21 10:40:32 +02:00
|
|
|
|
UiStile.Secondario(_btnIndietro);
|
|
|
|
|
|
UiStile.Arrotonda(_btnIndietro);
|
2026-09-16 16:25:52 +02:00
|
|
|
|
_btnIndietro.Click += (_, _) => VaiAllaFase(_faseAttuale - 1);
|
|
|
|
|
|
navPanel.Controls.Add(_btnIndietro);
|
2026-09-21 14:16:32 +02:00
|
|
|
|
navContainer.Controls.Add(navPanel);
|
|
|
|
|
|
navContainer.Controls.Add(navSeparatore);
|
2026-09-16 16:25:52 +02:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-21 14:16:32 +02:00
|
|
|
|
// Ordine dei Dock.Bottom: l'ultimo aggiunto è il più esterno (il vero bordo inferiore
|
|
|
|
|
|
// della finestra). La barra di stato va tra contenuto e azioni; i bottoni Avanti/Indietro
|
|
|
|
|
|
// restano il footer più esterno, come in ogni wizard/dialog standard di Windows.
|
2026-09-16 16:25:52 +02:00
|
|
|
|
Controls.Add(_faseContainer);
|
|
|
|
|
|
Controls.Add(statusPanel);
|
2026-09-21 14:16:32 +02:00
|
|
|
|
Controls.Add(navContainer);
|
2026-09-16 16:25:52 +02:00
|
|
|
|
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();
|
2026-09-21 09:54:06 +02:00
|
|
|
|
|
|
|
|
|
|
if (_faseAttuale == 4)
|
|
|
|
|
|
AggiornaAnteprimaAsm();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Popola le icone trascinabili (drag&drop) dei file .asm della distinta importata
|
|
|
|
|
|
/// (non i sotto-assiemi di fase 4, che in "raw" servono solo a completarla) già copiati in
|
|
|
|
|
|
/// "raw" (non riletti dal DB/share: sono già in locale), mostrate in fase 5 prima ancora di
|
|
|
|
|
|
/// avviare la verifica.</summary>
|
|
|
|
|
|
private void AggiornaAnteprimaAsm()
|
|
|
|
|
|
{
|
|
|
|
|
|
_lstAnteprimaAsm.Items.Clear();
|
2026-09-21 14:16:32 +02:00
|
|
|
|
_lstAnteprimaAsm.SmallImageList!.Images.Clear();
|
2026-09-21 09:54:06 +02:00
|
|
|
|
if (_ultimaCartellaRaw is null || !Directory.Exists(_ultimaCartellaRaw) || _righeCorrenti is null)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
var codiciDistinta = _righeCorrenti.Select(r => r.Componente).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
|
var percorsiAsm = Directory.GetFiles(_ultimaCartellaRaw)
|
|
|
|
|
|
.Where(p =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var (codice, tipo, _) = Indexer.ParseFilename(Path.GetFileName(p));
|
|
|
|
|
|
return tipo.Equals("asm", StringComparison.OrdinalIgnoreCase) && codiciDistinta.Contains(codice);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var percorso in percorsiAsm)
|
|
|
|
|
|
{
|
|
|
|
|
|
Icon? icona = null;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
icona = Icon.ExtractAssociatedIcon(percorso);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException)
|
|
|
|
|
|
{
|
|
|
|
|
|
// ponytail: file irraggiungibile (share offline) -> icona generica, non blocca gli altri
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-21 14:16:32 +02:00
|
|
|
|
int indice = _lstAnteprimaAsm.SmallImageList.Images.Count;
|
|
|
|
|
|
_lstAnteprimaAsm.SmallImageList.Images.Add(icona?.ToBitmap() ?? SystemIcons.WinLogo.ToBitmap());
|
2026-09-21 09:54:06 +02:00
|
|
|
|
icona?.Dispose();
|
|
|
|
|
|
_lstAnteprimaAsm.Items.Add(new ListViewItem(Path.GetFileName(percorso), indice) { Tag = percorso });
|
|
|
|
|
|
}
|
2026-09-16 16:25:52 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Condizione per cui la fase indicata è "conclusa" e si può passare alla successiva.</summary>
|
|
|
|
|
|
private bool AvantiConsentito(int fase) => fase switch
|
|
|
|
|
|
{
|
2026-09-21 11:18:43 +02:00
|
|
|
|
0 => _dbPath is not null && _cartellaLavoro is not null,
|
2026-09-16 16:25:52 +02:00
|
|
|
|
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
|
2026-09-16 09:58:55 +02:00
|
|
|
|
{
|
|
|
|
|
|
Dock = DockStyle.Top,
|
2026-09-16 10:59:16 +02:00
|
|
|
|
AutoSize = true,
|
|
|
|
|
|
AutoSizeMode = AutoSizeMode.GrowAndShrink,
|
|
|
|
|
|
ColumnCount = 1,
|
2026-09-16 16:25:52 +02:00
|
|
|
|
Padding = new Padding(16),
|
2026-09-16 09:58:55 +02:00
|
|
|
|
};
|
2026-09-21 10:40:32 +02:00
|
|
|
|
// Colonna al 100%: le label sotto (Dock=Top) vanno a capo sulla larghezza reale della
|
|
|
|
|
|
// finestra invece che su un limite fisso in pixel, e si riadattano se viene ridimensionata.
|
|
|
|
|
|
contenuto.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
2026-09-16 10:22:07 +02:00
|
|
|
|
|
2026-09-16 16:25:52 +02:00
|
|
|
|
var lblIstruzioni = new Label
|
|
|
|
|
|
{
|
2026-09-21 10:40:32 +02:00
|
|
|
|
Dock = DockStyle.Top,
|
2026-09-16 16:25:52 +02:00
|
|
|
|
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);
|
|
|
|
|
|
|
2026-09-21 10:40:32 +02:00
|
|
|
|
_lblDb.Dock = DockStyle.Top;
|
2026-09-16 16:25:52 +02:00
|
|
|
|
_lblDb.AutoSize = true;
|
|
|
|
|
|
_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.Margin = new Padding(0, 0, 8, 0);
|
2026-09-21 10:40:32 +02:00
|
|
|
|
UiStile.Secondario(_btnApriDb);
|
2026-09-16 16:25:52 +02:00
|
|
|
|
_btnApriDb.Click += (_, _) => ScegliDatabase();
|
|
|
|
|
|
bottoni.Controls.Add(_btnApriDb);
|
|
|
|
|
|
|
|
|
|
|
|
_btnAggiorna.Text = "Aggiorna indice";
|
2026-09-21 10:40:32 +02:00
|
|
|
|
UiStile.Secondario(_btnAggiorna);
|
2026-09-16 16:25:52 +02:00
|
|
|
|
_btnAggiorna.Click += (_, _) => AggiornaIndice();
|
|
|
|
|
|
bottoni.Controls.Add(_btnAggiorna);
|
|
|
|
|
|
|
|
|
|
|
|
contenuto.Controls.Add(bottoni);
|
|
|
|
|
|
|
2026-09-21 11:18:43 +02:00
|
|
|
|
var lblIstruzioniCartella = new Label
|
|
|
|
|
|
{
|
|
|
|
|
|
Dock = DockStyle.Top,
|
|
|
|
|
|
AutoSize = true,
|
|
|
|
|
|
ForeColor = SystemColors.GrayText,
|
|
|
|
|
|
Margin = new Padding(0, 20, 0, 4),
|
2026-09-21 11:23:46 +02:00
|
|
|
|
Text = "Cartella di lavoro: qui vengono scritti il CSV della distinta e la "
|
|
|
|
|
|
+ "cartella 'raw' (gli STEP esportati con Creo vanno invece in 'step', "
|
|
|
|
|
|
+ "dentro la stessa cartella). Proposta automaticamente (ricordata da un "
|
|
|
|
|
|
+ "avvio all'altro), modificabile con 'Cambia cartella...'.",
|
2026-09-21 11:18:43 +02:00
|
|
|
|
};
|
|
|
|
|
|
contenuto.Controls.Add(lblIstruzioniCartella);
|
|
|
|
|
|
|
|
|
|
|
|
_lblCartellaLavoro.Dock = DockStyle.Top;
|
|
|
|
|
|
_lblCartellaLavoro.AutoSize = true;
|
|
|
|
|
|
_lblCartellaLavoro.ForeColor = SystemColors.GrayText;
|
|
|
|
|
|
_lblCartellaLavoro.Margin = new Padding(0, 0, 0, 4);
|
|
|
|
|
|
contenuto.Controls.Add(_lblCartellaLavoro);
|
|
|
|
|
|
|
|
|
|
|
|
_btnCambiaCartella.Text = "Cambia cartella...";
|
|
|
|
|
|
UiStile.Secondario(_btnCambiaCartella);
|
|
|
|
|
|
_btnCambiaCartella.Click += (_, _) => ScegliCartellaLavoro();
|
|
|
|
|
|
contenuto.Controls.Add(_btnCambiaCartella);
|
|
|
|
|
|
|
2026-09-16 16:25:52 +02:00
|
|
|
|
var lblManutenzione = new Label
|
|
|
|
|
|
{
|
2026-09-21 10:40:32 +02:00
|
|
|
|
Dock = DockStyle.Top,
|
2026-09-16 16:25:52 +02:00
|
|
|
|
AutoSize = true,
|
|
|
|
|
|
ForeColor = SystemColors.GrayText,
|
|
|
|
|
|
Margin = new Padding(0, 20, 0, 8),
|
2026-09-21 11:18:43 +02:00
|
|
|
|
Text = "Manutenzione: svuota le cartelle 'raw' e 'step' nella cartella di lavoro "
|
|
|
|
|
|
+ "prima di un nuovo giro (non tocca l'indice, solo le copie locali).",
|
2026-09-16 16:25:52 +02:00
|
|
|
|
};
|
|
|
|
|
|
contenuto.Controls.Add(lblManutenzione);
|
|
|
|
|
|
|
|
|
|
|
|
_btnPulisci.Text = "Pulisci raw e step";
|
2026-09-21 10:40:32 +02:00
|
|
|
|
UiStile.Secondario(_btnPulisci);
|
2026-09-16 16:25:52 +02:00
|
|
|
|
_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),
|
|
|
|
|
|
};
|
2026-09-21 10:40:32 +02:00
|
|
|
|
contenuto.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
2026-09-16 16:25:52 +02:00
|
|
|
|
|
|
|
|
|
|
var lblIstruzioni = new Label
|
|
|
|
|
|
{
|
2026-09-21 10:40:32 +02:00
|
|
|
|
Dock = DockStyle.Top,
|
2026-09-16 16:25:52 +02:00
|
|
|
|
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...",
|
|
|
|
|
|
Margin = new Padding(0, 0, 0, 8),
|
|
|
|
|
|
};
|
2026-09-21 10:40:32 +02:00
|
|
|
|
UiStile.Secondario(btnSfoglia);
|
2026-09-16 16:25:52 +02:00
|
|
|
|
btnSfoglia.Click += (_, _) => ScegliFileDistinta();
|
|
|
|
|
|
contenuto.Controls.Add(btnSfoglia);
|
|
|
|
|
|
|
2026-09-21 10:40:32 +02:00
|
|
|
|
_btnVerifica.Text = "Verifica presenza";
|
2026-09-17 09:08:33 +02:00
|
|
|
|
_btnVerifica.Margin = new Padding(0, 0, 0, 8);
|
2026-09-21 10:40:32 +02:00
|
|
|
|
UiStile.Secondario(_btnVerifica);
|
2026-09-17 09:08:33 +02:00
|
|
|
|
_btnVerifica.Click += (_, _) => VerificaCodiciNelDb();
|
|
|
|
|
|
contenuto.Controls.Add(_btnVerifica);
|
|
|
|
|
|
|
2026-09-21 10:40:32 +02:00
|
|
|
|
_lblImportStatus.Dock = DockStyle.Top;
|
2026-09-16 10:59:16 +02:00
|
|
|
|
_lblImportStatus.AutoSize = false;
|
|
|
|
|
|
_lblImportStatus.AutoEllipsis = true;
|
2026-09-16 16:25:52 +02:00
|
|
|
|
_lblImportStatus.Height = 24;
|
2026-09-16 10:22:07 +02:00
|
|
|
|
_lblImportStatus.ForeColor = SystemColors.GrayText;
|
2026-09-16 16:25:52 +02:00
|
|
|
|
contenuto.Controls.Add(_lblImportStatus);
|
2026-09-16 10:22:07 +02:00
|
|
|
|
|
|
|
|
|
|
_gridImport.Dock = DockStyle.Fill;
|
|
|
|
|
|
_gridImport.ReadOnly = true;
|
2026-09-16 10:59:16 +02:00
|
|
|
|
_gridImport.BorderStyle = BorderStyle.None;
|
|
|
|
|
|
_gridImport.BackgroundColor = SystemColors.Window;
|
|
|
|
|
|
_gridImport.RowHeadersVisible = false;
|
2026-09-16 10:22:07 +02:00
|
|
|
|
_gridImport.AllowUserToAddRows = false;
|
|
|
|
|
|
_gridImport.AllowUserToDeleteRows = false;
|
2026-09-16 10:59:16 +02:00
|
|
|
|
_gridImport.AllowUserToResizeRows = false;
|
2026-09-16 10:28:15 +02:00
|
|
|
|
_gridImport.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
|
|
|
|
|
|
_gridImport.MultiSelect = false;
|
2026-09-16 10:22:07 +02:00
|
|
|
|
_gridImport.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
|
2026-09-16 10:59:16 +02:00
|
|
|
|
_gridImport.ColumnHeadersHeight = 32;
|
|
|
|
|
|
_gridImport.ColumnHeadersDefaultCellStyle.Font = new Font(Font, FontStyle.Bold);
|
|
|
|
|
|
_gridImport.ColumnHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleLeft;
|
|
|
|
|
|
_gridImport.RowTemplate.Height = 26;
|
|
|
|
|
|
_gridImport.AlternatingRowsDefaultCellStyle.BackColor = Color.FromArgb(245, 247, 250);
|
|
|
|
|
|
_gridImport.DefaultCellStyle.SelectionBackColor = ColorePrimario;
|
|
|
|
|
|
_gridImport.DefaultCellStyle.Padding = new Padding(4, 0, 4, 0);
|
2026-09-16 10:22:07 +02:00
|
|
|
|
_gridImport.Columns.Add("componente", "Componente");
|
|
|
|
|
|
_gridImport.Columns.Add("descrizione", "Descrizione");
|
2026-09-16 16:25:52 +02:00
|
|
|
|
_gridImport.Columns["componente"]!.FillWeight = 30;
|
|
|
|
|
|
_gridImport.Columns["descrizione"]!.FillWeight = 70;
|
2026-09-16 10:22:07 +02:00
|
|
|
|
|
2026-09-16 16:25:52 +02:00
|
|
|
|
var panel = new Panel();
|
|
|
|
|
|
panel.Controls.Add(_gridImport);
|
|
|
|
|
|
panel.Controls.Add(contenuto);
|
|
|
|
|
|
return panel;
|
2026-09-16 10:22:07 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 16:25:52 +02:00
|
|
|
|
private Panel BuildFaseCreaRaw()
|
2026-09-16 10:59:16 +02:00
|
|
|
|
{
|
|
|
|
|
|
var contenuto = new TableLayoutPanel
|
|
|
|
|
|
{
|
|
|
|
|
|
Dock = DockStyle.Top,
|
|
|
|
|
|
AutoSize = true,
|
|
|
|
|
|
AutoSizeMode = AutoSizeMode.GrowAndShrink,
|
|
|
|
|
|
ColumnCount = 1,
|
2026-09-16 16:25:52 +02:00
|
|
|
|
Padding = new Padding(16),
|
2026-09-16 10:59:16 +02:00
|
|
|
|
};
|
2026-09-21 10:40:32 +02:00
|
|
|
|
contenuto.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
2026-09-16 10:59:16 +02:00
|
|
|
|
|
2026-09-16 16:25:52 +02:00
|
|
|
|
var lblIstruzioni = new Label
|
2026-09-16 10:59:16 +02:00
|
|
|
|
{
|
2026-09-21 10:40:32 +02:00
|
|
|
|
Dock = DockStyle.Top,
|
2026-09-16 10:59:16 +02:00
|
|
|
|
AutoSize = true,
|
|
|
|
|
|
ForeColor = SystemColors.GrayText,
|
2026-09-16 16:25:52 +02:00
|
|
|
|
Margin = new Padding(0, 0, 0, 8),
|
|
|
|
|
|
Text = "Cerca nell'indice il percorso CAD di ogni componente importato e copia i "
|
2026-09-21 11:18:43 +02:00
|
|
|
|
+ "file trovati in una cartella 'raw' dentro la cartella di lavoro (fase 1).",
|
2026-09-16 10:59:16 +02:00
|
|
|
|
};
|
2026-09-16 16:25:52 +02:00
|
|
|
|
contenuto.Controls.Add(lblIstruzioni);
|
2026-09-16 10:59:16 +02:00
|
|
|
|
|
|
|
|
|
|
_btnCreaRaw.Text = "Crea cartella raw";
|
2026-09-16 16:25:52 +02:00
|
|
|
|
_btnCreaRaw.Margin = new Padding(0, 0, 0, 12);
|
2026-09-21 10:40:32 +02:00
|
|
|
|
UiStile.Primario(_btnCreaRaw);
|
2026-09-16 10:59:16 +02:00
|
|
|
|
_btnCreaRaw.Click += (_, _) => CreaCartellaRaw();
|
|
|
|
|
|
contenuto.Controls.Add(_btnCreaRaw);
|
|
|
|
|
|
|
2026-09-16 16:25:52 +02:00
|
|
|
|
_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
|
2026-09-16 11:40:34 +02:00
|
|
|
|
{
|
2026-09-16 10:59:16 +02:00
|
|
|
|
Dock = DockStyle.Top,
|
|
|
|
|
|
AutoSize = true,
|
|
|
|
|
|
AutoSizeMode = AutoSizeMode.GrowAndShrink,
|
2026-09-16 16:25:52 +02:00
|
|
|
|
ColumnCount = 1,
|
|
|
|
|
|
Padding = new Padding(16),
|
2026-09-16 10:59:16 +02:00
|
|
|
|
};
|
2026-09-21 10:40:32 +02:00
|
|
|
|
contenuto.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
2026-09-16 16:25:52 +02:00
|
|
|
|
|
|
|
|
|
|
var lblIstruzioni = new Label
|
|
|
|
|
|
{
|
2026-09-21 10:40:32 +02:00
|
|
|
|
Dock = DockStyle.Top,
|
2026-09-16 16:25:52 +02:00
|
|
|
|
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";
|
2026-09-21 10:40:32 +02:00
|
|
|
|
UiStile.Secondario(_btnTrovaPrt);
|
2026-09-16 16:25:52 +02:00
|
|
|
|
_btnTrovaPrt.Click += (_, _) => TrovaPrtReferenziati();
|
|
|
|
|
|
contenuto.Controls.Add(_btnTrovaPrt);
|
|
|
|
|
|
|
|
|
|
|
|
var panel = new Panel();
|
|
|
|
|
|
panel.Controls.Add(contenuto);
|
|
|
|
|
|
return panel;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private Panel BuildFaseVerifica()
|
|
|
|
|
|
{
|
2026-09-21 10:40:32 +02:00
|
|
|
|
// Padding qui invece che su ogni sotto-pannello: si applica uniformemente a top/fill/bottom
|
|
|
|
|
|
// (il layout docked usa il DisplayRectangle del contenitore, già ridotto dal Padding).
|
|
|
|
|
|
var panel = new Panel { Padding = new Padding(16) };
|
|
|
|
|
|
|
|
|
|
|
|
var top = new TableLayoutPanel
|
2026-09-16 16:25:52 +02:00
|
|
|
|
{
|
|
|
|
|
|
Dock = DockStyle.Top,
|
|
|
|
|
|
AutoSize = true,
|
|
|
|
|
|
AutoSizeMode = AutoSizeMode.GrowAndShrink,
|
|
|
|
|
|
ColumnCount = 1,
|
|
|
|
|
|
};
|
2026-09-21 10:40:32 +02:00
|
|
|
|
top.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
2026-09-16 16:25:52 +02:00
|
|
|
|
|
|
|
|
|
|
var lblIstruzioniCompletezza = new Label
|
|
|
|
|
|
{
|
2026-09-21 10:40:32 +02:00
|
|
|
|
Dock = DockStyle.Top,
|
2026-09-16 16:25:52 +02:00
|
|
|
|
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.",
|
|
|
|
|
|
};
|
2026-09-21 10:40:32 +02:00
|
|
|
|
top.Controls.Add(lblIstruzioniCompletezza);
|
2026-09-16 16:25:52 +02:00
|
|
|
|
|
2026-09-21 14:16:32 +02:00
|
|
|
|
// Primario: è l'azione principale di questa fase, non una scelta secondaria — prima
|
|
|
|
|
|
// era uno stile grigio che si confondeva con lo sfondo.
|
2026-09-16 16:25:52 +02:00
|
|
|
|
_btnCompletezza.Text = "Verifica completezza assiemi";
|
2026-09-21 10:40:32 +02:00
|
|
|
|
_btnCompletezza.Margin = new Padding(0, 0, 0, 12);
|
2026-09-21 14:16:32 +02:00
|
|
|
|
UiStile.Primario(_btnCompletezza);
|
|
|
|
|
|
UiStile.Arrotonda(_btnCompletezza);
|
2026-09-16 16:25:52 +02:00
|
|
|
|
_btnCompletezza.Click += (_, _) => VerificaCompletezzaAssiemi();
|
2026-09-21 10:40:32 +02:00
|
|
|
|
top.Controls.Add(_btnCompletezza);
|
2026-09-16 16:25:52 +02:00
|
|
|
|
|
2026-09-21 09:54:06 +02:00
|
|
|
|
var lblAnteprimaAsm = new Label
|
|
|
|
|
|
{
|
|
|
|
|
|
AutoSize = true,
|
|
|
|
|
|
ForeColor = SystemColors.GrayText,
|
|
|
|
|
|
Margin = new Padding(0, 0, 0, 4),
|
|
|
|
|
|
Text = "File .asm della distinta trovati nell'indice (trascinali su Creo/Esplora risorse):",
|
|
|
|
|
|
};
|
2026-09-21 10:40:32 +02:00
|
|
|
|
top.Controls.Add(lblAnteprimaAsm);
|
2026-09-21 09:54:06 +02:00
|
|
|
|
|
2026-09-21 14:16:32 +02:00
|
|
|
|
// View.Details invece di LargeIcon: con codici Pro-E lunghi e simili tra loro
|
|
|
|
|
|
// ("9244218...", "9244228..."), le icone in griglia troncavano tutti i nomi allo stesso
|
|
|
|
|
|
// modo (illeggibili, indistinguibili) e lasciavano un grande vuoto sotto le poche righe.
|
|
|
|
|
|
// Una lista a riga singola mostra il nome per intero, in linea con lo stile delle altre
|
|
|
|
|
|
// griglie dell'app (_gridImport/_gridRaw), restando comunque trascinabile.
|
|
|
|
|
|
_lstAnteprimaAsm.View = View.Details;
|
2026-09-21 09:54:06 +02:00
|
|
|
|
_lstAnteprimaAsm.MultiSelect = true;
|
2026-09-21 14:16:32 +02:00
|
|
|
|
_lstAnteprimaAsm.FullRowSelect = true;
|
|
|
|
|
|
_lstAnteprimaAsm.GridLines = false;
|
|
|
|
|
|
_lstAnteprimaAsm.HeaderStyle = ColumnHeaderStyle.Nonclickable;
|
|
|
|
|
|
_lstAnteprimaAsm.SmallImageList = new ImageList { ImageSize = new Size(20, 20) };
|
|
|
|
|
|
_lstAnteprimaAsm.Columns.Add("File .asm", -2);
|
2026-09-21 10:40:32 +02:00
|
|
|
|
_lstAnteprimaAsm.Dock = DockStyle.Fill;
|
|
|
|
|
|
_lstAnteprimaAsm.Margin = new Padding(0, 4, 0, 12);
|
2026-09-21 14:16:32 +02:00
|
|
|
|
_lstAnteprimaAsm.BorderStyle = BorderStyle.None;
|
|
|
|
|
|
// La colonna unica segue sempre la larghezza disponibile (niente scroll orizzontale
|
|
|
|
|
|
// né spazio sprecato a destra quando la finestra viene ingrandita).
|
|
|
|
|
|
_lstAnteprimaAsm.Resize += (_, _) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_lstAnteprimaAsm.Columns.Count > 0)
|
|
|
|
|
|
_lstAnteprimaAsm.Columns[0].Width = _lstAnteprimaAsm.ClientSize.Width;
|
|
|
|
|
|
};
|
2026-09-21 09:54:06 +02:00
|
|
|
|
_lstAnteprimaAsm.ItemDrag += (_, e) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var percorsi = _lstAnteprimaAsm.SelectedItems.Cast<ListViewItem>()
|
|
|
|
|
|
.Select(item => (string)item.Tag!)
|
|
|
|
|
|
.ToArray();
|
|
|
|
|
|
if (percorsi.Length > 0)
|
|
|
|
|
|
_lstAnteprimaAsm.DoDragDrop(new DataObject(DataFormats.FileDrop, percorsi), DragDropEffects.Copy);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-09-21 10:40:32 +02:00
|
|
|
|
panel.Controls.Add(_lstAnteprimaAsm);
|
|
|
|
|
|
panel.Controls.Add(top);
|
2026-09-16 16:25:52 +02:00
|
|
|
|
return panel;
|
2026-09-16 10:59:16 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 10:22:07 +02:00
|
|
|
|
private void ScegliFileDistinta()
|
|
|
|
|
|
{
|
|
|
|
|
|
using var dialog = new OpenFileDialog
|
|
|
|
|
|
{
|
|
|
|
|
|
Filter = "File distinta base (*.xls;*.xml)|*.xls;*.xml|Tutti i file|*.*",
|
|
|
|
|
|
Title = "Seleziona il file Distinta Base",
|
|
|
|
|
|
};
|
|
|
|
|
|
if (dialog.ShowDialog(this) == DialogResult.OK)
|
|
|
|
|
|
ImportaDistinta(dialog.FileName);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void ImportaDistinta(string percorso)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-09-21 11:18:43 +02:00
|
|
|
|
var (csv, righe) = DistintaBaseImporter.ConvertiInCsv(percorso, _cartellaLavoro!);
|
2026-09-16 10:59:16 +02:00
|
|
|
|
_percorsoInputCorrente = percorso;
|
|
|
|
|
|
_righeCorrenti = righe;
|
2026-09-16 10:28:15 +02:00
|
|
|
|
|
2026-09-16 10:22:07 +02:00
|
|
|
|
_gridImport.Rows.Clear();
|
|
|
|
|
|
foreach (var (componente, descrizione) in righe)
|
|
|
|
|
|
{
|
2026-09-16 16:25:52 +02:00
|
|
|
|
_gridImport.Rows.Add(componente, descrizione);
|
2026-09-16 10:22:07 +02:00
|
|
|
|
}
|
2026-09-16 16:25:52 +02:00
|
|
|
|
_gridRaw.Rows.Clear();
|
|
|
|
|
|
_ultimaCartellaRaw = null;
|
2026-09-17 09:13:38 +02:00
|
|
|
|
_ultimaVerifica = null;
|
2026-09-16 10:28:15 +02:00
|
|
|
|
|
2026-09-16 10:59:16 +02:00
|
|
|
|
ImpostaStatoImport($"CSV generato ({righe.Count} righe): {csv}", Color.DarkGreen);
|
2026-09-16 10:22:07 +02:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
2026-09-16 10:59:16 +02:00
|
|
|
|
_percorsoInputCorrente = null;
|
|
|
|
|
|
_righeCorrenti = null;
|
2026-09-16 10:22:07 +02:00
|
|
|
|
_gridImport.Rows.Clear();
|
2026-09-16 10:59:16 +02:00
|
|
|
|
ImpostaStatoImport($"Errore durante la conversione: {ex.Message}", Color.DarkRed);
|
2026-09-16 10:22:07 +02:00
|
|
|
|
}
|
2026-09-16 16:25:52 +02:00
|
|
|
|
AggiornaStatoAvanti();
|
2026-09-16 09:58:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 10:59:16 +02:00
|
|
|
|
/// <summary>Imposta testo/colore dello stato import e il tooltip con il testo completo
|
|
|
|
|
|
/// (la label tronca con "..." quando il percorso non ci sta in una riga).</summary>
|
|
|
|
|
|
private void ImpostaStatoImport(string testo, Color colore)
|
|
|
|
|
|
{
|
|
|
|
|
|
_lblImportStatus.ForeColor = colore;
|
|
|
|
|
|
_lblImportStatus.Text = testo;
|
|
|
|
|
|
_tipImportStatus.SetToolTip(_lblImportStatus, testo);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-17 09:08:33 +02:00
|
|
|
|
private void VerificaCodiciNelDb()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_righeCorrenti is null)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
var trovati = TrovaPercorsiCad(_righeCorrenti.Select(r => r.Componente));
|
2026-09-17 09:13:38 +02:00
|
|
|
|
_ultimaVerifica = trovati;
|
2026-09-17 09:08:33 +02:00
|
|
|
|
foreach (DataGridViewRow riga in _gridImport.Rows)
|
|
|
|
|
|
{
|
|
|
|
|
|
string componente = (string)riga.Cells["componente"].Value!;
|
|
|
|
|
|
riga.DefaultCellStyle.BackColor = trovati.ContainsKey(componente)
|
|
|
|
|
|
? Color.FromArgb(198, 239, 206)
|
|
|
|
|
|
: Color.FromArgb(255, 199, 206);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 10:59:16 +02:00
|
|
|
|
private void CreaCartellaRaw()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_percorsoInputCorrente is null || _righeCorrenti is null)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
2026-09-16 16:25:52 +02:00
|
|
|
|
// 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.
|
2026-09-17 09:13:38 +02:00
|
|
|
|
var percorsiTrovati = _ultimaVerifica ?? TrovaPercorsiCad(_righeCorrenti.Select(r => r.Componente));
|
2026-09-16 10:59:16 +02:00
|
|
|
|
|
2026-09-16 16:25:52 +02:00
|
|
|
|
_gridRaw.Rows.Clear();
|
|
|
|
|
|
foreach (var (componente, _) in _righeCorrenti)
|
2026-09-16 10:59:16 +02:00
|
|
|
|
{
|
2026-09-16 16:25:52 +02:00
|
|
|
|
string percorsoCad = percorsiTrovati.TryGetValue(componente, out var lista)
|
2026-09-16 10:59:16 +02:00
|
|
|
|
? string.Join("; ", lista)
|
|
|
|
|
|
: "";
|
2026-09-16 16:25:52 +02:00
|
|
|
|
_gridRaw.Rows.Add(componente, percorsoCad);
|
2026-09-16 10:59:16 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-21 11:18:43 +02:00
|
|
|
|
var cartellaRaw = Path.Combine(_cartellaLavoro!, "raw");
|
2026-09-16 10:59:16 +02:00
|
|
|
|
int copiati = CopiaFileTrovati(percorsiTrovati.Values.SelectMany(p => p).Distinct(), cartellaRaw);
|
2026-09-16 11:40:34 +02:00
|
|
|
|
_ultimaCartellaRaw = cartellaRaw;
|
2026-09-16 10:59:16 +02:00
|
|
|
|
|
|
|
|
|
|
ImpostaStatoImport(
|
|
|
|
|
|
$"{percorsiTrovati.Count} componenti con percorso CAD trovato su {_righeCorrenti.Count}. "
|
|
|
|
|
|
+ $"{copiati} file copiati in {cartellaRaw}",
|
|
|
|
|
|
Color.DarkGreen);
|
2026-09-16 16:25:52 +02:00
|
|
|
|
AggiornaStatoAvanti();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-21 11:23:46 +02:00
|
|
|
|
/// <summary>Svuota in un colpo solo le cartelle "raw" e "step" nella cartella di lavoro,
|
|
|
|
|
|
/// previa un'unica conferma. Non tocca l'indice CAD: solo le copie locali generate dalla
|
|
|
|
|
|
/// pipeline.</summary>
|
2026-09-16 12:05:07 +02:00
|
|
|
|
private void PulisciRawEStep()
|
|
|
|
|
|
{
|
2026-09-21 11:18:43 +02:00
|
|
|
|
string radice = _cartellaLavoro!;
|
2026-09-21 11:23:46 +02:00
|
|
|
|
var cartelle = new[] { Path.Combine(radice, "raw"), Path.Combine(radice, "step") }
|
2026-09-16 12:05:07 +02:00
|
|
|
|
.Where(Directory.Exists)
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
if (cartelle.Count == 0)
|
|
|
|
|
|
{
|
2026-09-21 11:23:46 +02:00
|
|
|
|
ImpostaStatoImport($"Nessuna cartella 'raw' o 'step' trovata in {radice}.", Color.DarkRed);
|
2026-09-16 12:05:07 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var conferma = MessageBox.Show(this,
|
|
|
|
|
|
$"Eliminare tutti i file in:\n{string.Join("\n", cartelle)}\n\nL'operazione non è reversibile.",
|
|
|
|
|
|
"Pulisci raw e step", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
|
|
|
|
|
|
if (conferma != DialogResult.Yes)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
int eliminati = 0;
|
|
|
|
|
|
foreach (var cartella in cartelle)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var file in Directory.GetFiles(cartella))
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
File.Delete(file);
|
|
|
|
|
|
eliminati++;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
|
|
|
|
|
{
|
|
|
|
|
|
// ponytail: file in uso (es. aperto in Creo) -> si salta, non blocca gli altri
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImpostaStatoImport($"{eliminati} file eliminati da {string.Join(" e ", cartelle)}.", Color.DarkGreen);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 08:41:01 +02:00
|
|
|
|
/// <summary>Tutti i codici prt/asm noti all'indice, con i relativi percorsi (ultima
|
|
|
|
|
|
/// versione, un solo file per tipo - stesso criterio di dedup per data_modifica DESC
|
|
|
|
|
|
/// usato ovunque in questa classe). Caricato in un'unica query invece che una per
|
|
|
|
|
|
/// codice: serve sia a risolvere i percorsi dei componenti trovati, sia a
|
|
|
|
|
|
/// AssemblyComponentScanner per validare i token generici (parti commerciali).</summary>
|
|
|
|
|
|
private static Dictionary<string, List<(string Tipo, string Percorso)>> CaricaCodiciNoti(string dbPath)
|
|
|
|
|
|
{
|
|
|
|
|
|
var codiciNoti = new Dictionary<string, List<(string Tipo, string Percorso)>>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
|
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
|
|
|
|
|
|
connection.Open();
|
|
|
|
|
|
using var command = connection.CreateCommand();
|
|
|
|
|
|
command.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione "
|
|
|
|
|
|
+ "WHERE LOWER(tipo_file) IN ('prt', 'asm') ORDER BY codice, tipo_file, data_modifica DESC";
|
|
|
|
|
|
using var reader = command.ExecuteReader();
|
|
|
|
|
|
while (reader.Read())
|
|
|
|
|
|
{
|
|
|
|
|
|
string codice = reader.GetString(0);
|
|
|
|
|
|
string tipo = reader.GetString(1).ToLowerInvariant();
|
|
|
|
|
|
var lista = codiciNoti.TryGetValue(codice, out var l) ? l : codiciNoti[codice] = new();
|
|
|
|
|
|
if (!lista.Any(x => x.Tipo == tipo))
|
|
|
|
|
|
lista.Add((tipo, reader.GetString(2)));
|
|
|
|
|
|
}
|
|
|
|
|
|
return codiciNoti;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 11:52:16 +02:00
|
|
|
|
/// <summary>Tutti i file prt/asm dell'indice raggruppati per codice, tenendo per ogni
|
|
|
|
|
|
/// cartella la sola versione più alta. A differenza di <see cref="CaricaCodiciNoti"/> non
|
|
|
|
|
|
/// sceglie un vincitore fra le cartelle: lo stesso codice può esistere in più punti della
|
|
|
|
|
|
/// share (copie in cartelle di studio, di lavoro, temporanee) con contenuto diverso, e
|
|
|
|
|
|
/// quale sia quello giusto dipende da chi lo referenzia - vedi
|
|
|
|
|
|
/// <see cref="ScegliPerCartella"/>.</summary>
|
|
|
|
|
|
private static Dictionary<string, List<(string Tipo, string Percorso, int Versione, string Data)>> CaricaCandidati(string dbPath)
|
|
|
|
|
|
{
|
|
|
|
|
|
var candidati = new Dictionary<string, List<(string Tipo, string Percorso, int Versione, string Data)>>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
|
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
|
|
|
|
|
|
connection.Open();
|
|
|
|
|
|
using var command = connection.CreateCommand();
|
|
|
|
|
|
command.CommandText = "SELECT codice, tipo_file, percorso_completo, versione, data_modifica FROM files "
|
|
|
|
|
|
+ "WHERE LOWER(tipo_file) IN ('prt', 'asm')";
|
|
|
|
|
|
using var reader = command.ExecuteReader();
|
|
|
|
|
|
while (reader.Read())
|
|
|
|
|
|
{
|
|
|
|
|
|
string codice = reader.GetString(0);
|
|
|
|
|
|
string tipo = reader.GetString(1).ToLowerInvariant();
|
|
|
|
|
|
string percorso = reader.GetString(2);
|
|
|
|
|
|
int versione = reader.GetInt32(3);
|
|
|
|
|
|
var lista = candidati.TryGetValue(codice, out var l) ? l : candidati[codice] = new();
|
|
|
|
|
|
|
|
|
|
|
|
string cartella = Path.GetDirectoryName(percorso) ?? "";
|
|
|
|
|
|
int esistente = lista.FindIndex(x => x.Tipo == tipo
|
|
|
|
|
|
&& string.Equals(Path.GetDirectoryName(x.Percorso), cartella, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
|
if (esistente < 0)
|
|
|
|
|
|
lista.Add((tipo, percorso, versione, reader.GetString(4)));
|
|
|
|
|
|
else if (versione > lista[esistente].Versione)
|
|
|
|
|
|
lista[esistente] = (tipo, percorso, versione, reader.GetString(4));
|
|
|
|
|
|
}
|
|
|
|
|
|
return candidati;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Fra più copie dello stesso codice sulla share sceglie quella che userebbe Creo:
|
|
|
|
|
|
/// prima cerca nella cartella dell'assieme che la referenzia (Creo risolve i componenti
|
|
|
|
|
|
/// partendo da lì), poi ripiega sulla versione più alta e, a parità, sulla più recente.
|
|
|
|
|
|
/// Senza questo criterio si finisce per copiare in "raw" una copia omonima ma di un altro
|
|
|
|
|
|
/// modello - Creo la carica e i vincoli di accoppiamento non si risolvono, quindi l'assieme
|
|
|
|
|
|
/// non si apre affatto.</summary>
|
|
|
|
|
|
private static List<(string Tipo, string Percorso)> ScegliPerCartella(
|
|
|
|
|
|
List<(string Tipo, string Percorso, int Versione, string Data)> candidati, string? cartellaPadre)
|
|
|
|
|
|
{
|
|
|
|
|
|
return candidati
|
|
|
|
|
|
.GroupBy(c => c.Tipo)
|
|
|
|
|
|
.Select(perTipo =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var scelto = perTipo
|
|
|
|
|
|
.OrderByDescending(c => cartellaPadre is not null
|
|
|
|
|
|
&& string.Equals(Path.GetDirectoryName(c.Percorso), cartellaPadre, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
.ThenByDescending(c => c.Versione)
|
|
|
|
|
|
.ThenByDescending(c => c.Data)
|
|
|
|
|
|
.First();
|
|
|
|
|
|
return (scelto.Tipo, scelto.Percorso);
|
|
|
|
|
|
})
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-21 16:14:00 +02:00
|
|
|
|
/// <summary>Codici referenziati direttamente in un assieme: prova prima il parser esatto
|
|
|
|
|
|
/// dei record binari (<see cref="AsmRecordReader"/>, vedi docs/formato-asm-record.md), che
|
|
|
|
|
|
/// non ha i falsi positivi/negativi noti dell'euristica testuale (docs/formato-asm.md); se
|
|
|
|
|
|
/// il file non rispetta il formato validato (<see cref="InvalidDataException"/> - capita su
|
|
|
|
|
|
/// .asm di versioni Creo mai riscontrate nei campioni usati per il reverse engineering)
|
|
|
|
|
|
/// ripiega su <see cref="AssemblyComponentScanner"/>, che ha copertura più ampia anche se
|
|
|
|
|
|
/// meno precisa. Un'eventuale IOException (file irraggiungibile) non viene intercettata qui:
|
|
|
|
|
|
/// resta responsabilità del chiamante, come prima di questo cambiamento.</summary>
|
|
|
|
|
|
private static IEnumerable<string> EstraiCodiciReferenziati(string percorsoAsm, ICollection<string> codiciNoti)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
return AsmRecordReader.EstraiCodiciReferenziati(percorsoAsm);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (InvalidDataException)
|
|
|
|
|
|
{
|
|
|
|
|
|
return AssemblyComponentScanner.EstraiCodiciReferenziati(percorsoAsm, codiciNoti);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 11:52:16 +02:00
|
|
|
|
/// <summary>Componenti soppressi di un assieme (<see cref="AsmRecordReader.EstraiCodiciSoppressi"/>):
|
|
|
|
|
|
/// non sono nella distinta, ma senza il loro file Creo non apre l'assieme. Vuoto per i file
|
|
|
|
|
|
/// che il parser esatto non sa leggere: lì si usa l'euristica testuale, che i piazzamenti li
|
|
|
|
|
|
/// riconosce già per conto suo (<see cref="AssemblyComponentScanner"/>).</summary>
|
|
|
|
|
|
private static IEnumerable<string> CodiciSoppressi(string percorsoAsm)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
return AsmRecordReader.EstraiCodiciSoppressi(percorsoAsm);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (InvalidDataException)
|
|
|
|
|
|
{
|
|
|
|
|
|
return Array.Empty<string>();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Cartella della share da cui proviene un file già copiato in "raw": fra le copie
|
|
|
|
|
|
/// omonime indicizzate è quella di pari dimensione. Serve per gli assiemi di partenza, che
|
|
|
|
|
|
/// arrivano dalla fase precedente senza portarsi dietro l'origine. Null se non si riesce a
|
|
|
|
|
|
/// stabilirla (il file non è nell'indice, o la share non risponde): in quel caso la scelta
|
|
|
|
|
|
/// del componente ricade sul criterio generale.</summary>
|
|
|
|
|
|
private static string? CartellaDiOrigine(string fileInRaw,
|
|
|
|
|
|
Dictionary<string, List<(string Tipo, string Percorso, int Versione, string Data)>> candidati)
|
|
|
|
|
|
{
|
|
|
|
|
|
string nome = Path.GetFileName(fileInRaw);
|
|
|
|
|
|
if (!candidati.TryGetValue(nome.Split('.')[0], out var perCodice))
|
|
|
|
|
|
return null;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
long dimensione = new FileInfo(fileInRaw).Length;
|
|
|
|
|
|
foreach (var c in perCodice.Where(c => string.Equals(Path.GetFileName(c.Percorso), nome, StringComparison.OrdinalIgnoreCase)))
|
|
|
|
|
|
{
|
|
|
|
|
|
var info = new FileInfo(c.Percorso);
|
|
|
|
|
|
if (info.Exists && info.Length == dimensione)
|
|
|
|
|
|
return Path.GetDirectoryName(c.Percorso);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
|
|
|
|
|
{
|
|
|
|
|
|
// share non raggiungibile: si prosegue col criterio generale
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 16:25:52 +02:00
|
|
|
|
/// <summary>Risolve ricorsivamente l'albero dei componenti a partire dai file .asm indicati
|
|
|
|
|
|
/// (già copiati in "raw"): per ogni assieme legge i codici referenziati al suo interno, li
|
|
|
|
|
|
/// cerca nell'indice, copia in "raw" quelli trovati e, se sono a loro volta assiemi, li
|
|
|
|
|
|
/// accoda per essere scansionati finché non restano solo PRT (foglie) o codici non presenti
|
|
|
|
|
|
/// nell'indice. Usata dal bottone "Trova componenti referenziati".</summary>
|
|
|
|
|
|
private (List<(string Codice, string Tipo, string Percorso)> Trovati, List<string> NonTrovati) RisolviAlberoComponenti(
|
2026-09-17 09:55:14 +02:00
|
|
|
|
IEnumerable<string> asmIniziali, IProgress<(int Fatti, int Totale)>? progress = null)
|
2026-09-16 16:25:52 +02:00
|
|
|
|
{
|
2026-09-22 11:52:16 +02:00
|
|
|
|
// Ogni assieme viaggia con la cartella della share da cui proviene: e' li' che Creo
|
|
|
|
|
|
// cerca per primi i suoi componenti, quindi e' li' che li cerchiamo anche noi.
|
2026-09-22 14:09:48 +02:00
|
|
|
|
// "Esterno" marca i rami raggiunti da un riferimento esterno invece che dalla distinta:
|
|
|
|
|
|
// servono su disco ma non sono componenti, quindi i loro codici mancanti non vanno fra i
|
|
|
|
|
|
// "non trovati" (sarebbero falsi allarmi, come per i soppressi).
|
|
|
|
|
|
var daScansionare = new Queue<(string File, string? CartellaOrigine, bool Esterno)>(
|
|
|
|
|
|
asmIniziali.Select(f => (f, (string?)null, false)));
|
2026-09-22 11:52:16 +02:00
|
|
|
|
var codiciIniziali = asmIniziali.Select(f => Path.GetFileName(f).Split('.')[0]).ToList();
|
2026-09-16 16:25:52 +02:00
|
|
|
|
var codiciVisti = new HashSet<string>(
|
2026-09-17 15:32:46 +02:00
|
|
|
|
codiciIniziali.Concat(codiciIniziali.Select(AssemblyComponentScanner.CodiceRadice)),
|
|
|
|
|
|
StringComparer.OrdinalIgnoreCase);
|
2026-09-16 16:25:52 +02:00
|
|
|
|
|
2026-09-22 11:52:16 +02:00
|
|
|
|
var codiciSoppressiVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
2026-09-22 14:09:48 +02:00
|
|
|
|
var codiciEsterniVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
2026-09-16 16:25:52 +02:00
|
|
|
|
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);
|
|
|
|
|
|
|
2026-09-18 08:41:01 +02:00
|
|
|
|
var codiciNoti = CaricaCodiciNoti(_dbPath);
|
2026-09-22 11:52:16 +02:00
|
|
|
|
var candidati = CaricaCandidati(_dbPath);
|
2026-09-16 16:25:52 +02:00
|
|
|
|
|
2026-09-17 09:55:14 +02:00
|
|
|
|
int processati = 0;
|
2026-09-16 16:25:52 +02:00
|
|
|
|
while (daScansionare.Count > 0)
|
|
|
|
|
|
{
|
2026-09-22 14:09:48 +02:00
|
|
|
|
var (asm, cartellaOrigine, esterno) = daScansionare.Dequeue();
|
2026-09-22 11:52:16 +02:00
|
|
|
|
cartellaOrigine ??= CartellaDiOrigine(asm, candidati);
|
2026-09-17 09:55:14 +02:00
|
|
|
|
processati++;
|
2026-09-21 16:14:00 +02:00
|
|
|
|
foreach (var codice in EstraiCodiciReferenziati(asm, codiciNoti.Keys))
|
2026-09-16 16:25:52 +02:00
|
|
|
|
{
|
|
|
|
|
|
if (!codiciVisti.Add(codice))
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
2026-09-22 11:52:16 +02:00
|
|
|
|
if (!candidati.TryGetValue(codice, out var perCodice))
|
2026-09-16 16:25:52 +02:00
|
|
|
|
{
|
2026-09-22 14:09:48 +02:00
|
|
|
|
if (!esterno)
|
|
|
|
|
|
codiciNonTrovati.Add(codice);
|
2026-09-16 16:25:52 +02:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 11:52:16 +02:00
|
|
|
|
foreach (var (tipo, percorso) in ScegliPerCartella(perCodice, cartellaOrigine))
|
2026-09-16 16:25:52 +02:00
|
|
|
|
{
|
|
|
|
|
|
CopiaFileTrovati(new[] { percorso }, _ultimaCartellaRaw!);
|
|
|
|
|
|
trovati.Add((codice, tipo, percorso));
|
|
|
|
|
|
if (tipo == "asm")
|
|
|
|
|
|
{
|
2026-09-22 11:52:16 +02:00
|
|
|
|
daScansionare.Enqueue((Path.Combine(_ultimaCartellaRaw!, Path.GetFileName(percorso)),
|
2026-09-22 14:09:48 +02:00
|
|
|
|
Path.GetDirectoryName(percorso), esterno));
|
2026-09-16 16:25:52 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-22 11:52:16 +02:00
|
|
|
|
|
|
|
|
|
|
// I componenti soppressi non compaiono nella BOM, ma Creo ne vuole comunque il file
|
|
|
|
|
|
// per aprire l'assieme: si copiano senza entrare nel conteggio dei "non trovati"
|
|
|
|
|
|
// (non essendo componenti della distinta, segnalarli come mancanti sarebbe un falso
|
|
|
|
|
|
// allarme) e senza ricorrervi dentro.
|
|
|
|
|
|
foreach (var codice in CodiciSoppressi(asm))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!codiciSoppressiVisti.Add(codice) || codiciVisti.Contains(codice))
|
|
|
|
|
|
continue;
|
|
|
|
|
|
if (!candidati.TryGetValue(codice, out var perCodice))
|
|
|
|
|
|
continue;
|
|
|
|
|
|
foreach (var (tipo, percorso) in ScegliPerCartella(perCodice, cartellaOrigine))
|
|
|
|
|
|
{
|
|
|
|
|
|
CopiaFileTrovati(new[] { percorso }, _ultimaCartellaRaw!);
|
|
|
|
|
|
trovati.Add((codice, tipo, percorso));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-22 14:09:48 +02:00
|
|
|
|
|
|
|
|
|
|
// I riferimenti esterni (assembly feature, datum copiati verso un altro modello) non
|
|
|
|
|
|
// sono componenti e non compaiono fra i piazzamenti, ma Creo carica il modello citato
|
|
|
|
|
|
// per aprire l'assieme - e con lui i *suoi* componenti, quindi qui, a differenza dei
|
|
|
|
|
|
// soppressi, si ricorre dentro. Vedi docs/formato-asm-record.md §7.5.
|
|
|
|
|
|
foreach (var codice in AsmRecordReader.EstraiCodiciRiferimentiEsterni(asm))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!codiciEsterniVisti.Add(codice) || codiciVisti.Contains(codice))
|
|
|
|
|
|
continue;
|
|
|
|
|
|
if (!candidati.TryGetValue(codice, out var perCodice))
|
|
|
|
|
|
continue;
|
|
|
|
|
|
foreach (var (tipo, percorso) in ScegliPerCartella(perCodice, cartellaOrigine))
|
|
|
|
|
|
{
|
|
|
|
|
|
CopiaFileTrovati(new[] { percorso }, _ultimaCartellaRaw!);
|
|
|
|
|
|
trovati.Add((codice, tipo, percorso));
|
|
|
|
|
|
if (tipo == "asm")
|
|
|
|
|
|
{
|
|
|
|
|
|
daScansionare.Enqueue((Path.Combine(_ultimaCartellaRaw!, Path.GetFileName(percorso)),
|
|
|
|
|
|
Path.GetDirectoryName(percorso), true));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-17 09:55:14 +02:00
|
|
|
|
progress?.Report((processati, processati + daScansionare.Count));
|
2026-09-16 16:25:52 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (trovati, codiciNonTrovati);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-17 09:55:14 +02:00
|
|
|
|
private async void TrovaPrtReferenziati()
|
2026-09-16 11:40:34 +02:00
|
|
|
|
{
|
|
|
|
|
|
if (_ultimaCartellaRaw is null || !Directory.Exists(_ultimaCartellaRaw))
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
2026-09-16 12:05:07 +02:00
|
|
|
|
if (_dbPath is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
ImpostaStatoImport("Nessun database caricato: impossibile risolvere i componenti referenziati.", Color.DarkRed);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 16:25:52 +02:00
|
|
|
|
// L'assieme generale è già in raw come qualsiasi altro assieme importato (vedi
|
|
|
|
|
|
// CreaCartellaRaw), quindi rientra qui senza bisogno di un caso speciale.
|
2026-09-17 09:55:14 +02:00
|
|
|
|
var asmIniziali = Directory.GetFiles(_ultimaCartellaRaw).Where(f => Path.GetFileName(f).Contains(".asm.")).ToList();
|
|
|
|
|
|
|
|
|
|
|
|
using var progressForm = new ProgressoComponentiForm();
|
|
|
|
|
|
var progress = new Progress<(int Fatti, int Totale)>(p => progressForm.Aggiorna(p.Fatti, p.Totale));
|
|
|
|
|
|
var task = Task.Run(() => RisolviAlberoComponenti(asmIniziali, progress));
|
|
|
|
|
|
progressForm.Shown += async (_, _) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
await task;
|
|
|
|
|
|
progressForm.Close();
|
|
|
|
|
|
};
|
|
|
|
|
|
progressForm.ShowDialog(this);
|
|
|
|
|
|
|
|
|
|
|
|
var (trovati, codiciNonTrovati) = await task;
|
2026-09-16 11:40:34 +02:00
|
|
|
|
|
2026-09-16 12:05:07 +02:00
|
|
|
|
int numPrt = trovati.Count(r => r.Tipo == "prt");
|
|
|
|
|
|
int numAsm = trovati.Count(r => r.Tipo == "asm");
|
|
|
|
|
|
ImpostaStatoImport(
|
|
|
|
|
|
$"Componenti referenziati: {numPrt} PRT e {numAsm} sotto-assiemi trovati e copiati in raw, "
|
2026-09-16 16:25:52 +02:00
|
|
|
|
+ $"{codiciNonTrovati.Count} codici referenziati non trovati nell'indice.",
|
2026-09-16 12:05:07 +02:00
|
|
|
|
Color.DarkGreen);
|
|
|
|
|
|
|
2026-09-16 11:40:34 +02:00
|
|
|
|
using var dialog = new PrtMancantiForm(trovati, codiciNonTrovati, _ultimaCartellaRaw);
|
|
|
|
|
|
dialog.ShowDialog(this);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 16:25:52 +02:00
|
|
|
|
/// <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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-18 08:41:01 +02:00
|
|
|
|
var codiciNoti = CaricaCodiciNoti(_dbPath);
|
2026-09-16 16:25:52 +02:00
|
|
|
|
var cacheCodiciFile = new Dictionary<string, HashSet<string>>();
|
|
|
|
|
|
|
2026-09-18 08:41:01 +02:00
|
|
|
|
List<(string Tipo, string Percorso)> CercaComponenti(string codice) =>
|
|
|
|
|
|
codiciNoti.TryGetValue(codice, out var trovati) ? trovati : new List<(string, string)>();
|
2026-09-16 16:25:52 +02:00
|
|
|
|
|
|
|
|
|
|
bool AssiemeCompleto(string percorsoAsmIniziale)
|
|
|
|
|
|
{
|
|
|
|
|
|
var daVisitare = new Queue<string>();
|
|
|
|
|
|
daVisitare.Enqueue(percorsoAsmIniziale);
|
|
|
|
|
|
var fileVisitati = new HashSet<string> { percorsoAsmIniziale };
|
2026-09-17 15:32:46 +02:00
|
|
|
|
var codiceIniziale = Path.GetFileName(percorsoAsmIniziale).Split('.')[0];
|
|
|
|
|
|
var codiciVisti = new HashSet<string>
|
|
|
|
|
|
{
|
|
|
|
|
|
codiceIniziale,
|
|
|
|
|
|
AssemblyComponentScanner.CodiceRadice(codiceIniziale),
|
|
|
|
|
|
};
|
2026-09-16 16:25:52 +02:00
|
|
|
|
|
|
|
|
|
|
while (daVisitare.Count > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
var file = daVisitare.Dequeue();
|
|
|
|
|
|
if (!cacheCodiciFile.TryGetValue(file, out var codiciReferenziati))
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-09-21 16:14:00 +02:00
|
|
|
|
codiciReferenziati = EstraiCodiciReferenziati(file, codiciNoti.Keys).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
2026-09-16 16:25:52 +02:00
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 10:59:16 +02:00
|
|
|
|
/// <summary>Copia i file trovati nella cartella "raw" indicata, creandola se necessario.
|
2026-09-21 10:07:46 +02:00
|
|
|
|
/// Ritorna il numero di file copiati con successo. Parallelizzata (grado limitato): il
|
|
|
|
|
|
/// collo di bottiglia è la latenza di rete verso la share, non la CPU, quindi poche copie
|
|
|
|
|
|
/// concorrenti bastano a sovrapporre le attese senza saturare la share.</summary>
|
2026-09-16 11:40:34 +02:00
|
|
|
|
internal static int CopiaFileTrovati(IEnumerable<string> percorsi, string cartellaRaw)
|
2026-09-16 10:59:16 +02:00
|
|
|
|
{
|
2026-09-21 10:07:46 +02:00
|
|
|
|
Directory.CreateDirectory(cartellaRaw);
|
2026-09-16 10:59:16 +02:00
|
|
|
|
int copiati = 0;
|
2026-09-21 10:07:46 +02:00
|
|
|
|
Parallel.ForEach(percorsi, new ParallelOptions { MaxDegreeOfParallelism = 6 }, percorso =>
|
2026-09-16 10:59:16 +02:00
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
File.Copy(percorso, Path.Combine(cartellaRaw, Path.GetFileName(percorso)), overwrite: true);
|
2026-09-21 10:07:46 +02:00
|
|
|
|
Interlocked.Increment(ref copiati);
|
2026-09-16 10:59:16 +02:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
|
|
|
|
|
{
|
|
|
|
|
|
// ponytail: file non raggiungibile (share offline, permessi) -> lo si salta, non blocca gli altri
|
|
|
|
|
|
}
|
2026-09-21 10:07:46 +02:00
|
|
|
|
});
|
2026-09-16 10:59:16 +02:00
|
|
|
|
return copiati;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 10:28:15 +02:00
|
|
|
|
/// <summary>Cerca, per ogni codice componente, i percorsi dei file CAD (ultima versione,
|
|
|
|
|
|
/// solo prt/asm) corrispondenti nell'indice.</summary>
|
|
|
|
|
|
private Dictionary<string, List<string>> TrovaPercorsiCad(IEnumerable<string> codici)
|
|
|
|
|
|
{
|
|
|
|
|
|
var risultato = new Dictionary<string, List<string>>();
|
|
|
|
|
|
if (_dbPath is null)
|
|
|
|
|
|
return risultato;
|
|
|
|
|
|
|
|
|
|
|
|
using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly");
|
|
|
|
|
|
connection.Open();
|
|
|
|
|
|
|
|
|
|
|
|
var nomiParametri = TipiPredefiniti.Select((_, i) => $"@tipo{i}");
|
|
|
|
|
|
using var command = connection.CreateCommand();
|
|
|
|
|
|
command.CommandText = $"""
|
2026-09-16 16:25:52 +02:00
|
|
|
|
SELECT tipo_file, percorso_completo FROM files_ultima_versione
|
|
|
|
|
|
WHERE LOWER(codice) = LOWER(@codice) AND LOWER(tipo_file) IN ({string.Join(",", nomiParametri)})
|
|
|
|
|
|
ORDER BY tipo_file, data_modifica DESC
|
2026-09-16 10:28:15 +02:00
|
|
|
|
""";
|
|
|
|
|
|
var pCodice = command.Parameters.Add("@codice", Microsoft.Data.Sqlite.SqliteType.Text);
|
|
|
|
|
|
for (int i = 0; i < TipiPredefiniti.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
command.Parameters.AddWithValue($"@tipo{i}", TipiPredefiniti[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var codice in codici.Distinct())
|
|
|
|
|
|
{
|
|
|
|
|
|
pCodice.Value = codice;
|
|
|
|
|
|
using var reader = command.ExecuteReader();
|
|
|
|
|
|
var percorsi = new List<string>();
|
2026-09-16 16:25:52 +02:00
|
|
|
|
// 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>();
|
2026-09-16 10:28:15 +02:00
|
|
|
|
while (reader.Read())
|
|
|
|
|
|
{
|
2026-09-16 16:25:52 +02:00
|
|
|
|
if (tipiVisti.Add(reader.GetString(0).ToLowerInvariant()))
|
|
|
|
|
|
{
|
|
|
|
|
|
percorsi.Add(reader.GetString(1));
|
|
|
|
|
|
}
|
2026-09-16 10:28:15 +02:00
|
|
|
|
}
|
|
|
|
|
|
if (percorsi.Count > 0)
|
|
|
|
|
|
risultato[codice] = percorsi;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return risultato;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-21 11:18:43 +02:00
|
|
|
|
private static readonly string CartellaImpostazioni = Path.Combine(
|
|
|
|
|
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "PrepStep");
|
|
|
|
|
|
private static readonly string PercorsoImpostazioni = Path.Combine(CartellaImpostazioni, "ultimo_db.txt");
|
|
|
|
|
|
private static readonly string PercorsoImpostazioniCartella = Path.Combine(CartellaImpostazioni, "ultima_cartella_lavoro.txt");
|
|
|
|
|
|
|
2026-09-21 14:33:18 +02:00
|
|
|
|
/// <summary>Propone l'ultima cartella di lavoro usata (se ancora esiste su disco) o, se non
|
|
|
|
|
|
/// c'è ancora una preferenza salvata (o punta a una cartella cancellata), la cartella dove si
|
|
|
|
|
|
/// trova il DB (più sensata dell'eseguibile: quella è dove l'utente tiene il proprio lavoro,
|
|
|
|
|
|
/// non dov'è installata la app). Richiede quindi che InizializzaDatabase sia già stata
|
|
|
|
|
|
/// chiamata. Sempre modificabile con "Cambia cartella..." in fase 1.</summary>
|
2026-09-21 11:18:43 +02:00
|
|
|
|
private void InizializzaCartellaLavoro()
|
|
|
|
|
|
{
|
|
|
|
|
|
var ultima = LeggiUltimaCartellaLavoro();
|
2026-09-21 14:33:18 +02:00
|
|
|
|
_cartellaLavoro = ultima is not null && Directory.Exists(ultima)
|
|
|
|
|
|
? ultima
|
|
|
|
|
|
: (_dbPath is not null ? Path.GetDirectoryName(_dbPath) : null) ?? AppContext.BaseDirectory;
|
2026-09-21 11:18:43 +02:00
|
|
|
|
AggiornaLabelCartellaLavoro();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void ScegliCartellaLavoro()
|
|
|
|
|
|
{
|
|
|
|
|
|
using var dialog = new FolderBrowserDialog
|
|
|
|
|
|
{
|
2026-09-21 11:23:46 +02:00
|
|
|
|
Description = "Seleziona la cartella di lavoro (CSV, raw, step)",
|
2026-09-21 11:18:43 +02:00
|
|
|
|
SelectedPath = _cartellaLavoro ?? string.Empty,
|
|
|
|
|
|
};
|
|
|
|
|
|
if (dialog.ShowDialog(this) != DialogResult.OK)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
string percorso = dialog.SelectedPath;
|
|
|
|
|
|
_cartellaLavoro = percorso;
|
|
|
|
|
|
AggiornaLabelCartellaLavoro();
|
|
|
|
|
|
SalvaUltimaCartellaLavoro(percorso);
|
|
|
|
|
|
AggiornaStatoAvanti();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void AggiornaLabelCartellaLavoro() => _lblCartellaLavoro.Text = $"Cartella di lavoro: {_cartellaLavoro}";
|
|
|
|
|
|
|
|
|
|
|
|
private static string? LeggiUltimaCartellaLavoro()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
return File.Exists(PercorsoImpostazioniCartella) ? File.ReadAllText(PercorsoImpostazioniCartella).Trim() : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (IOException)
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void SalvaUltimaCartellaLavoro(string path)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
Directory.CreateDirectory(CartellaImpostazioni);
|
|
|
|
|
|
File.WriteAllText(PercorsoImpostazioniCartella, path);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (IOException)
|
|
|
|
|
|
{
|
|
|
|
|
|
// ponytail: non critico se non si riesce a salvare, si ripropone il default al prossimo avvio
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-16 10:59:16 +02:00
|
|
|
|
|
2026-09-16 09:58:55 +02:00
|
|
|
|
private void InizializzaDatabase()
|
|
|
|
|
|
{
|
|
|
|
|
|
var candidati = new[]
|
|
|
|
|
|
{
|
2026-09-16 10:59:16 +02:00
|
|
|
|
LeggiUltimoDbUsato(),
|
2026-09-16 09:58:55 +02:00
|
|
|
|
Path.Combine(AppContext.BaseDirectory, "cad_index.db"),
|
|
|
|
|
|
Path.Combine(Directory.GetCurrentDirectory(), "cad_index.db"),
|
|
|
|
|
|
};
|
2026-09-16 10:59:16 +02:00
|
|
|
|
_dbPath = candidati.FirstOrDefault(p => p is not null && File.Exists(p));
|
2026-09-16 09:58:55 +02:00
|
|
|
|
|
|
|
|
|
|
if (_dbPath is null)
|
|
|
|
|
|
{
|
2026-09-16 10:59:16 +02:00
|
|
|
|
_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.";
|
2026-09-16 16:25:52 +02:00
|
|
|
|
_lblUltimoAggiornamento.Text = "";
|
|
|
|
|
|
AggiornaStatoAvanti();
|
2026-09-16 09:58:55 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
CaricaDatabase(_dbPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void ScegliDatabase()
|
|
|
|
|
|
{
|
|
|
|
|
|
using var dialog = new OpenFileDialog
|
|
|
|
|
|
{
|
|
|
|
|
|
Filter = "Database SQLite (*.db)|*.db|Tutti i file (*.*)|*.*",
|
|
|
|
|
|
Title = "Seleziona cad_index.db",
|
|
|
|
|
|
};
|
|
|
|
|
|
if (dialog.ShowDialog(this) == DialogResult.OK)
|
|
|
|
|
|
{
|
|
|
|
|
|
CaricaDatabase(dialog.FileName);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void CaricaDatabase(string path)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
using var connection = new SqliteConnection($"Data Source={path};Mode=ReadOnly");
|
|
|
|
|
|
connection.Open();
|
|
|
|
|
|
|
|
|
|
|
|
long totale = (long)(new SqliteCommand("SELECT COUNT(*) FROM files", connection).ExecuteScalar() ?? 0L);
|
2026-09-16 16:25:52 +02:00
|
|
|
|
object? ultimaScansioneRaw = new SqliteCommand("SELECT MAX(scansionato_il) FROM files", connection).ExecuteScalar();
|
2026-09-16 09:58:55 +02:00
|
|
|
|
|
|
|
|
|
|
_dbPath = path;
|
|
|
|
|
|
_lblDb.Text = $"Database: {path} ({totale:N0} file indicizzati)";
|
2026-09-16 16:25:52 +02:00
|
|
|
|
_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";
|
2026-09-16 09:58:55 +02:00
|
|
|
|
_lblStatus.Text = "Pronto.";
|
2026-09-16 10:59:16 +02:00
|
|
|
|
SalvaUltimoDbUsato(path);
|
2026-09-16 09:58:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
MessageBox.Show(this, $"Impossibile aprire il database:\n{ex.Message}", "Errore",
|
|
|
|
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
|
|
|
|
}
|
2026-09-16 16:25:52 +02:00
|
|
|
|
AggiornaStatoAvanti();
|
2026-09-16 09:58:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 10:59:16 +02:00
|
|
|
|
/// <summary>Ricorda l'ultimo DB caricato con successo, così al prossimo avvio si
|
|
|
|
|
|
/// ricarica automaticamente anche se l'eseguibile gira da una cartella diversa.</summary>
|
|
|
|
|
|
private static string? LeggiUltimoDbUsato()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
return File.Exists(PercorsoImpostazioni) ? File.ReadAllText(PercorsoImpostazioni).Trim() : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (IOException)
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void SalvaUltimoDbUsato(string path)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-09-21 11:18:43 +02:00
|
|
|
|
Directory.CreateDirectory(CartellaImpostazioni);
|
2026-09-16 10:59:16 +02:00
|
|
|
|
File.WriteAllText(PercorsoImpostazioni, path);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (IOException)
|
|
|
|
|
|
{
|
|
|
|
|
|
// ponytail: non critico se non si riesce a salvare, si torna a chiedere "Apri DB..." la prossima volta
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 09:58:55 +02:00
|
|
|
|
private async void AggiornaIndice()
|
|
|
|
|
|
{
|
|
|
|
|
|
string dbPath = _dbPath ?? Path.Combine(AppContext.BaseDirectory, "cad_index.db");
|
|
|
|
|
|
|
|
|
|
|
|
if (!Directory.Exists(RadiceDiscoDefault))
|
|
|
|
|
|
{
|
|
|
|
|
|
MessageBox.Show(this, $"Disco non raggiungibile: {RadiceDiscoDefault}", "Errore",
|
|
|
|
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_btnAggiorna.Enabled = false;
|
|
|
|
|
|
_lblStatus.Text = "Aggiornamento indice in corso...";
|
|
|
|
|
|
|
|
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
|
|
using var dialog = new ProgressForm();
|
|
|
|
|
|
var progress = new Progress<ScanProgress>(dialog.Aggiorna);
|
|
|
|
|
|
dialog.Annullato += (_, _) => cts.Cancel();
|
|
|
|
|
|
|
|
|
|
|
|
var scanTask = Indexer.ScanAsync(RadiceDiscoDefault, dbPath, progress, cts.Token);
|
|
|
|
|
|
|
|
|
|
|
|
// Chiude automaticamente il dialog quando lo scan termina (successo, errore o annullamento)
|
|
|
|
|
|
_ = scanTask.ContinueWith(_ =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if (dialog.Visible)
|
|
|
|
|
|
{
|
|
|
|
|
|
dialog.BeginInvoke(dialog.Close);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, TaskScheduler.Default);
|
|
|
|
|
|
|
|
|
|
|
|
dialog.ShowDialog(this);
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
await scanTask;
|
|
|
|
|
|
_lblStatus.Text = "Indice aggiornato.";
|
|
|
|
|
|
CaricaDatabase(dbPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
|
{
|
|
|
|
|
|
_lblStatus.Text = "Aggiornamento annullato.";
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
MessageBox.Show(this, $"Errore durante l'aggiornamento dell'indice:\n{ex.Message}", "Errore",
|
|
|
|
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
|
|
|
|
_lblStatus.Text = "Aggiornamento fallito.";
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
_btnAggiorna.Enabled = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void ApriCartellaFileSelezionato()
|
|
|
|
|
|
{
|
2026-09-16 16:25:52 +02:00
|
|
|
|
if (_gridRaw.CurrentRow is null)
|
2026-09-16 09:58:55 +02:00
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-09-16 16:25:52 +02:00
|
|
|
|
string? percorsi = _gridRaw.CurrentRow.Cells[1].Value as string;
|
2026-09-16 10:28:15 +02:00
|
|
|
|
if (string.IsNullOrEmpty(percorsi))
|
2026-09-16 09:58:55 +02:00
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-09-16 10:28:15 +02:00
|
|
|
|
string primoPercorso = percorsi.Split("; ", 2)[0];
|
2026-09-16 09:58:55 +02:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-09-16 10:28:15 +02:00
|
|
|
|
Process.Start("explorer.exe", $"/select,\"{primoPercorso}\"");
|
2026-09-16 09:58:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
MessageBox.Show(this, $"Impossibile aprire Esplora risorse:\n{ex.Message}", "Errore",
|
|
|
|
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|