From 51c61eabf797432b8c83fd8729b3cbd799e9302d Mon Sep 17 00:00:00 2001 From: Alessio Dal Santo Date: Sat, 28 Jun 2025 02:05:59 +0200 Subject: [PATCH] feat: Implementa gestione intelligente della chiave sorgente con rilevamento PK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Aggiunge rilevamento automatico Primary Key per connessioni database - Rimuove completamente il fallback automatico per lato sorgente - Implementa selezione manuale obbligatoria per file e sorgenti non-DB - Migliora UI con suggerimenti intelligenti e feedback visivo - Aggiunge validazione multi-livello (UI, pre-transfer, runtime) - Introduce metodo GetPrimaryKeyFieldAsync in IDatabaseManager - Modifica GenerateSourceKey per richiedere sempre campo specifico - Implementa controllo IsTransferButtonEnabled per validazione form Breaking changes: - La generazione automatica delle chiavi sorgente è stata rimossa - Il campo chiave sorgente è ora obbligatorio quando si usa il sistema associazioni Fixes: Risolve problema di discovery schema vuoto con selezione database --- CredentialManager/Data/CredentialDbContext.cs | 51 ++ .../20250628_AddRecordAssociations.cs | 73 ++ CredentialManager/Models/MappingModels.cs | 0 CredentialManager/Models/RecordAssociation.cs | 75 ++ .../Services/DatabaseInitializer.cs | 140 +++- .../Services/IRecordAssociationService.cs | 54 ++ .../Services/KeyMappingService.cs | 0 .../Services/RecordAssociationService.cs | 250 ++++++ .../IDataConnectionCredentialService.cs | 10 + .../ServiceCollectionExtensions.cs | 3 + .../DataConnectionCredentialService.cs | 47 ++ DataConnection/DB/EF/EFCoreDatabaseManager.cs | 192 ++++- .../SqlServerSchemaProvider.cs | 27 +- .../DB/Interfaces/IDatabaseManager.cs | 23 +- .../Implementations/BaseRestServiceClient.cs | 30 + .../SalesforceServiceClient.cs | 248 +++++- .../REST/Interfaces/IRestServiceClient.cs | 39 +- Data_Coupler/Pages/DataCoupler.razor | 784 ++++++++++++++++-- Data_Coupler/Pages/RecordAssociations.razor | 535 ++++++++++++ Data_Coupler/Pages/_Host.cshtml | 2 + Data_Coupler/Program.cs | 2 +- Data_Coupler/Shared/NavMenu.razor | 5 + Data_Coupler/wwwroot/data/credentials.db | Bin 45056 -> 77824 bytes Data_Coupler/wwwroot/data/credentials.db-wal | Bin 0 -> 1586232 bytes Data_Coupler/wwwroot/js/site.js | 10 + GESTIONE_CHIAVE_SORGENTE.md | 130 +++ TEST_CHANGES.md | 53 ++ TestDatabaseFix/Program.cs | 50 ++ TestDatabaseFix/TestDatabaseFix.csproj | 19 + 29 files changed, 2748 insertions(+), 104 deletions(-) create mode 100644 CredentialManager/Migrations/20250628_AddRecordAssociations.cs create mode 100644 CredentialManager/Models/MappingModels.cs create mode 100644 CredentialManager/Models/RecordAssociation.cs create mode 100644 CredentialManager/Services/IRecordAssociationService.cs create mode 100644 CredentialManager/Services/KeyMappingService.cs create mode 100644 CredentialManager/Services/RecordAssociationService.cs create mode 100644 Data_Coupler/Pages/RecordAssociations.razor create mode 100644 Data_Coupler/wwwroot/js/site.js create mode 100644 GESTIONE_CHIAVE_SORGENTE.md create mode 100644 TEST_CHANGES.md create mode 100644 TestDatabaseFix/Program.cs create mode 100644 TestDatabaseFix/TestDatabaseFix.csproj diff --git a/CredentialManager/Data/CredentialDbContext.cs b/CredentialManager/Data/CredentialDbContext.cs index fea8c6d..df51b73 100644 --- a/CredentialManager/Data/CredentialDbContext.cs +++ b/CredentialManager/Data/CredentialDbContext.cs @@ -9,6 +9,7 @@ namespace CredentialManager.Data; public class CredentialDbContext : DbContext { public DbSet Credentials { get; set; } + public DbSet RecordAssociations { get; set; } public CredentialDbContext(DbContextOptions options) : base(options) { @@ -84,5 +85,55 @@ public class CredentialDbContext : DbContext entity.HasIndex(e => e.IsActive); }); + + // Configurazione della tabella RecordAssociations + modelBuilder.Entity(entity => + { + entity.ToTable("RecordAssociations"); + + entity.HasKey(e => e.Id); + + entity.Property(e => e.SourceName) + .IsRequired() + .HasMaxLength(200); + + entity.Property(e => e.SourceType) + .IsRequired() + .HasMaxLength(50); + + entity.Property(e => e.SourceKey) + .IsRequired() + .HasMaxLength(500); + + entity.Property(e => e.DestinationEntity) + .IsRequired() + .HasMaxLength(200); + + entity.Property(e => e.DestinationId) + .IsRequired() + .HasMaxLength(200); + + entity.Property(e => e.RestCredentialName) + .IsRequired() + .HasMaxLength(100); + + entity.Property(e => e.AdditionalInfo) + .HasMaxLength(2000); + + // Valori di default + entity.Property(e => e.IsActive) + .HasDefaultValue(true); + + // Indici + entity.HasIndex(e => new { e.SourceName, e.SourceKey, e.DestinationEntity }) + .IsUnique() + .HasDatabaseName("IX_RecordAssociations_Unique"); + + entity.HasIndex(e => e.SourceType); + entity.HasIndex(e => e.DestinationEntity); + entity.HasIndex(e => e.RestCredentialName); + entity.HasIndex(e => e.IsActive); + entity.HasIndex(e => e.CreatedAt); + }); } } diff --git a/CredentialManager/Migrations/20250628_AddRecordAssociations.cs b/CredentialManager/Migrations/20250628_AddRecordAssociations.cs new file mode 100644 index 0000000..eedf7dc --- /dev/null +++ b/CredentialManager/Migrations/20250628_AddRecordAssociations.cs @@ -0,0 +1,73 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace CredentialManager.Migrations +{ + /// + /// Aggiunge la tabella RecordAssociations per tracciare le associazioni tra record sorgente e destinazione + /// + public partial class AddRecordAssociations : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "RecordAssociations", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + SourceName = table.Column(type: "TEXT", maxLength: 200, nullable: false), + SourceType = table.Column(type: "TEXT", maxLength: 50, nullable: false), + SourceKey = table.Column(type: "TEXT", maxLength: 500, nullable: false), + DestinationEntity = table.Column(type: "TEXT", maxLength: 200, nullable: false), + DestinationId = table.Column(type: "TEXT", maxLength: 200, nullable: false), + RestCredentialName = table.Column(type: "TEXT", maxLength: 100, nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false, defaultValueSql: "datetime('now')"), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + IsActive = table.Column(type: "INTEGER", nullable: false, defaultValue: true), + AdditionalInfo = table.Column(type: "TEXT", maxLength: 2000, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RecordAssociations", x => x.Id); + }); + + // Indici per migliorare le performance + migrationBuilder.CreateIndex( + name: "IX_RecordAssociations_Unique", + table: "RecordAssociations", + columns: new[] { "SourceName", "SourceKey", "DestinationEntity" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_RecordAssociations_SourceType", + table: "RecordAssociations", + column: "SourceType"); + + migrationBuilder.CreateIndex( + name: "IX_RecordAssociations_DestinationEntity", + table: "RecordAssociations", + column: "DestinationEntity"); + + migrationBuilder.CreateIndex( + name: "IX_RecordAssociations_RestCredentialName", + table: "RecordAssociations", + column: "RestCredentialName"); + + migrationBuilder.CreateIndex( + name: "IX_RecordAssociations_IsActive", + table: "RecordAssociations", + column: "IsActive"); + + migrationBuilder.CreateIndex( + name: "IX_RecordAssociations_CreatedAt", + table: "RecordAssociations", + column: "CreatedAt"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RecordAssociations"); + } + } +} diff --git a/CredentialManager/Models/MappingModels.cs b/CredentialManager/Models/MappingModels.cs new file mode 100644 index 0000000..e69de29 diff --git a/CredentialManager/Models/RecordAssociation.cs b/CredentialManager/Models/RecordAssociation.cs new file mode 100644 index 0000000..3d9b4fa --- /dev/null +++ b/CredentialManager/Models/RecordAssociation.cs @@ -0,0 +1,75 @@ +using System.ComponentModel.DataAnnotations; + +namespace CredentialManager.Models; + +/// +/// Entità per memorizzare le associazioni tra record sorgente e destinazione +/// +public class RecordAssociation +{ + [Key] + public int Id { get; set; } + + /// + /// Nome della sorgente dati (nome tabella/file/foglio) + /// + [Required] + [MaxLength(200)] + public string SourceName { get; set; } = string.Empty; + + /// + /// Tipo di sorgente (database, file) + /// + [Required] + [MaxLength(50)] + public string SourceType { get; set; } = string.Empty; + + /// + /// Chiave del record sorgente (può essere un ID o una combinazione di campi) + /// + [Required] + [MaxLength(500)] + public string SourceKey { get; set; } = string.Empty; + + /// + /// Nome dell'entità di destinazione + /// + [Required] + [MaxLength(200)] + public string DestinationEntity { get; set; } = string.Empty; + + /// + /// ID del record di destinazione + /// + [Required] + [MaxLength(200)] + public string DestinationId { get; set; } = string.Empty; + + /// + /// Nome della credenziale REST utilizzata + /// + [Required] + [MaxLength(100)] + public string RestCredentialName { get; set; } = string.Empty; + + /// + /// Data e ora della creazione dell'associazione + /// + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + /// + /// Data e ora dell'ultimo aggiornamento + /// + public DateTime? UpdatedAt { get; set; } + + /// + /// Indica se l'associazione è ancora attiva + /// + public bool IsActive { get; set; } = true; + + /// + /// Informazioni aggiuntive in formato JSON + /// + [MaxLength(2000)] + public string? AdditionalInfo { get; set; } +} diff --git a/CredentialManager/Services/DatabaseInitializer.cs b/CredentialManager/Services/DatabaseInitializer.cs index 950b57e..a656cb5 100644 --- a/CredentialManager/Services/DatabaseInitializer.cs +++ b/CredentialManager/Services/DatabaseInitializer.cs @@ -64,15 +64,27 @@ public class DatabaseInitializer : IDatabaseInitializer { try { - // Prova a fare una query semplice per verificare che la tabella esista + // Verifica che la tabella principale Credentials esista await _context.Credentials.CountAsync(); - _logger.LogInformation("Verifica tabelle completata con successo"); - } - catch (Exception ex) - { - _logger.LogWarning("Tabelle mancanti, ricreazione database..."); + _logger.LogInformation("Tabella Credentials verificata con successo"); - // Se le tabelle non esistono, le ricreiamo + // Verifica se la tabella RecordAssociations esiste, se non esiste la crea senza ricreare tutto il database + try + { + await _context.RecordAssociations.CountAsync(); + _logger.LogInformation("Tabella RecordAssociations verificata con successo"); + } + catch (Exception) + { + _logger.LogInformation("Tabella RecordAssociations non trovata, creazione tramite migrazione..."); + await CreateRecordAssociationsTableAsync(); + } + } + catch (Exception) + { + _logger.LogWarning("Tabella Credentials mancante, ricreazione database..."); + + // Solo se la tabella principale non esiste, ricreiamo tutto await _context.Database.EnsureDeletedAsync(); await _context.Database.EnsureCreatedAsync(); await SeedInitialDataAsync(); @@ -142,7 +154,7 @@ public class DatabaseInitializer : IDatabaseInitializer { _logger.LogInformation("Verifica e applicazione migrazioni..."); - // Verifica se la colonna RestServiceType esiste usando una query diretta + // Migrazione 1: Verifica se la colonna RestServiceType esiste try { await _context.Database.ExecuteSqlRawAsync( @@ -157,6 +169,62 @@ public class DatabaseInitializer : IDatabaseInitializer "ALTER TABLE Credentials ADD COLUMN RestServiceType TEXT"); _logger.LogInformation("Colonna RestServiceType aggiunta con successo"); } + + // Migrazione 2: Verifica se la tabella RecordAssociations esiste + try + { + await _context.Database.ExecuteSqlRawAsync( + "SELECT COUNT(*) FROM RecordAssociations LIMIT 1"); + _logger.LogInformation("Tabella RecordAssociations già presente"); + } + catch (Microsoft.Data.Sqlite.SqliteException) + { + // La tabella non esiste, la creiamo + _logger.LogInformation("Creazione tabella RecordAssociations..."); + + // Crea la tabella + await _context.Database.ExecuteSqlRawAsync(@" + CREATE TABLE RecordAssociations ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + SourceName TEXT NOT NULL, + SourceType TEXT NOT NULL, + SourceKey TEXT NOT NULL, + DestinationEntity TEXT NOT NULL, + DestinationId TEXT NOT NULL, + RestCredentialName TEXT NOT NULL, + CreatedAt TEXT NOT NULL DEFAULT (datetime('now')), + UpdatedAt TEXT, + IsActive INTEGER NOT NULL DEFAULT 1, + AdditionalInfo TEXT + )"); + + // Crea gli indici + await _context.Database.ExecuteSqlRawAsync(@" + CREATE UNIQUE INDEX IX_RecordAssociations_Unique + ON RecordAssociations (SourceName, SourceKey, DestinationEntity)"); + + await _context.Database.ExecuteSqlRawAsync(@" + CREATE INDEX IX_RecordAssociations_SourceType + ON RecordAssociations (SourceType)"); + + await _context.Database.ExecuteSqlRawAsync(@" + CREATE INDEX IX_RecordAssociations_DestinationEntity + ON RecordAssociations (DestinationEntity)"); + + await _context.Database.ExecuteSqlRawAsync(@" + CREATE INDEX IX_RecordAssociations_RestCredentialName + ON RecordAssociations (RestCredentialName)"); + + await _context.Database.ExecuteSqlRawAsync(@" + CREATE INDEX IX_RecordAssociations_IsActive + ON RecordAssociations (IsActive)"); + + await _context.Database.ExecuteSqlRawAsync(@" + CREATE INDEX IX_RecordAssociations_CreatedAt + ON RecordAssociations (CreatedAt)"); + + _logger.LogInformation("Tabella RecordAssociations creata con successo"); + } } catch (Exception ex) { @@ -164,4 +232,60 @@ public class DatabaseInitializer : IDatabaseInitializer throw; } } + + private async Task CreateRecordAssociationsTableAsync() + { + try + { + _logger.LogInformation("Creazione tabella RecordAssociations..."); + + // Crea la tabella + await _context.Database.ExecuteSqlRawAsync(@" + CREATE TABLE RecordAssociations ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + SourceName TEXT NOT NULL, + SourceType TEXT NOT NULL, + SourceKey TEXT NOT NULL, + DestinationEntity TEXT NOT NULL, + DestinationId TEXT NOT NULL, + RestCredentialName TEXT NOT NULL, + CreatedAt TEXT NOT NULL DEFAULT (datetime('now')), + UpdatedAt TEXT, + IsActive INTEGER NOT NULL DEFAULT 1, + AdditionalInfo TEXT + )"); + + // Crea gli indici + await _context.Database.ExecuteSqlRawAsync(@" + CREATE UNIQUE INDEX IX_RecordAssociations_Unique + ON RecordAssociations (SourceName, SourceKey, DestinationEntity)"); + + await _context.Database.ExecuteSqlRawAsync(@" + CREATE INDEX IX_RecordAssociations_SourceType + ON RecordAssociations (SourceType)"); + + await _context.Database.ExecuteSqlRawAsync(@" + CREATE INDEX IX_RecordAssociations_DestinationEntity + ON RecordAssociations (DestinationEntity)"); + + await _context.Database.ExecuteSqlRawAsync(@" + CREATE INDEX IX_RecordAssociations_RestCredentialName + ON RecordAssociations (RestCredentialName)"); + + await _context.Database.ExecuteSqlRawAsync(@" + CREATE INDEX IX_RecordAssociations_IsActive + ON RecordAssociations (IsActive)"); + + await _context.Database.ExecuteSqlRawAsync(@" + CREATE INDEX IX_RecordAssociations_CreatedAt + ON RecordAssociations (CreatedAt)"); + + _logger.LogInformation("Tabella RecordAssociations creata con successo"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Errore nella creazione della tabella RecordAssociations"); + throw; + } + } } diff --git a/CredentialManager/Services/IRecordAssociationService.cs b/CredentialManager/Services/IRecordAssociationService.cs new file mode 100644 index 0000000..c9d8792 --- /dev/null +++ b/CredentialManager/Services/IRecordAssociationService.cs @@ -0,0 +1,54 @@ +using CredentialManager.Models; + +namespace CredentialManager.Services; + +/// +/// Interfaccia per il servizio di gestione delle associazioni record +/// +public interface IRecordAssociationService +{ + /// + /// Salva una nuova associazione tra record sorgente e destinazione + /// + Task SaveAssociationAsync(RecordAssociation association); + + /// + /// Cerca un'associazione esistente tramite chiave sorgente + /// + Task FindAssociationAsync(string sourceName, string sourceKey, string destinationEntity); + + /// + /// Ottiene tutte le associazioni per una sorgente specifica + /// + Task> GetAssociationsBySourceAsync(string sourceName, string sourceType); + + /// + /// Ottiene tutte le associazioni per un'entità di destinazione specifica + /// + Task> GetAssociationsByDestinationAsync(string destinationEntity, string restCredentialName); + + /// + /// Ottiene tutte le associazioni attive + /// + Task> GetAllActiveAssociationsAsync(); + + /// + /// Aggiorna un'associazione esistente + /// + Task UpdateAssociationAsync(RecordAssociation association); + + /// + /// Disattiva un'associazione (soft delete) + /// + Task DeactivateAssociationAsync(int id); + + /// + /// Elimina definitivamente un'associazione + /// + Task DeleteAssociationAsync(int id); + + /// + /// Pulisce le associazioni obsolete (opzionale) + /// + Task CleanupOldAssociationsAsync(TimeSpan olderThan); +} diff --git a/CredentialManager/Services/KeyMappingService.cs b/CredentialManager/Services/KeyMappingService.cs new file mode 100644 index 0000000..e69de29 diff --git a/CredentialManager/Services/RecordAssociationService.cs b/CredentialManager/Services/RecordAssociationService.cs new file mode 100644 index 0000000..dd91e4e --- /dev/null +++ b/CredentialManager/Services/RecordAssociationService.cs @@ -0,0 +1,250 @@ +using CredentialManager.Data; +using CredentialManager.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace CredentialManager.Services; + +/// +/// Servizio per la gestione delle associazioni tra record sorgente e destinazione +/// +public class RecordAssociationService : IRecordAssociationService +{ + private readonly CredentialDbContext _context; + private readonly ILogger _logger; + + public RecordAssociationService( + CredentialDbContext context, + ILogger logger) + { + _context = context; + _logger = logger; + } + + public async Task SaveAssociationAsync(RecordAssociation association) + { + try + { + // Controlla se esiste già un'associazione per questa combinazione + var existing = await _context.RecordAssociations + .FirstOrDefaultAsync(ra => + ra.SourceName == association.SourceName && + ra.SourceKey == association.SourceKey && + ra.DestinationEntity == association.DestinationEntity && + ra.IsActive); + + if (existing != null) + { + // Aggiorna l'associazione esistente + existing.DestinationId = association.DestinationId; + existing.RestCredentialName = association.RestCredentialName; + existing.UpdatedAt = DateTime.UtcNow; + existing.AdditionalInfo = association.AdditionalInfo; + + _context.RecordAssociations.Update(existing); + await _context.SaveChangesAsync(); + + _logger.LogInformation("Associazione aggiornata: {SourceName}/{SourceKey} -> {DestinationEntity}/{DestinationId}", + association.SourceName, association.SourceKey, association.DestinationEntity, association.DestinationId); + + return existing.Id; + } + else + { + // Crea nuova associazione + _context.RecordAssociations.Add(association); + await _context.SaveChangesAsync(); + + _logger.LogInformation("Nuova associazione creata: {SourceName}/{SourceKey} -> {DestinationEntity}/{DestinationId}", + association.SourceName, association.SourceKey, association.DestinationEntity, association.DestinationId); + + return association.Id; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Errore nel salvare l'associazione: {SourceName}/{SourceKey} -> {DestinationEntity}", + association.SourceName, association.SourceKey, association.DestinationEntity); + throw; + } + } + + public async Task FindAssociationAsync(string sourceName, string sourceKey, string destinationEntity) + { + try + { + return await _context.RecordAssociations + .FirstOrDefaultAsync(ra => + ra.SourceName == sourceName && + ra.SourceKey == sourceKey && + ra.DestinationEntity == destinationEntity && + ra.IsActive); + } + catch (Exception ex) + { + _logger.LogError(ex, "Errore nella ricerca dell'associazione: {SourceName}/{SourceKey} -> {DestinationEntity}", + sourceName, sourceKey, destinationEntity); + throw; + } + } + + public async Task> GetAssociationsBySourceAsync(string sourceName, string sourceType) + { + try + { + return await _context.RecordAssociations + .Where(ra => ra.SourceName == sourceName && ra.SourceType == sourceType && ra.IsActive) + .OrderByDescending(ra => ra.CreatedAt) + .ToListAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Errore nel recupero delle associazioni per sorgente: {SourceName} ({SourceType})", + sourceName, sourceType); + throw; + } + } + + public async Task> GetAssociationsByDestinationAsync(string destinationEntity, string restCredentialName) + { + try + { + return await _context.RecordAssociations + .Where(ra => ra.DestinationEntity == destinationEntity && + ra.RestCredentialName == restCredentialName && + ra.IsActive) + .OrderByDescending(ra => ra.CreatedAt) + .ToListAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Errore nel recupero delle associazioni per destinazione: {DestinationEntity} ({RestCredentialName})", + destinationEntity, restCredentialName); + throw; + } + } + + public async Task> GetAllActiveAssociationsAsync() + { + try + { + return await _context.RecordAssociations + .Where(ra => ra.IsActive) + .OrderByDescending(ra => ra.CreatedAt) + .ToListAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Errore nel recupero di tutte le associazioni attive"); + throw; + } + } + + public async Task UpdateAssociationAsync(RecordAssociation association) + { + try + { + var existing = await _context.RecordAssociations.FindAsync(association.Id); + if (existing == null) + { + _logger.LogWarning("Associazione con ID {Id} non trovata per l'aggiornamento", association.Id); + return false; + } + + existing.DestinationId = association.DestinationId; + existing.RestCredentialName = association.RestCredentialName; + existing.UpdatedAt = DateTime.UtcNow; + existing.AdditionalInfo = association.AdditionalInfo; + existing.IsActive = association.IsActive; + + _context.RecordAssociations.Update(existing); + await _context.SaveChangesAsync(); + + _logger.LogInformation("Associazione aggiornata: ID {Id}", association.Id); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Errore nell'aggiornamento dell'associazione: ID {Id}", association.Id); + throw; + } + } + + public async Task DeactivateAssociationAsync(int id) + { + try + { + var association = await _context.RecordAssociations.FindAsync(id); + if (association == null) + { + _logger.LogWarning("Associazione con ID {Id} non trovata per la disattivazione", id); + return false; + } + + association.IsActive = false; + association.UpdatedAt = DateTime.UtcNow; + + _context.RecordAssociations.Update(association); + await _context.SaveChangesAsync(); + + _logger.LogInformation("Associazione disattivata: ID {Id}", id); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Errore nella disattivazione dell'associazione: ID {Id}", id); + throw; + } + } + + public async Task DeleteAssociationAsync(int id) + { + try + { + var association = await _context.RecordAssociations.FindAsync(id); + if (association == null) + { + _logger.LogWarning("Associazione con ID {Id} non trovata per l'eliminazione", id); + return false; + } + + _context.RecordAssociations.Remove(association); + await _context.SaveChangesAsync(); + + _logger.LogInformation("Associazione eliminata: ID {Id}", id); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Errore nell'eliminazione dell'associazione: ID {Id}", id); + throw; + } + } + + public async Task CleanupOldAssociationsAsync(TimeSpan olderThan) + { + try + { + var cutoffDate = DateTime.UtcNow - olderThan; + var oldAssociations = await _context.RecordAssociations + .Where(ra => ra.CreatedAt < cutoffDate && !ra.IsActive) + .ToListAsync(); + + if (oldAssociations.Any()) + { + _context.RecordAssociations.RemoveRange(oldAssociations); + await _context.SaveChangesAsync(); + + _logger.LogInformation("Pulite {Count} associazioni obsolete più vecchie di {Cutoff}", + oldAssociations.Count, cutoffDate); + } + + return oldAssociations.Count; + } + catch (Exception ex) + { + _logger.LogError(ex, "Errore nella pulizia delle associazioni obsolete"); + throw; + } + } +} diff --git a/DataConnection/CredentialManagement/Interfaces/IDataConnectionCredentialService.cs b/DataConnection/CredentialManagement/Interfaces/IDataConnectionCredentialService.cs index 87e2647..9f56df7 100644 --- a/DataConnection/CredentialManagement/Interfaces/IDataConnectionCredentialService.cs +++ b/DataConnection/CredentialManagement/Interfaces/IDataConnectionCredentialService.cs @@ -56,4 +56,14 @@ public interface IDataConnectionCredentialService Task<(bool Success, string Message)> TestSapB1ConnectionAsync(SapB1ServiceLayerCredential credential); Task<(bool Success, string Message)> TestSalesforceConnectionAsync(string credentialName); Task<(bool Success, string Message)> TestSalesforceConnectionAsync(SalesforceCredential credential); + + // Record associations + Task SaveRecordAssociationAsync(RecordAssociation association); + Task FindRecordAssociationAsync(string sourceName, string sourceKey, string destinationEntity); + Task> GetRecordAssociationsBySourceAsync(string sourceName, string sourceType); + Task> GetRecordAssociationsByDestinationAsync(string destinationEntity, string restCredentialName); + Task> GetAllActiveRecordAssociationsAsync(); + Task UpdateRecordAssociationAsync(RecordAssociation association); + Task DeactivateRecordAssociationAsync(int id); + Task DeleteRecordAssociationAsync(int id); } diff --git a/DataConnection/CredentialManagement/ServiceCollectionExtensions.cs b/DataConnection/CredentialManagement/ServiceCollectionExtensions.cs index f823dab..1d1e6a9 100644 --- a/DataConnection/CredentialManagement/ServiceCollectionExtensions.cs +++ b/DataConnection/CredentialManagement/ServiceCollectionExtensions.cs @@ -38,6 +38,9 @@ public static class ServiceCollectionExtensions // Aggiungi i servizi base di CredentialManager services.AddCredentialManager(databasePath); + // Aggiungi il servizio di gestione associazioni record + services.AddScoped(); + // Aggiungi il servizio di integrazione DataConnection services.AddScoped(); diff --git a/DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs b/DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs index 0c909f0..1a66638 100644 --- a/DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs +++ b/DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs @@ -15,13 +15,16 @@ namespace DataConnection.CredentialManagement.Services; public class DataConnectionCredentialService : IDataConnectionCredentialService { private readonly ICredentialService _credentialService; + private readonly IRecordAssociationService _recordAssociationService; private readonly ILogger _logger; public DataConnectionCredentialService( ICredentialService credentialService, + IRecordAssociationService recordAssociationService, ILogger logger) { _credentialService = credentialService; + _recordAssociationService = recordAssociationService; _logger = logger; } @@ -855,4 +858,48 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService } #endregion + + #region Record Associations + + public async Task SaveRecordAssociationAsync(RecordAssociation association) + { + return await _recordAssociationService.SaveAssociationAsync(association); + } + + public async Task FindRecordAssociationAsync(string sourceName, string sourceKey, string destinationEntity) + { + return await _recordAssociationService.FindAssociationAsync(sourceName, sourceKey, destinationEntity); + } + + public async Task> GetRecordAssociationsBySourceAsync(string sourceName, string sourceType) + { + return await _recordAssociationService.GetAssociationsBySourceAsync(sourceName, sourceType); + } + + public async Task> GetRecordAssociationsByDestinationAsync(string destinationEntity, string restCredentialName) + { + return await _recordAssociationService.GetAssociationsByDestinationAsync(destinationEntity, restCredentialName); + } + + public async Task> GetAllActiveRecordAssociationsAsync() + { + return await _recordAssociationService.GetAllActiveAssociationsAsync(); + } + + public async Task UpdateRecordAssociationAsync(RecordAssociation association) + { + return await _recordAssociationService.UpdateAssociationAsync(association); + } + + public async Task DeactivateRecordAssociationAsync(int id) + { + return await _recordAssociationService.DeactivateAssociationAsync(id); + } + + public async Task DeleteRecordAssociationAsync(int id) + { + return await _recordAssociationService.DeleteAssociationAsync(id); + } + + #endregion } diff --git a/DataConnection/DB/EF/EFCoreDatabaseManager.cs b/DataConnection/DB/EF/EFCoreDatabaseManager.cs index 6a54388..be66f8b 100644 --- a/DataConnection/DB/EF/EFCoreDatabaseManager.cs +++ b/DataConnection/DB/EF/EFCoreDatabaseManager.cs @@ -109,35 +109,26 @@ public class EFCoreDatabaseManager : IDatabaseManager { try { - Console.WriteLine($"[DEBUG] Iniziando GetDatabaseSchemaAsync - DatabaseType: {_options.DatabaseType}"); - // Assicurarsi che il contesto sia connesso await _context.Database.OpenConnectionAsync(); - Console.WriteLine($"[DEBUG] Connessione al database aperta. Connection string: {_context.Database.GetConnectionString()}"); // Usa la factory per ottenere il provider appropriato in base al tipo di database var schemaProvider = DatabaseSchemaProviderFactory.CreateProvider(_options.DatabaseType); - Console.WriteLine($"[DEBUG] Schema provider creato: {schemaProvider.GetType().Name}"); // Usa il provider per ottenere lo schema - var result = await schemaProvider.GetDatabaseSchemaAsync(_context.Database.GetConnectionString()); - Console.WriteLine($"[DEBUG] Schema ottenuto. Numero tabelle: {result?.Count ?? 0}"); - - if (result != null && result.Count > 0) - { - foreach (var table in result.Take(3)) - { - Console.WriteLine($"[DEBUG] Tabella: {table.Key}, Colonne: {table.Value?.Count() ?? 0}"); - } - } + var connectionString = _context.Database.GetConnectionString(); + if (connectionString == null) + throw new InvalidOperationException("Connection string is null"); + + var result = await schemaProvider.GetDatabaseSchemaAsync(connectionString); return result; } catch (Exception ex) { Console.WriteLine($"Errore nel recupero dello schema del database: {ex.Message}"); - Console.WriteLine($"[DEBUG] Stack trace: {ex.StackTrace}"); - throw; } + throw; + } } public async Task>> GetAllRecordsAsync(string tableName) { try @@ -146,7 +137,8 @@ public class EFCoreDatabaseManager : IDatabaseManager // Usa la stessa connection string utilizzata per il discovery dello schema var connectionString = _context.Database.GetConnectionString(); - Console.WriteLine($"[DEBUG] GetAllRecordsAsync - Using connection string: {connectionString?.Substring(0, Math.Min(50, connectionString?.Length ?? 0))}..."); + if (connectionString == null) + throw new InvalidOperationException("Connection string is null"); // Determina il tipo di connessione in base al DatabaseType using var connection = CreateConnection(connectionString); @@ -171,7 +163,6 @@ public class EFCoreDatabaseManager : IDatabaseManager } command.CommandText = $"SELECT TOP 1000 * FROM {tableReference}"; - Console.WriteLine($"[DEBUG] GetAllRecordsAsync - Query: {command.CommandText}"); using var reader = await command.ExecuteReaderAsync(); @@ -183,13 +174,12 @@ public class EFCoreDatabaseManager : IDatabaseManager { var columnName = reader.GetName(i); var value = reader.IsDBNull(i) ? null : reader.GetValue(i); - record[columnName] = value; + record[columnName] = value!; } records.Add(record); } - Console.WriteLine($"[DEBUG] GetAllRecordsAsync - Tabella: {tableName}, Record ottenuti: {records.Count}"); return records; } catch (Exception ex) @@ -199,6 +189,114 @@ public class EFCoreDatabaseManager : IDatabaseManager } } + public async Task> GetAvailableDatabasesAsync() + { + try + { + var connectionString = _context.Database.GetConnectionString(); + if (connectionString == null) + throw new InvalidOperationException("Connection string is null"); + + // Crea una connessione al server (senza specificare il database) + var serverConnectionString = GetServerConnectionString(connectionString); + + using var connection = CreateConnection(serverConnectionString); + await connection.OpenAsync(); + + using var command = connection.CreateCommand(); + + // Query per ottenere i database disponibili (esclude quelli di sistema) + command.CommandText = @" + SELECT name + FROM sys.databases + WHERE state_desc = 'ONLINE' + AND name NOT IN ('master', 'tempdb', 'model', 'msdb', 'distribution') + ORDER BY name"; + + var databases = new List(); + using var reader = await command.ExecuteReaderAsync(); + + while (await reader.ReadAsync()) + { + databases.Add(reader.GetString(0)); + } + + return databases; + } + catch (Exception ex) + { + Console.WriteLine($"Errore nell'ottenere la lista dei database: {ex.Message}"); + throw; + } + } + + public async Task ChangeDatabaseAsync(string databaseName) + { + try + { + var currentConnectionString = _context.Database.GetConnectionString(); + if (currentConnectionString == null) + throw new InvalidOperationException("Connection string is null"); + + // Crea una nuova connection string con il database specificato + var newConnectionString = UpdateConnectionStringDatabase(currentConnectionString, databaseName); + + // Ricrea il contesto con la nuova connection string + var optionsBuilder = new DbContextOptionsBuilder(); + + switch (_options.DatabaseType) + { + case Enums.DatabaseType.SqlServer: + optionsBuilder.UseSqlServer(newConnectionString, options => + { + if (_options.CommandTimeout > 0) + options.CommandTimeout(_options.CommandTimeout); + }); + break; + default: + throw new NotSupportedException($"Database type {_options.DatabaseType} is not supported"); + } + + // Disponi il vecchio contesto e crea quello nuovo + _context.Dispose(); + _context = new ExistingDatabaseContext( + optionsBuilder.Options, + _options.ModelConfigurator, + _options.EnableAutoDiscovery, + _options.EntityAssembly, + _options.EntityNamespace, + _options.NamingStrategy); + + // Testa la connessione al nuovo database + await _context.Database.OpenConnectionAsync(); + } + catch (Exception ex) + { + Console.WriteLine($"Errore nel cambio database a '{databaseName}': {ex.Message}"); + throw; + } + } + + /// + /// Estrae la connection string del server (senza database specifico) da una connection string completa + /// + private string GetServerConnectionString(string connectionString) + { + var builder = new SqlConnectionStringBuilder(connectionString); + builder.InitialCatalog = ""; // Rimuove il database specifico + return builder.ConnectionString; + } + + /// + /// Aggiorna la connection string con un nuovo database + /// + private string UpdateConnectionStringDatabase(string connectionString, string databaseName) + { + var builder = new SqlConnectionStringBuilder(connectionString); + builder.InitialCatalog = databaseName; + return builder.ConnectionString; + } + /// /// Crea una connessione database appropriata in base al tipo di database /// @@ -222,4 +320,58 @@ public class EFCoreDatabaseManager : IDatabaseManager { _context?.Dispose(); } + + public async Task GetPrimaryKeyFieldAsync(string tableName) + { + try + { + var connectionString = _context.Database.GetConnectionString(); + if (connectionString == null) + throw new InvalidOperationException("Connection string is null"); + + using var connection = CreateConnection(connectionString); + await connection.OpenAsync(); + + using var command = connection.CreateCommand(); + + // Query per ottenere la Primary Key della tabella + // Gestisce anche tabelle con schema (es. "dbo.TableName") + string schemaName = "dbo"; // Default schema + string tableNameOnly = tableName; + + if (tableName.Contains('.')) + { + var parts = tableName.Split('.'); + schemaName = parts[0]; + tableNameOnly = parts[1]; + } + + command.CommandText = @" + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE + WHERE OBJECTPROPERTY(OBJECT_ID(CONSTRAINT_SCHEMA + '.' + QUOTENAME(CONSTRAINT_NAME)), 'IsPrimaryKey') = 1 + AND TABLE_SCHEMA = @schemaName + AND TABLE_NAME = @tableName + ORDER BY ORDINAL_POSITION"; + + // Usa parametri per evitare SQL injection + var schemaParam = command.CreateParameter(); + schemaParam.ParameterName = "@schemaName"; + schemaParam.Value = schemaName; + command.Parameters.Add(schemaParam); + + var tableParam = command.CreateParameter(); + tableParam.ParameterName = "@tableName"; + tableParam.Value = tableNameOnly; + command.Parameters.Add(tableParam); + + var result = await command.ExecuteScalarAsync(); + return result?.ToString(); + } + catch (Exception ex) + { + Console.WriteLine($"Errore nel recupero della Primary Key per la tabella {tableName}: {ex.Message}"); + return null; + } + } } diff --git a/DataConnection/DB/EF/SchemaProviders/SqlServerSchemaProvider.cs b/DataConnection/DB/EF/SchemaProviders/SqlServerSchemaProvider.cs index 6ade431..15e6edb 100644 --- a/DataConnection/DB/EF/SchemaProviders/SqlServerSchemaProvider.cs +++ b/DataConnection/DB/EF/SchemaProviders/SqlServerSchemaProvider.cs @@ -17,13 +17,24 @@ public class SqlServerSchemaProvider : IDatabaseSchemaProvider try { - Console.WriteLine($"[DEBUG] SqlServerSchemaProvider - Connection string: {connectionString?.Substring(0, Math.Min(50, connectionString?.Length ?? 0))}..."); - using (var connection = new SqlConnection(connectionString)) { await connection.OpenAsync(); - Console.WriteLine($"[DEBUG] SqlServerSchemaProvider - Connessione aperta"); + // Prima verifichiamo se ci sono tabelle utente con una query semplice + string testSql = "SELECT COUNT(*) FROM sys.tables WHERE is_ms_shipped = 0"; + using (var testCommand = new SqlCommand(testSql, connection)) + { + var scalarResult = await testCommand.ExecuteScalarAsync(); + var tableCount = scalarResult != null ? (int)scalarResult : 0; + + if (tableCount == 0) + { + return new Dictionary>(); // Restituisce dizionario vuoto + } + } + + // Se ci sono tabelle, procediamo con la query completa // Query per ottenere la struttura delle tabelle in SQL Server string sql = @" SELECT @@ -71,8 +82,8 @@ public class SqlServerSchemaProvider : IDatabaseSchemaProvider using (var reader = await command.ExecuteReaderAsync()) { - string currentTable = null; - List columns = null; + string? currentTable = null; + List? columns = null; while (await reader.ReadAsync()) { @@ -117,12 +128,6 @@ public class SqlServerSchemaProvider : IDatabaseSchemaProvider { result[currentTable] = columns; } - - Console.WriteLine($"[DEBUG] SqlServerSchemaProvider - Query completata. Trovate {result.Count} tabelle"); - foreach (var table in result.Take(3)) - { - Console.WriteLine($"[DEBUG] SqlServerSchemaProvider - Tabella: {table.Key}, Colonne: {table.Value?.Count() ?? 0}"); - } } } } diff --git a/DataConnection/DB/Interfaces/IDatabaseManager.cs b/DataConnection/DB/Interfaces/IDatabaseManager.cs index c5d96fe..de98d65 100644 --- a/DataConnection/DB/Interfaces/IDatabaseManager.cs +++ b/DataConnection/DB/Interfaces/IDatabaseManager.cs @@ -45,6 +45,16 @@ public interface IDatabaseManager : IDisposable /// Esegue un comando SQL che non restituisce risultati /// Task ExecuteCommandAsync(string sql, params object[] parameters); + + /// + /// Ottiene l'elenco dei database disponibili sul server + /// + Task> GetAvailableDatabasesAsync(); + + /// + /// Cambia il database corrente per la connessione + /// + Task ChangeDatabaseAsync(string databaseName); /// /// Ottiene i metadati delle tabelle nel database /// @@ -54,6 +64,11 @@ public interface IDatabaseManager : IDisposable /// Ottiene tutti i record da una tabella specifica come dizionari chiave-valore /// Task>> GetAllRecordsAsync(string tableName); + + /// + /// Ottiene il nome del campo Primary Key di una tabella specifica + /// + Task GetPrimaryKeyFieldAsync(string tableName); } /// @@ -61,11 +76,11 @@ public interface IDatabaseManager : IDisposable /// public class DbColumnInfo { - public string Name { get; set; } - public string DataType { get; set; } + public string Name { get; set; } = string.Empty; + public string DataType { get; set; } = string.Empty; public bool IsNullable { get; set; } public bool IsPrimaryKey { get; set; } public bool IsForeignKey { get; set; } - public string ReferencedTable { get; set; } - public string ReferencedColumn { get; set; } + public string? ReferencedTable { get; set; } + public string? ReferencedColumn { get; set; } } diff --git a/DataConnection/REST/Implementations/BaseRestServiceClient.cs b/DataConnection/REST/Implementations/BaseRestServiceClient.cs index a094483..1676ce1 100644 --- a/DataConnection/REST/Implementations/BaseRestServiceClient.cs +++ b/DataConnection/REST/Implementations/BaseRestServiceClient.cs @@ -135,6 +135,36 @@ namespace DataConnection.REST.Implementations return await CreateEntityAsync(entityName, entityData, cancellationToken); } + public virtual async Task>> FindEntitiesByKeysAsync(string entityName, Dictionary keyFields, CancellationToken cancellationToken = default) + { + // Default implementation - returns empty list + // Derived classes should override this method for service-specific entity search logic + await Task.CompletedTask; + return new List>(); + } + + public virtual async Task DeleteEntityAsync(string entityName, string entityId, CancellationToken cancellationToken = default) + { + // Default implementation - returns false (not supported) + // Derived classes should override this method for service-specific entity deletion logic + await Task.CompletedTask; + return false; + } public virtual async Task?> UpdateEntityAsync(string entityName, string entityId, Dictionary entityData, CancellationToken cancellationToken = default) + { + // Default implementation - returns null (not supported) + // Derived classes should override this method for service-specific entity update logic + await Task.CompletedTask; + return null; + } + + public virtual async Task>> FindEntitiesByRequiredFieldsAsync(string entityName, Dictionary requiredFields, CancellationToken cancellationToken = default) + { + // Default implementation - returns empty list (not supported) + // Derived classes should override this method for service-specific duplicate detection logic + await Task.CompletedTask; + return new List>(); + } + public virtual async Task AuthenticateAsync(CancellationToken cancellationToken = default) { // Default implementation for basic authentication (already handled in ConfigureHttpClient) diff --git a/DataConnection/REST/Implementations/SalesforceServiceClient.cs b/DataConnection/REST/Implementations/SalesforceServiceClient.cs index c507c64..d861df1 100644 --- a/DataConnection/REST/Implementations/SalesforceServiceClient.cs +++ b/DataConnection/REST/Implementations/SalesforceServiceClient.cs @@ -481,6 +481,176 @@ namespace DataConnection.REST.Implementations Console.WriteLine($"Error during Salesforce entity upsert: {ex.Message}"); return null; } + } /// + /// Finds entities by their key fields in Salesforce. + /// + /// The name of the SObject to search (e.g., "Account", "Contact"). + /// The key fields and their values to match. + /// Cancellation token. + /// A list of matching entities or an empty list if none found. + public override async Task>> FindEntitiesByKeysAsync(string entityName, Dictionary keyFields, CancellationToken cancellationToken = default) + { + try + { + Console.WriteLine($"--- Starting Salesforce Entity Search: {entityName} ---"); + Console.WriteLine($"Key Fields: {string.Join(", ", keyFields.Select(kvp => $"{kvp.Key}={kvp.Value}"))}"); + + if (!await EnsureAuthenticatedAsync(cancellationToken)) + { + Console.WriteLine("Authentication failed for entity search"); + return new List>(); + } + + // Costruisci la query SOQL + var whereConditions = keyFields.Select(kvp => + { + var value = kvp.Value?.ToString() ?? ""; + // Se il valore è una stringa, aggiungi le virgolette + if (kvp.Value is string) + { + value = $"'{value.Replace("'", "\\'")}'"; // Escape delle virgolette + } + return $"{kvp.Key} = {value}"; + }); + + var query = $"SELECT Id FROM {entityName} WHERE {string.Join(" AND ", whereConditions)}"; + Console.WriteLine($"SOQL Query: {query}"); + + var encodedQuery = Uri.EscapeDataString(query); + var queryEndpoint = $"/services/data/v59.0/query/?q={encodedQuery}"; var response = await GetAsync(queryEndpoint, cancellationToken); + + if (response?.Records != null) + { + var results = response.Records.Select(record => + record as Dictionary ?? new Dictionary() + ).ToList(); + + Console.WriteLine($"Found {results.Count} entities matching the key fields"); + return results; + } + + Console.WriteLine("No entities found matching the key fields"); + return new List>(); + } + catch (Exception ex) + { + Console.WriteLine($"Error during Salesforce entity search: {ex.Message}"); + return new List>(); + } + } /// + /// Deletes an entity in Salesforce by its ID. + /// + /// The name of the SObject (e.g., "Account", "Contact"). + /// The ID of the entity to delete. + /// Cancellation token. + /// True if deletion was successful, false otherwise. + public override async Task DeleteEntityAsync(string entityName, string entityId, CancellationToken cancellationToken = default) + { + try + { + Console.WriteLine($"--- Starting Salesforce Entity Delete: {entityName}/{entityId} ---"); + + if (!await EnsureAuthenticatedAsync(cancellationToken)) + { + Console.WriteLine("Authentication failed for entity deletion"); + return false; + } + + var deleteEndpoint = $"/services/data/v59.0/sobjects/{entityName}/{entityId}"; + + // Salesforce usa DELETE HTTP method per eliminare record + var request = new HttpRequestMessage(HttpMethod.Delete, $"{_instanceUrl}{deleteEndpoint}"); + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _accessToken); + + var response = await _httpClient.SendAsync(request, cancellationToken); + + if (response.IsSuccessStatusCode) + { + Console.WriteLine($"Entity {entityName}/{entityId} deleted successfully"); + return true; + } + else + { + var errorContent = await response.Content.ReadAsStringAsync(cancellationToken); + Console.WriteLine($"Failed to delete entity {entityName}/{entityId}. Status: {response.StatusCode}, Error: {errorContent}"); + return false; + } + } + catch (Exception ex) + { + Console.WriteLine($"Error during Salesforce entity deletion: {ex.Message}"); + return false; + } + } + + /// + /// Updates an existing entity in Salesforce by its ID. + /// + /// The name of the SObject (e.g., "Account", "Contact"). + /// The ID of the entity to update. + /// The data to update as key-value pairs. + /// Cancellation token. + /// The updated entity data or null if update failed. + public override async Task?> UpdateEntityAsync(string entityName, string entityId, Dictionary entityData, CancellationToken cancellationToken = default) + { + try + { + Console.WriteLine($"--- Starting Salesforce Entity Update: {entityName}/{entityId} ---"); + + if (!await EnsureAuthenticatedAsync(cancellationToken)) + { + Console.WriteLine("Authentication failed for entity update"); + return null; + } + + var updateEndpoint = $"/services/data/v59.0/sobjects/{entityName}/{entityId}"; + + // Salesforce usa PATCH HTTP method per aggiornare record + var request = new HttpRequestMessage(HttpMethod.Patch, $"{_instanceUrl}{updateEndpoint}"); + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _accessToken); + request.Content = JsonContent.Create(entityData); + + var response = await _httpClient.SendAsync(request, cancellationToken); + + if (response.IsSuccessStatusCode) + { + Console.WriteLine($"Entity {entityName}/{entityId} updated successfully"); + + // Ritorna i dati aggiornati includendo l'ID + var updatedData = new Dictionary(entityData) + { + ["Id"] = entityId + }; + return updatedData; + } + else + { + var errorContent = await response.Content.ReadAsStringAsync(cancellationToken); + Console.WriteLine($"Failed to update entity {entityName}/{entityId}. Status: {response.StatusCode}, Error: {errorContent}"); + return null; + } + } + catch (Exception ex) + { + Console.WriteLine($"Error during Salesforce entity update: {ex.Message}"); + return null; + } + } + + /// + /// Ensures the client is authenticated, attempting to authenticate if not already authenticated. + /// + /// Cancellation token. + /// True if authentication is successful, false otherwise. + private async Task EnsureAuthenticatedAsync(CancellationToken cancellationToken = default) + { + if (IsAuthenticated()) + { + return true; + } + + Console.WriteLine("Client not authenticated, attempting to authenticate..."); + return await AuthenticateAsync(cancellationToken); } // --- Nested classes for deserializing Salesforce responses --- @@ -541,7 +711,77 @@ namespace DataConnection.REST.Implementations public string Label { get; set; } = string.Empty; [JsonPropertyName("fields")] - public List Fields { get; set; } = new List(); + public List Fields { get; set; } = new List(); } /// + /// Finds entities by required fields to detect duplicates. + /// + /// The name of the entity to search. + /// The required fields and their values to search for. + /// Cancellation token. + /// A list of matching entities. + public override async Task>> FindEntitiesByRequiredFieldsAsync(string entityName, Dictionary requiredFields, CancellationToken cancellationToken = default) + { + try + { + Console.WriteLine($"--- Searching for duplicates in {entityName} by required fields ---"); + + if (!await EnsureAuthenticatedAsync(cancellationToken)) + { + Console.WriteLine("Authentication failed for required fields search"); + return new List>(); + } + + if (!requiredFields.Any()) + { + Console.WriteLine("No required fields provided for duplicate search"); + return new List>(); + } // Build WHERE clause with required fields + var whereConditions = new List(); + foreach (var field in requiredFields) + { + if (field.Value != null) + { + var value = field.Value.ToString(); + // Escape single quotes in string values + if (field.Value is string stringValue) + { + value = stringValue.Replace("'", "\\'"); + whereConditions.Add($"{field.Key} = '{value}'"); + } + else + { + whereConditions.Add($"{field.Key} = {value}"); + } + } + } + + if (!whereConditions.Any()) + { + Console.WriteLine("No valid field values provided for duplicate search"); + return new List>(); + } + + var whereClause = string.Join(" AND ", whereConditions); + var query = $"SELECT Id, {string.Join(", ", requiredFields.Keys)} FROM {entityName} WHERE {whereClause}"; + + Console.WriteLine($"Executing duplicate search query: {query}"); + + var queryEndpoint = $"/services/data/v59.0/query?q={Uri.EscapeDataString(query)}"; + var response = await GetAsync($"{_instanceUrl}{queryEndpoint}", cancellationToken); + + if (response?.Records != null) + { + Console.WriteLine($"Found {response.Records.Count} potential duplicates for required fields: {string.Join(", ", requiredFields.Select(kv => $"{kv.Key}={kv.Value}"))}"); + return response.Records; + } + + Console.WriteLine("No duplicates found"); + return new List>(); + } + catch (Exception ex) + { + Console.WriteLine($"Error during required fields search: {ex.Message}"); + return new List>(); + } } private class SalesforceField @@ -564,5 +804,11 @@ namespace DataConnection.REST.Implementations [JsonPropertyName("unique")] public bool Unique { get; set; } } + + private class SalesforceQueryResponse + { + [JsonPropertyName("records")] + public List> Records { get; set; } = new List>(); + } } } \ No newline at end of file diff --git a/DataConnection/REST/Interfaces/IRestServiceClient.cs b/DataConnection/REST/Interfaces/IRestServiceClient.cs index 8d4db40..942650d 100644 --- a/DataConnection/REST/Interfaces/IRestServiceClient.cs +++ b/DataConnection/REST/Interfaces/IRestServiceClient.cs @@ -42,9 +42,7 @@ namespace DataConnection.REST.Interfaces /// The data for the new entity as key-value pairs. /// Cancellation token. /// The created entity data or null if creation failed. - Task?> CreateEntityAsync(string entityName, Dictionary entityData, CancellationToken cancellationToken = default); - - /// + Task?> CreateEntityAsync(string entityName, Dictionary entityData, CancellationToken cancellationToken = default); /// /// Creates a new entity or updates an existing one (upsert operation). /// /// The name of the entity to upsert. @@ -53,6 +51,41 @@ namespace DataConnection.REST.Interfaces /// The upserted entity data or null if operation failed. Task?> UpsertEntityAsync(string entityName, Dictionary entityData, CancellationToken cancellationToken = default); + /// + /// Searches for entities matching the specified key fields. + /// + /// The name of the entity to search. + /// The key fields and their values to search for. + /// Cancellation token. + /// A list of matching entities. + Task>> FindEntitiesByKeysAsync(string entityName, Dictionary keyFields, CancellationToken cancellationToken = default); + + /// + /// Deletes an entity by its ID or unique identifier. + /// + /// The name of the entity to delete. + /// The ID or unique identifier of the entity to delete. + /// Cancellation token. + /// True if deletion was successful, false otherwise. + Task DeleteEntityAsync(string entityName, string entityId, CancellationToken cancellationToken = default); /// + /// Updates an existing entity by its ID with the provided data. + /// + /// The name of the entity to update. + /// The ID or unique identifier of the entity to update. + /// The data to update as key-value pairs. + /// Cancellation token. + /// The updated entity data or null if update failed. + Task?> UpdateEntityAsync(string entityName, string entityId, Dictionary entityData, CancellationToken cancellationToken = default); + + /// + /// Searches for entities matching the specified required fields to detect duplicates. + /// + /// The name of the entity to search. + /// The required fields and their values to search for. + /// Cancellation token. + /// A list of matching entities. + Task>> FindEntitiesByRequiredFieldsAsync(string entityName, Dictionary requiredFields, CancellationToken cancellationToken = default); + // Add other methods as needed (PUT, DELETE, PATCH, etc.) // Consider adding methods for handling raw HttpResponseMessage or string responses } diff --git a/Data_Coupler/Pages/DataCoupler.razor b/Data_Coupler/Pages/DataCoupler.razor index 56b076e..47ea0e1 100644 --- a/Data_Coupler/Pages/DataCoupler.razor +++ b/Data_Coupler/Pages/DataCoupler.razor @@ -610,11 +610,18 @@ - - @if (fieldMappings.Any()) + @if (fieldMappings.Any()) {
-
Mappature Correnti (@fieldMappings.Count)
+
+
Mappature Correnti (@fieldMappings.Count)
+ @if (keyFields.Any()) + { + + @keyFields.Count campo/i chiave: @string.Join(", ", keyFields) + + } +
@@ -626,7 +633,9 @@ - @foreach (var mapping in fieldMappings) + + + @foreach (var mapping in fieldMappings) { DbColumnInfo? dbColumn = null; if (selectedSourceType == "database" && !string.IsNullOrEmpty(selectedTable)) @@ -652,10 +661,118 @@
Tipo REST Azioni
- }
+ } + + + @if (fieldMappings.Any()) + { +
+
+
+
+ Configurazione Chiave Sorgente +
+
+
+
+ + +
+ + @if (useRecordAssociations) + { +
+
+ + + @if (requiresManualKeySelection || selectedSourceType != "database") + { + + + Selezione del campo chiave obbligatoria. Scegli un campo che identifichi univocamente ogni record. + + } + else if (!string.IsNullOrEmpty(suggestedPrimaryKey)) + { + + + Primary Key rilevata: @suggestedPrimaryKey (consigliato per l'identificazione univoca) + + } +
+
+ @if (!string.IsNullOrEmpty(sourceKeyField)) + { +
+
+ + Campo chiave selezionato: @sourceKeyField +
Questo campo verrà utilizzato per identificare univocamente i record sorgente + @if (sourceKeyField == suggestedPrimaryKey) + { +
Ottima scelta! Stai usando la Primary Key della tabella. + } +
+
+ } + else + { +
+
+ + Campo chiave richiesto +
Seleziona un campo che identifichi univocamente ogni record per abilitare il sistema di associazioni. + @if (!string.IsNullOrEmpty(suggestedPrimaryKey)) + { +
Consiglio: seleziona @suggestedPrimaryKey (Primary Key rilevata) + } +
+
+ } +
+
+ } + else + { +
+ + Sistema associazioni disabilitato
+ Tutti i record verranno sempre inseriti come nuovi. Non sarà possibile tracciare aggiornamenti automatici. +
+ } +
+
+
+ } + +
- } -
- -
+
@if (fieldMappings.Any()) { - @fieldMappings.Count mapping(s) configurati + @fieldMappings.Count mapping(s) configurati
+ @if (useRecordAssociations) + { + Modalità Smart Update + @if (!string.IsNullOrEmpty(sourceKeyField)) + { + (Chiave: @sourceKeyField) + } + else + { + (Rilevamento automatico) + } + } + else + { + Modalità Insert Only + }
} else @@ -689,14 +820,95 @@ }
- - @if (!string.IsNullOrEmpty(transferMessage)) + @if (!string.IsNullOrEmpty(transferMessage)) { - + } + + @if (transferResults.Any()) + { +
+
+
Risultati Dettagliati Trasferimento (@transferResults.Count record)
+ +
+ +
+
+
+
+
+ + Inseriti: @transferResults.Count(r => r.Status == "success") +
+
+ + Aggiornati: @transferResults.Count(r => r.Status == "updated") +
+
+ + Duplicati: @transferResults.Count(r => r.Status == "duplicate") +
+
+ + Errori: @transferResults.Count(r => r.Status == "error") +
+
+
+
+
+ + + + + + + + + + + @foreach (var result in transferResults) + { + + + + + + + } + +
#StatoID EntitàMessaggio
@result.RecordNumber + + + @GetResultStatusText(result.Status) + + + @if (!string.IsNullOrEmpty(result.EntityId)) + { + @result.EntityId + } + else + { + - + } + + @result.Message +
+
+
+
+
+
+ }
@@ -704,7 +916,69 @@ } + +@if (showDatabaseSelectionModal) +{ + +} + @code { + // Classe per i risultati del trasferimento + public class TransferResult + { + public int RecordNumber { get; set; } + public string Status { get; set; } = ""; // "success", "error", "updated", "duplicate" + public string Message { get; set; } = ""; + public string? EntityId { get; set; } + public Dictionary RecordData { get; set; } = new(); + } + // Stato delle credenziali private List databaseCredentials = new(); private List restApiCredentials = new(); @@ -729,7 +1003,14 @@ // Database discovery private Dictionary> databaseTables = new(); private string selectedTable = ""; - private string databaseSearchTerm = ""; // File handling + private string databaseSearchTerm = ""; + + // Database selection + private List availableDatabases = new(); + private string selectedDatabase = ""; + private bool showDatabaseSelection = false; + private bool showDatabaseSelectionModal = false; + private bool isLoadingDatabases = false; // File handling private string selectedFileName = ""; private bool isProcessingFile = false; private string fileErrorMessage = ""; @@ -748,16 +1029,25 @@ private RestEntitySummary? selectedRestEntity = null; private RestEntityInfo? restEntityDetails = null; private string restSearchTerm = ""; - - // Mapping campi + // Mapping campi private Dictionary fieldMappings = new(); // DbColumn -> RestProperty + private HashSet keyFields = new(); // REST properties marked as keys private string selectedDbColumn = ""; private string selectedRestProperty = ""; + // Gestione chiavi sorgente e associazioni + private string sourceKeyField = ""; // Campo che identifica univocamente il record sorgente + private string suggestedPrimaryKey = ""; // Campo PK suggerito per database + private bool requiresManualKeySelection = false; // Flag per indicare se è richiesta selezione manuale + private Dictionary sourceKeyMappings = new(); // Per CSV: mapppatura colonna -> nome campo chiave + private bool useRecordAssociations = true; // Se utilizzare il sistema di associazioni + // Trasferimento dati private bool isTransferringData = false; private string transferMessage = ""; private string transferMessageType = ""; + private List transferResults = new(); + private bool showDetailedResults = false; // Servizi private IDatabaseManager? currentDatabaseManager = null; @@ -1048,7 +1338,7 @@ } await Task.CompletedTask; - }private void SelectSheet(string sheetName) + } private void SelectSheet(string sheetName) { selectedSheet = sheetName; @@ -1058,6 +1348,11 @@ // Clear mappings when changing sheet ClearAllMappings(); + // For file sources, always require manual key selection + sourceKeyField = ""; + suggestedPrimaryKey = ""; + requiresManualKeySelection = true; + StateHasChanged(); } @@ -1178,31 +1473,39 @@ databaseErrorMessage = $"Connessione fallita: {message}"; return; } // Crea il database manager usando il factory con le credenziali complete + Logger.LogInformation("Creando database manager per credenziale: {CredentialName}", selectedDatabaseCredential); currentDatabaseManager = await ConnectionFactory.CreateDatabaseManagerAsync(selectedDatabaseCredential); + Logger.LogInformation("Database manager creato con successo"); Logger.LogInformation("Iniziando discovery dello schema per database {DatabaseType} con credenziale: {CredentialName}", credential.DatabaseType, selectedDatabaseCredential); - // Discovery dello schema - var schema = await currentDatabaseManager.GetDatabaseSchemaAsync(); - - Logger.LogInformation("Schema discovery completato. Tipo restituito: {SchemaType}, Numero elementi: {Count}", - schema?.GetType().Name ?? "null", - schema?.Count() ?? 0); - - if (schema != null) + // Discovery dello schema con try-catch specifico + try { - foreach (var item in schema.Take(5)) // Log primi 5 elementi per debug + var schema = await currentDatabaseManager.GetDatabaseSchemaAsync(); + + Logger.LogInformation("Schema discovery completato. Tipo restituito: {SchemaType}, Numero elementi: {Count}", + schema?.GetType().Name ?? "null", + schema?.Count() ?? 0); + + databaseTables = schema as Dictionary> ?? + (schema != null ? new Dictionary>(schema) : new Dictionary>()); + + Logger.LogInformation("Database tables dopo conversione: {Count} tabelle", databaseTables.Count); + + if (databaseTables.Count == 0) { - Logger.LogInformation("Schema item - Key: {Key}, Value type: {ValueType}, Column count: {ColumnCount}", - item.Key, - item.Value?.GetType().Name ?? "null", - item.Value?.Count() ?? 0); + // Se non ci sono tabelle, potrebbe essere perché non è stato selezionato un database specifico + await HandleDatabaseSelectionRequired(); + return; } } - databaseTables = schema as Dictionary> ?? - (schema != null ? new Dictionary>(schema) : new Dictionary>()); - - Logger.LogInformation("Database tables dopo conversione: {Count} tabelle", databaseTables.Count); + catch (Exception schemaEx) + { + Logger.LogError(schemaEx, "Errore specifico durante lo schema discovery"); + databaseErrorMessage = $"Errore nello schema discovery: {schemaEx.Message}"; + throw; + } isDatabaseConnected = true; } @@ -1277,11 +1580,49 @@ { isConnectingRest = false; } - } private void SelectTable(string tableName) + } private async void SelectTable(string tableName) { selectedTable = tableName; // Clear mappings when changing table ClearAllMappings(); + + // Reset key field logic + sourceKeyField = ""; + suggestedPrimaryKey = ""; + requiresManualKeySelection = false; + + // If it's a database source, try to detect the primary key + if (selectedSourceType == "database" && currentDatabaseManager != null) + { + try + { + var primaryKey = await currentDatabaseManager.GetPrimaryKeyFieldAsync(tableName); + if (!string.IsNullOrEmpty(primaryKey)) + { + suggestedPrimaryKey = primaryKey; + // Suggest the primary key but don't auto-select it + Logger.LogInformation("Primary key detected for table {TableName}: {PrimaryKey}", tableName, primaryKey); + } + else + { + // No primary key found, require manual selection + requiresManualKeySelection = true; + Logger.LogInformation("No primary key found for table {TableName}, manual selection required", tableName); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Error detecting primary key for table {TableName}", tableName); + requiresManualKeySelection = true; + } + } + else + { + // For non-database sources, always require manual selection + requiresManualKeySelection = true; + } + + StateHasChanged(); } private async Task SelectRestEntity(RestEntitySummary entity) { selectedRestEntity = entity; @@ -1390,23 +1731,24 @@ fieldMappings.Remove(selectedDbColumn); Logger.LogInformation("Rimosso mapping per campo: {DbColumn}", selectedDbColumn); - } - - private void RemoveSpecificMapping(string dbColumn) + } private void RemoveSpecificMapping(string dbColumn) { if (fieldMappings.ContainsKey(dbColumn)) { fieldMappings.Remove(dbColumn); Logger.LogInformation("Rimosso mapping specifico per campo: {DbColumn}", dbColumn); } - } private void ClearAllMappings() + } + + private void ClearAllMappings() { fieldMappings.Clear(); selectedDbColumn = ""; selectedRestProperty = ""; + sourceKeyField = ""; transferMessage = ""; transferMessageType = ""; - Logger.LogInformation("Tutti i mapping sono stati cancellati"); + Logger.LogInformation("Tutti i mapping e le configurazioni sono stati cancellati"); } private void AutoMapFields() @@ -1436,12 +1778,20 @@ } Logger.LogInformation("Auto-mapping completato. Creati {Count} mapping automatici", mappingsCreated); } private async Task ShowMappingSummary() { - var summary = "Riepilogo Mapping:\n\n"; + var summary = "Riepilogo Configurazione:\n\n"; + summary += "=== MAPPING CAMPI ===\n"; foreach (var mapping in fieldMappings) { summary += $"• {mapping.Key} → {mapping.Value}\n"; } + summary += "\n=== CONFIGURAZIONE ASSOCIAZIONI ===\n"; + summary += $"• Sistema associazioni: {(useRecordAssociations ? "Abilitato" : "Disabilitato")}\n"; + if (useRecordAssociations) + { + summary += $"• Campo chiave sorgente: {(!string.IsNullOrEmpty(sourceKeyField) ? sourceKeyField : "Rilevamento automatico")}\n"; + } + await JSRuntime.InvokeVoidAsync("alert", summary); } private async Task StartDataTransfer() { @@ -1466,10 +1816,19 @@ transferMessageType = "error"; return; } + + // Validate source key field when using record associations + if (useRecordAssociations && string.IsNullOrEmpty(sourceKeyField)) + { + transferMessage = "Campo chiave sorgente richiesto. Seleziona un campo che identifichi univocamente ogni record per utilizzare il sistema di associazioni."; + transferMessageType = "error"; + return; + } isTransferringData = true; transferMessage = ""; transferMessageType = ""; + transferResults.Clear(); try { @@ -1488,53 +1847,184 @@ return; } - // 2. Trasforma e trasferisci ogni record + // 2. Ottieni i campi obbligatori dell'entità REST (se non ci sono campi chiave) + var requiredFields = new HashSet(); + if (!keyFields.Any() && restEntityDetails != null) + { + requiredFields = restEntityDetails.Properties + .Where(p => p.IsRequired && fieldMappings.ContainsValue(p.Name)) + .Select(p => p.Name) + .ToHashSet(); + + Logger.LogInformation("Nessun campo chiave definito. Utilizzo {RequiredFieldsCount} campi obbligatori per controllo duplicati: {RequiredFields}", + requiredFields.Count, string.Join(", ", requiredFields)); + } + + // 3. Trasforma e trasferisci ogni record int successCount = 0; int errorCount = 0; + int updatedCount = 0; + int duplicateCount = 0; var errors = new List(); + int recordNumber = 1; foreach (var record in records) { + var transferResult = new TransferResult + { + RecordNumber = recordNumber, + RecordData = new Dictionary(record) + }; + try { // Trasforma il record in base ai mapping var restData = TransformRecordToRestEntity(record); - // Esegui upsert (crea o aggiorna) - var result = await currentRestClient.UpsertEntityAsync(selectedRestEntity.Name, restData); + // Genera la chiave sorgente per questo record + var sourceKey = GenerateSourceKey(record); + var currentSourceName = selectedSourceType == "database" ? selectedTable : selectedSheet; + + // NUOVA LOGICA: Cerca associazione esistente + if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey)) + { + var existingAssociation = await CredentialService.FindRecordAssociationAsync( + currentSourceName, sourceKey, selectedRestEntity.Name); + + if (existingAssociation != null && existingAssociation.IsActive) + { + // Prova ad aggiornare il record esistente + var updateResult = await currentRestClient.UpdateEntityAsync( + selectedRestEntity.Name, existingAssociation.DestinationId, restData); + + if (updateResult != null) + { + updatedCount++; + transferResult.Status = "updated"; + transferResult.Message = $"Record aggiornato con successo tramite associazione (ID: {existingAssociation.DestinationId})"; + transferResult.EntityId = existingAssociation.DestinationId; + + // Aggiorna l'associazione con la data di ultimo aggiornamento + existingAssociation.UpdatedAt = DateTime.UtcNow; + await CredentialService.UpdateRecordAssociationAsync(existingAssociation); + + Logger.LogDebug("Record aggiornato tramite associazione: {EntityId} per chiave sorgente {SourceKey}", + existingAssociation.DestinationId, sourceKey); + } + else + { + // Se l'aggiornamento fallisce, prova a creare un nuovo record + Logger.LogWarning("Aggiornamento fallito per associazione {AssociationId}, provo a creare nuovo record", existingAssociation.Id); + goto CreateNewRecord; + } + + transferResults.Add(transferResult); + recordNumber++; + continue; + } + } + + CreateNewRecord: + // Crea un nuovo record + var result = await currentRestClient.CreateEntityAsync(selectedRestEntity.Name, restData); if (result != null) { successCount++; + transferResult.Status = "success"; + transferResult.Message = "Record inserito con successo"; + transferResult.EntityId = result.ContainsKey("id") ? result["id"]?.ToString() : + result.ContainsKey("Id") ? result["Id"]?.ToString() : null; + + // Crea associazione solo se abbiamo una chiave sorgente e un ID destinazione + if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey) && !string.IsNullOrEmpty(transferResult.EntityId)) + { + try + { + var association = new RecordAssociation + { + SourceName = currentSourceName, + SourceType = selectedSourceType, + SourceKey = sourceKey, + DestinationEntity = selectedRestEntity.Name, + DestinationId = transferResult.EntityId, + RestCredentialName = selectedRestCredential, + AdditionalInfo = System.Text.Json.JsonSerializer.Serialize(new + { + TransferDate = DateTime.UtcNow, + RecordNumber = recordNumber, + MappingCount = fieldMappings.Count + }) + }; + + await CredentialService.SaveRecordAssociationAsync(association); + Logger.LogDebug("Associazione creata: {SourceKey} -> {DestinationId}", sourceKey, transferResult.EntityId); + } + catch (Exception assocEx) + { + Logger.LogWarning(assocEx, "Errore nella creazione dell'associazione per record {RecordNumber}", recordNumber); + // Non interrompiamo il trasferimento per errori di associazione + } + } + Logger.LogDebug("Record trasferito con successo: {Data}", string.Join(", ", restData.Select(kvp => $"{kvp.Key}={kvp.Value}"))); } else { errorCount++; - errors.Add($"Errore nel trasferimento del record (result null)"); + transferResult.Status = "error"; + transferResult.Message = "Errore nel trasferimento del record (result null)"; + errors.Add($"Errore nel trasferimento del record {recordNumber}"); } } catch (Exception ex) { errorCount++; - errors.Add($"Errore nel trasferimento: {ex.Message}"); - Logger.LogError(ex, "Errore nel trasferimento di un record"); + transferResult.Status = "error"; + transferResult.Message = $"Errore: {ex.Message}"; + errors.Add($"Errore nel trasferimento del record {recordNumber}: {ex.Message}"); + Logger.LogError(ex, "Errore nel trasferimento del record {RecordNumber}", recordNumber); } + + transferResults.Add(transferResult); + recordNumber++; } - // 3. Mostra risultati + // 4. Mostra risultati if (errorCount == 0) { - transferMessage = $"Trasferimento completato con successo! {successCount} record trasferiti."; + var message = $"Trasferimento completato con successo! "; + var messageParts = new List(); + + if (successCount > 0) messageParts.Add($"{successCount} record inseriti"); + if (updatedCount > 0) messageParts.Add($"{updatedCount} record aggiornati"); + if (duplicateCount > 0) messageParts.Add($"{duplicateCount} duplicati rilevati (warning)"); + + message += string.Join(", ", messageParts) + "."; + transferMessage = message; transferMessageType = "success"; } else { - transferMessage = $"Trasferimento completato con errori. Successi: {successCount}, Errori: {errorCount}. Primi errori: {string.Join("; ", errors.Take(3))}"; - transferMessageType = "error"; + var message = $"Trasferimento completato con {(duplicateCount > 0 ? "warning e " : "")}errori. "; + var messageParts = new List(); + + if (successCount > 0) messageParts.Add($"Inserimenti: {successCount}"); + if (updatedCount > 0) messageParts.Add($"Aggiornamenti: {updatedCount}"); + if (duplicateCount > 0) messageParts.Add($"Duplicati (warning): {duplicateCount}"); + messageParts.Add($"Errori: {errorCount}"); + + message += string.Join(", ", messageParts); + if (errors.Any()) + { + message += $". Primi errori: {string.Join("; ", errors.Take(3))}"; + } + transferMessage = message; + transferMessageType = errorCount > 0 ? "error" : "warning"; } - Logger.LogInformation("Trasferimento completato. Successi: {SuccessCount}, Errori: {ErrorCount}", successCount, errorCount); + Logger.LogInformation("Trasferimento completato. Inserimenti: {SuccessCount}, Aggiornamenti: {UpdatedCount}, Duplicati: {DuplicateCount}, Errori: {ErrorCount}", + successCount, updatedCount, duplicateCount, errorCount); } catch (Exception ex) { @@ -1717,8 +2207,190 @@ { return mostCommon.Key; } - } - + } return ','; // Default fallback } + + /// + /// Verifica se il pulsante di trasferimento può essere abilitato + /// + private bool IsTransferButtonEnabled() + { + // Base requirements + if (!fieldMappings.Any()) + return false; + + // Se il sistema di associazioni è abilitato, il campo chiave sorgente è obbligatorio + if (useRecordAssociations && string.IsNullOrEmpty(sourceKeyField)) + return false; + + return true; + } + + // Helper methods per UI risultati + private string GetResultRowClass(string status) + { + return status switch + { + "success" => "", + "updated" => "table-info", + "duplicate" => "table-warning", + "error" => "table-danger", + _ => "" + }; + } + + private string GetResultBadgeClass(string status) + { + return status switch + { + "success" => "bg-success", + "updated" => "bg-info", + "duplicate" => "bg-warning text-dark", + "error" => "bg-danger", + _ => "bg-secondary" + }; + } + + private string GetResultIcon(string status) + { + return status switch + { + "success" => "fa-check-circle", + "updated" => "fa-edit", + "duplicate" => "fa-exclamation-triangle", + "error" => "fa-times-circle", + _ => "fa-question-circle" + }; + } + + private string GetResultStatusText(string status) + { + return status switch + { + "success" => "Inserito", + "updated" => "Aggiornato", + "duplicate" => "Duplicato", + "error" => "Errore", + _ => "Sconosciuto" + }; + } + + /// + /// Genera una chiave univoca per il record sorgente + /// + private string GenerateSourceKey(Dictionary record) + { + try + { + // Il campo chiave sorgente deve essere sempre specificato + if (string.IsNullOrEmpty(sourceKeyField)) + { + throw new InvalidOperationException("Campo chiave sorgente non specificato. La selezione del campo chiave è obbligatoria."); + } + + if (!record.ContainsKey(sourceKeyField)) + { + throw new InvalidOperationException($"Il campo chiave '{sourceKeyField}' non è presente nel record sorgente."); + } + + var keyValue = record[sourceKeyField]?.ToString(); + if (string.IsNullOrEmpty(keyValue)) + { + throw new InvalidOperationException($"Il valore del campo chiave '{sourceKeyField}' è vuoto o null per questo record."); + } + + return keyValue; + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nella generazione della chiave sorgente per il campo {SourceKeyField}", sourceKeyField); + throw; + } + } + + private async Task HandleDatabaseSelectionRequired() + { + try + { + if (currentDatabaseManager == null) + { + databaseErrorMessage = "Database manager non inizializzato"; + return; + } + + // Ottieni la lista dei database disponibili + availableDatabases = await currentDatabaseManager.GetAvailableDatabasesAsync(); + + if (availableDatabases != null && availableDatabases.Any()) + { + // Mostra il modal per la selezione del database + showDatabaseSelectionModal = true; + StateHasChanged(); + } + else + { + databaseErrorMessage = "Nessun database disponibile per la selezione"; + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nell'ottenere la lista dei database disponibili"); + databaseErrorMessage = $"Errore nel recupero dei database: {ex.Message}"; + } + } + + private async Task OnDatabaseSelected() + { + if (string.IsNullOrEmpty(selectedDatabase)) + { + return; + } + + if (currentDatabaseManager == null) + { + databaseErrorMessage = "Database manager non inizializzato"; + return; + } + + try + { + // Cambia il database attivo + await currentDatabaseManager.ChangeDatabaseAsync(selectedDatabase); + + // Nasconde il modal + showDatabaseSelectionModal = false; + + // Ritenta il discovery dello schema + var schema = await currentDatabaseManager.GetDatabaseSchemaAsync(); + databaseTables = schema as Dictionary> ?? + (schema != null ? new Dictionary>(schema) : new Dictionary>()); + + if (databaseTables.Count == 0) + { + databaseErrorMessage = $"Il database '{selectedDatabase}' non contiene tabelle accessibili"; + } + else + { + isDatabaseConnected = true; + databaseErrorMessage = ""; + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nel cambio di database a {Database}", selectedDatabase); + databaseErrorMessage = $"Errore nel cambio di database: {ex.Message}"; + } + finally + { + StateHasChanged(); + } + } + + private void CancelDatabaseSelection() + { + showDatabaseSelectionModal = false; + selectedDatabase = ""; + StateHasChanged(); + } } diff --git a/Data_Coupler/Pages/RecordAssociations.razor b/Data_Coupler/Pages/RecordAssociations.razor new file mode 100644 index 0000000..b55bf72 --- /dev/null +++ b/Data_Coupler/Pages/RecordAssociations.razor @@ -0,0 +1,535 @@ +@page "/record-associations" +@using CredentialManager.Models +@using DataConnection.CredentialManagement.Interfaces +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.JSInterop +@inject IDataConnectionCredentialService CredentialService +@inject IJSRuntime JSRuntime +@inject ILogger Logger + +Associazioni Record + +
+
+
+

Associazioni Record

+

Visualizza e gestisci le associazioni tra record sorgente e destinazione

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+

@filteredAssociations.Count

+

Associazioni Totali

+
+
+ +
+
+
+
+
+
+
+
+
+
+

@filteredAssociations.Where(a => a.IsActive).Count()

+

Attive

+
+
+ +
+
+
+
+
+
+
+
+
+
+

@filteredAssociations.Where(a => !a.IsActive).Count()

+

Disattivate

+
+
+ +
+
+
+
+
+
+
+
+
+
+

@filteredAssociations.Select(a => a.SourceName).Distinct().Count()

+

Sorgenti Diverse

+
+
+ +
+
+
+
+
+
+ + + @if (isLoading) + { +
+
+ Caricamento... +
+

Caricamento associazioni...

+
+ } + else if (!filteredAssociations.Any()) + { +
+ + @if (!allAssociations.Any()) + { + Nessuna associazione trovata. Le associazioni vengono create automaticamente durante il trasferimento dati. + } + else + { + Nessuna associazione corrisponde ai filtri applicati. Prova a modificare i criteri di ricerca. + } +
+ } + else + { +
+
+
+ Associazioni Record + @filteredAssociations.Count +
+
+
+
+ + + + + + + + + + + + + + + + + @foreach (var association in pagedAssociations) + { + + + + + + + + + + + + + } + +
SorgenteTipoChiave SorgenteEntità DestinazioneID DestinazioneCredenziale RESTStatoCreataAggiornataAzioni
+ @association.SourceName + + + @association.SourceType + + + @association.SourceKey + + @association.DestinationEntity + + @association.DestinationId + + @association.RestCredentialName + + @if (association.IsActive) + { + + Attiva + + } + else + { + + Disattivata + + } + + + @association.CreatedAt.ToString("dd/MM/yyyy HH:mm") + + + + @(association.UpdatedAt?.ToString("dd/MM/yyyy HH:mm") ?? "-") + + +
+ @if (association.IsActive) + { + + } + else + { + + } + + @if (!string.IsNullOrEmpty(association.AdditionalInfo)) + { + + } +
+
+
+
+
+ + + @if (filteredAssociations.Count > pageSize) + { + + } + } + + + @if (filteredAssociations.Any()) + { +
+
+
+
+
Azioni di Massa
+
+
+
+ + + +
+
+
+
+
+ } +
+ +@code { + private List allAssociations = new(); + private List filteredAssociations = new(); + private List pagedAssociations = new(); + private bool isLoading = true; + + // Filtri + private string sourceFilter = ""; + private string entityFilter = ""; + private string credentialFilter = ""; + + // Paginazione + private int currentPage = 1; + private int pageSize = 25; + private int totalPages => (int)Math.Ceiling((double)filteredAssociations.Count / pageSize); + + protected override async Task OnInitializedAsync() + { + await RefreshAssociations(); + } + + private async Task RefreshAssociations() + { + try + { + isLoading = true; + allAssociations = await CredentialService.GetAllActiveRecordAssociationsAsync(); + ApplyFilters(); + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nel caricamento delle associazioni"); + await JSRuntime.InvokeVoidAsync("alert", $"Errore nel caricamento delle associazioni: {ex.Message}"); + } + finally + { + isLoading = false; + } + } + + private void ApplyFilters() + { + filteredAssociations = allAssociations.Where(a => + (string.IsNullOrEmpty(sourceFilter) || a.SourceName.Contains(sourceFilter, StringComparison.OrdinalIgnoreCase)) && + (string.IsNullOrEmpty(entityFilter) || a.DestinationEntity.Contains(entityFilter, StringComparison.OrdinalIgnoreCase)) && + (string.IsNullOrEmpty(credentialFilter) || a.RestCredentialName.Contains(credentialFilter, StringComparison.OrdinalIgnoreCase)) + ).OrderByDescending(a => a.CreatedAt).ToList(); + + currentPage = 1; + UpdatePagedAssociations(); + StateHasChanged(); + } + + private void ClearFilters() + { + sourceFilter = ""; + entityFilter = ""; + credentialFilter = ""; + ApplyFilters(); + } + + private void ChangePage(int page) + { + if (page >= 1 && page <= totalPages) + { + currentPage = page; + UpdatePagedAssociations(); + } + } + + private void UpdatePagedAssociations() + { + var startIndex = (currentPage - 1) * pageSize; + pagedAssociations = filteredAssociations.Skip(startIndex).Take(pageSize).ToList(); + } + + private async Task DeactivateAssociation(int id) + { + if (await JSRuntime.InvokeAsync("confirm", "Sei sicuro di voler disattivare questa associazione?")) + { + try + { + var success = await CredentialService.DeactivateRecordAssociationAsync(id); + if (success) + { + await JSRuntime.InvokeVoidAsync("alert", "Associazione disattivata con successo!"); + await RefreshAssociations(); + } + else + { + await JSRuntime.InvokeVoidAsync("alert", "Errore nella disattivazione dell'associazione."); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nella disattivazione dell'associazione {Id}", id); + await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}"); + } + } + } + + private async Task ActivateAssociation(int id) + { + try + { + var association = allAssociations.FirstOrDefault(a => a.Id == id); + if (association != null) + { + association.IsActive = true; + var success = await CredentialService.UpdateRecordAssociationAsync(association); + if (success) + { + await JSRuntime.InvokeVoidAsync("alert", "Associazione riattivata con successo!"); + await RefreshAssociations(); + } + else + { + await JSRuntime.InvokeVoidAsync("alert", "Errore nella riattivazione dell'associazione."); + } + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nella riattivazione dell'associazione {Id}", id); + await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}"); + } + } + + private async Task DeleteAssociation(int id) + { + if (await JSRuntime.InvokeAsync("confirm", "Sei sicuro di voler eliminare definitivamente questa associazione? Questa azione non può essere annullata.")) + { + try + { + var success = await CredentialService.DeleteRecordAssociationAsync(id); + if (success) + { + await JSRuntime.InvokeVoidAsync("alert", "Associazione eliminata con successo!"); + await RefreshAssociations(); + } + else + { + await JSRuntime.InvokeVoidAsync("alert", "Errore nell'eliminazione dell'associazione."); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nell'eliminazione dell'associazione {Id}", id); + await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}"); + } + } + } + + private async Task ShowAdditionalInfo(RecordAssociation association) + { + var info = $"Informazioni aggiuntive per l'associazione:\n\n"; + info += $"ID: {association.Id}\n"; + info += $"Sorgente: {association.SourceName} ({association.SourceType})\n"; + info += $"Chiave Sorgente: {association.SourceKey}\n"; + info += $"Destinazione: {association.DestinationEntity}\n"; + info += $"ID Destinazione: {association.DestinationId}\n"; + info += $"Credenziale REST: {association.RestCredentialName}\n"; + info += $"Creata: {association.CreatedAt}\n"; + if (association.UpdatedAt.HasValue) + info += $"Aggiornata: {association.UpdatedAt}\n"; + info += $"Stato: {(association.IsActive ? "Attiva" : "Disattivata")}\n"; + if (!string.IsNullOrEmpty(association.AdditionalInfo)) + info += $"\nInformazioni aggiuntive:\n{association.AdditionalInfo}"; + + await JSRuntime.InvokeVoidAsync("alert", info); + } + + private async Task DeactivateAllInactive() + { + if (await JSRuntime.InvokeAsync("confirm", "Sei sicuro di voler disattivare tutte le associazioni che non sono attualmente in uso?")) + { + try + { + // Implementa logica per disattivare associazioni inattive + await JSRuntime.InvokeVoidAsync("alert", "Funzionalità in via di sviluppo."); + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nella disattivazione di massa"); + await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}"); + } + } + } + + private async Task DeleteAllInactive() + { + if (await JSRuntime.InvokeAsync("confirm", "Sei sicuro di voler eliminare definitivamente tutte le associazioni disattivate? Questa azione non può essere annullata.")) + { + try + { + var inactiveAssociations = allAssociations.Where(a => !a.IsActive).ToList(); + var deletedCount = 0; + + foreach (var association in inactiveAssociations) + { + if (await CredentialService.DeleteRecordAssociationAsync(association.Id)) + { + deletedCount++; + } + } + + await JSRuntime.InvokeVoidAsync("alert", $"Eliminate {deletedCount} associazioni disattivate."); + await RefreshAssociations(); + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nell'eliminazione di massa"); + await JSRuntime.InvokeVoidAsync("alert", $"Errore: {ex.Message}"); + } + } + } + + private async Task ExportAssociations() + { + try + { + var csv = "Sorgente,Tipo,Chiave Sorgente,Entità Destinazione,ID Destinazione,Credenziale REST,Stato,Creata,Aggiornata\n"; + + foreach (var association in filteredAssociations) + { + csv += $"\"{association.SourceName}\",\"{association.SourceType}\",\"{association.SourceKey}\","; + csv += $"\"{association.DestinationEntity}\",\"{association.DestinationId}\",\"{association.RestCredentialName}\","; + csv += $"\"{(association.IsActive ? "Attiva" : "Disattivata")}\",\"{association.CreatedAt:dd/MM/yyyy HH:mm}\","; + csv += $"\"{(association.UpdatedAt?.ToString("dd/MM/yyyy HH:mm") ?? "")}\"\n"; + } + + var fileName = $"associazioni_record_{DateTime.Now:yyyyMMdd_HHmmss}.csv"; + var bytes = System.Text.Encoding.UTF8.GetBytes(csv); + var base64 = Convert.ToBase64String(bytes); + + await JSRuntime.InvokeVoidAsync("downloadFile", fileName, base64); + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nell'esportazione delle associazioni"); + await JSRuntime.InvokeVoidAsync("alert", $"Errore nell'esportazione: {ex.Message}"); + } + } +} diff --git a/Data_Coupler/Pages/_Host.cshtml b/Data_Coupler/Pages/_Host.cshtml index b56680a..35b9527 100644 --- a/Data_Coupler/Pages/_Host.cshtml +++ b/Data_Coupler/Pages/_Host.cshtml @@ -12,6 +12,7 @@ + @@ -30,5 +31,6 @@ + diff --git a/Data_Coupler/Program.cs b/Data_Coupler/Program.cs index efb4e83..800a306 100644 --- a/Data_Coupler/Program.cs +++ b/Data_Coupler/Program.cs @@ -35,7 +35,7 @@ builder.Services.AddHttpClient(); // Register Data Connection Factory builder.Services.AddScoped(); -builder.WebHost.UseUrls("http://*:7550"); +//builder.WebHost.UseUrls("http://*:7550"); var app = builder.Build(); diff --git a/Data_Coupler/Shared/NavMenu.razor b/Data_Coupler/Shared/NavMenu.razor index d932379..184ffb9 100644 --- a/Data_Coupler/Shared/NavMenu.razor +++ b/Data_Coupler/Shared/NavMenu.razor @@ -32,6 +32,11 @@ Data Coupler + diff --git a/Data_Coupler/wwwroot/data/credentials.db b/Data_Coupler/wwwroot/data/credentials.db index 07d78473ca16895a7c8fdc286b7700f7c3c73fb8..052349c31683e3ec11a355fb8da6a118c2f4bc73 100644 GIT binary patch literal 77824 zcmeHw3vgW5dENqx#lG(qMNtq%kpe+c6hv})-w#L>b@xTQ2m%WbBAIA7;9^OFA^{Q) z>ftc<#&RsVwL6{IuASEIIGNN|Yp1mvCw1dwsx)>YyLPIkZpNzPj3@Hg>d0}d*iAi+ z+y3X^+{M|w|J{2vvNLjhSJKgcaQ;2}{r`EN|D6BK&P*$v#Cm&U z4-XE;VzD^(m&a%i}@BbOXFA@?-xF2FaloO|*b0zS{c zcSXIGK`?UiGl1)GHaYMp;5yR?@uQOehv_w>Vn8qBfJXCgn{PCryE8u^KkRl zRB~Vf!0KJVLQyZQHx`|BFUWKlkge}`1GmLAd@-3EQ1|sT6E3E$y0&(XR8uaV@&Mox zDf8x!a(^X_b+fWB_iz5tcI<~pwdTH+WIyETEI6KXwDr?Ig8M7^J(@7QmPigf0D|qn ze$TBz+&BBX3g>Lh4qE=3u}!IttPtuF#jd=$J-c&pBCfE z0a@;O=NSCf6{Hb*t5qNS{W+&NS@G#`^O-^T42>-gLC-x`IbNv^PMw?^oH{i*IaoS5 zH9J>x;O~P&(-X*;Ff=$_8$03D&J0de&I~%I=1z`H!Ec_ZOwAo)8FT&l;9TW25w)Qwozq;`_*Os^@!Tmrh7iVMr(%i~pjb$X8(#qAV3(JdhOII5! z8@wQH(|0SPD!Gbayt6PdTwYw`WbgcnEVaHfc5!)SwK2PPrLwxZvfAxSt*?+4INMmg zu~etMDS~~pv9Q=6nQc}EPV}wDGQF@0I$p=wpyIW+&5jh=R5NY6NQUlF9dj?|Lci$arZezsV zz4>hwFnaZ*K_R8mKzep&(p_E6UcW+C360e&EA@pdmtc{W_~6=tQ<)vPpg;A*%uKz0 zsXF?YE?vGpx;Ryvy?FHKo<&7c^uv;ISTP3`WkfedRI6YZwq!^KpQMhc>WHou6h*ge zQ(m~zSX+DhXnbdE*REI$@XR!K5KyVTvA3_4Vgu=NTNc;O;!|F$@Y0IR3hl+shHro4J_ zOs}rZ9Y1?=eWP~a{Q5-g$x-Y2sq)FQkErX9tT<0DOwT*o%(Ps&d}TwQsa?Kyarxk5 z+MdPP)~~*0sB~osegHN+MuyZA^T+K)r?T;cazkF5ym9H=xOM*I6UzAIjbd4U%Bh>{ z%X76yPAzE8^yB9mk6*F$xs@^qWo$gYbn4t<>7;V$%B78SlaotRm*A!}JvMZR z{RAuwR~zf_Gjh3Nn~GB`mkdp_9J^AIl!|F-PEm7|s$A6NYPnc-G`&(btL3Uy(aLI7 zRZXoT8=CH@kR#}E9@cKKE{FVdyCA{e#@1$G#d?0_3HVbL6bSZ#Ay`b$t}I*w_SPS7 z@h=R5NY72Ndvd{#^oOG8LT<6 zC%&%-jt#J5o@?<0NYy94N@UwE{%`Srir*7o&AgVe({D?CK9x((_5W((8;JvbEAdbF z{#|de=b6}--^^-gnmh8b-MbI$l6RG2>81=jbcK_pS{aT%(IauA&g|$pLshI|N!FZt zePv^LU6SOBlM?xq^!n-IjB}Rlq4HZcunnt^NP0n*Ow+Q|w+zj#E-bHIXsp5(a09mc zws_~1HeOph3tNMmB{8*e^?YM>Xhe|@4V_rHb`8$>ln8>M5&34*ye$}cN!N;!UX~H$ zkF1wwoOxg5TEUPFRh7daXS|AxAeS)Y+k=rik|kNPZX(F*^Jly1Tq`J+q$rjiLg$Rv zHoVAX8$oW|>LPc1YYQMR+Pdu^$mjKq8K>&2bG2ZrvZ>i2fN!$u5~6b1LV%mwT;TrI z+$<}wn_V>!;PtsDW}NXZz;)d=4I@ltQ45sU#E_c^a{U$;IZ^o$w1Ju>r>x3`jv=3& z>{eO8P9jUHYJ^fbWi^qiVj#Fp(Z$_Gurl(ELC zAh^w}i`&1FtD2)(x>`c2N_}#@o5tmWY+H(<%At^N^4f^T(a|##jNFn+j$CpS1o>H2 z>sHC7f(<4+#R$_p8Lx@ea%}W8(mZm!dFq;ED!N+5kl(n`9l0UvhGK^45ha)-0I!80 zM+eVTFmOc!qjgy=Bf#ququnaGR8VZmvUDp%WnsJq(wAd{r;!W>UY0A+ImnpC>th#u zYq>T8_F+r070nC;zQt=H^A&9DH2Q;qgV|atmuw99*&F`T6$(a^ZJ2_D15SAr1UNQ$ z8i`=ws%o1K*u{~ma_XYtTgL&oRe;8!%W_~(zQwB`Jvlmd_5}ks$`wg1l4g0obswj9mmE_BOU^{q!zWJjEk~V&=*gWjet~7 zZ#h=R5NRONK%{|418)uu zWaGPf*frjGA->QP-eqKOe|$UfEx5V<|5{A^zW8nNYi|yzA{3DZA`L_uh%^vsAksjj zfk*?91|kha8i+IyX&}--cny%<0roURoTGcQ@!e_n=@PR3pG&+eCVoSFUi^sI6cV`J|etbs0$@wpU{_oIsdu*Z{**RZ{#ca{rN=ho4G&AeKhyZ+{N7S+<{y& z`>pIBWj~sIXZB+Dc=kXxnfX@ck24?3Jd?SU8O_|6Nu|G?{^Rt=($AzXrAO2ErPHZb zQZJ;QOFf%fN{yw4Q|aU@$rqB(C7(?$CC8G($#nlK{V()C*Z*w)QvX>0{r#E5|44ix z@h<^r^ocYOX&}kHK*`&y9%6Pg`fEUAb)38c@>F}Ny_-C|>e(9<%Tks5# zgQv>{+cr%2_hG+u!%%G9(2x10D=IuvC94nN=@L{gs3D4a)Gr<0JtOX4A4Jk6;%$w* zprmT*1Agh6rRtVykNBm_nj~wQVtcDMd`Z^}s%bzGTi&meWUXM?vTkdJ>6Z?5OOZf< ze(4Hm*R(YqPge^DG!8?THNSN5lS*Hh!qcI8ShA#>vf`Hxd4uawL-tDt*MS;1vy=SN zCGfWj?$?gs>2kr)3=OJ4!7p7QotI`E#?!%>qGA|O!4LVRL-kg5$vTJ@p9a+fnx~}Q zkA5AJq2)pGTf=_o0NsS_@59qA@Wd%YpRFG7OV?Bb%167`FCCsIhvs4I$J5P%2`>U_ zx_Xaax(dAm_(|O7mkz}P-LF1`r-Q37%`{9)-|LsID$xEVZ4gg~-dj^8&9?9MONZJ8 z&MUP&e(B&S5#Bqrb|dNFZyV$X7m(06@A69rc*HmKuHErt9fK3`*9~v&Nv3SwiGSND zz_>^J9Pji?hp|JlHTe!aT_=6JuBrBbU%I9kmS&o_`=v8);ydv4me20n{L-0c?CnV9 z2kBk^)3@SZXP%o4b(`PUH+?DJf`7f`vRL#>XO4jdzjWq*H;C7)`+Ap2CA59_Y;7G9LMN5{PX!w=by`eDF2UP2jR*5)%-&KRDLXfG%v$m!p?j?-;?`6?v>o1=U&WxF87(- zCvqRjy)XA{?q=?Ku8}*FJCSp8dTuzk8-PZiNCS}uA`L_uh%^vsAksjjfk*?927Y`R zAcjKb%6^WcXF0mY(K8%9&Cw@0dWxe@aP%ZcPjK`&M~`vzC`TXX=qg87IJ(TyC5|p~ zw8PO4bM!Heeu$%wa`b~7{QyUgaJ0?Q7Dt;LZE&>C(Hcjq9IbG)%+V4@AK~Z%M<3?s zLmYjOqwnYFVUE6!qYrTOy&S!tqwnG9eH=Z+(R(?1kfZPB=sg_0o1^dI=v^FrCr9t( z=sP%ifTM5c=p7t=8%J;F=vz5@8%N*5(IQ6+9G&Or97kt4I>XUvj!toOlB4@MI>FI> z98C`Zuy);>iSJ4?ny&waM`Pgq?{CG|#P7f@fWH*KE`C+~ui~GGUx3|!{~-RZ_}k(q z#gB_06@N|qkoW=dSH$E&~J|xk?_BSzY+dQctv?6St3HFj;kOX&=U=Im)li)5A>>|OPB-lxU zJ4i43g9NvcU^@wJCBZfl+(H781Of^2B*>8pR@2|q?|I^|G*z;xa9&td-fc^fjg})U38=U?B ztnjaeUlZOZJOlRmWnn>>6OIcbaPogp*e>+vf0%zY{}=iH0zL!&UH)I?KMd#opUyv? zznp(GKb?|4;UN*_X3_mi+<%jXseEA`L_uh%^vsAksjjfk*?927XpGkc;o`SzK6OIKQyg znBya}e5A%lX86c7A34cKrufJSJ~GKiCiuuW9~t8#qkQBzAF1+@3Lh!+krE#%@)3uR zJj_Rq@sWr4$WcD>ARl>vkBsmUn~zw0#N;CeAJO@U#z#~>qVN%!k4Svv2p=i%k;8oC z5Fa_nNABk%!+hjEK5~GM+{;Jy^O1Y_$UZ(Y#7Fk>kwHFkHy_!=k(qyhNw3&`Hl}nrMNK;fvaq~Rs zO>6z$RA-v9h2=zN8hEa9^PKEVw5>eH+S4Y<&nZkVqez;}(vG*M zm2C21X;mc6Vri9*G>em=+>r)l+mfNwk;a!_u_KKyKc^#212=K5ULN+OZT;S{&NL3o zLmg>+ojBT&rVyY^_6IxD2vC;xKxdk2x6(#B(>N^lP-6Fit@beL%#NOujiOREO*d-` zVF6cl;LxtR-T(v%W-XRI(5 z6=4C_eRf4Dnv6w3Sma_+QHs?PW04US@KC6fz>6DWkr0-OQM4*0#bGQ*5Ef0<%Ed~# z%vcHti&X_@YD&qK=rF=kF6nBqXt+Og2w?${9lfl&LLWp}jH+BJ8M?->+>fy6Rn2g~ zF(qRe##l5HysIfL%Y6uoA(f<}R;{vJ4j?SxAg~B75{r!GUWBEhNma$Hx|Lx+!r~aF zB3CP}>h3{U0Et}&ccScv_8~0bt*}%zS}kA*V{uAyrP}K2_985fBHLxP?CNC@V^K@C zQF2@?cOxv-QdNPXc3JiyEK;RtmP?XbcXuN!MnzRBj^nECE`+6|R2{u!xGcL6mNHbQ ziqvY2cOon{G-tV7bcNoDut?x7&?#DO3%CPgsmPV0s=EEl0Kx(lvji?z-PU+J!U9e` ztzt=btM?9srKD>`NiVxy&}|5dBU#}5QFq(XcATYX>$ck(Z$((lvSL-VR^8o(v6P%L z_z87O`WB2ul8TmPxmZMmMK-FASybKLTR>P0yX+{XR`t#!EKqSx(@|X2;RvF@vyx+ez?Cs=7Tx8evg1%Qm4Qu?Cw$Sjuw6kTu!OC5f<9z`t*~ zWV3do{ES%0ioysO> z`ah8Pdg7kG%fOC(nlp)fY&ZOvysK17H;=eK>25R7pYBit%#bfmk|~2!(obD9W}GTJ zli+n%*m9THkz|d?RsjlHS2g7=LvyPO%WD@JtMIl#V`yY(i+2v3X%nx9EE`2XJ9KEM z)~K(nE>3M+J>OUz8qp68omjYbZE5*pX=P)1eP~3!*_`PMmZ_uMc$w;>>WnkqMJDFr zaSJDL`mKUY;P=!9zq6JaN+z0(YI%P*0AKaF&SdX-d1m5*Y=Mg9^myL}`i-_B-WrBt37{_>_ z&Z?SktpqeJ2e%%fWN>;dfzxu+2r9y@Y)qtD%96JXn=@%%D7;>FjJ$0^-MG%?V_(t zP-&C`bHN=1e2W)pGBPrah{gJbIN*4X-LV?rf!C)^-wH;-FyG*zfKy%rnbXP0WC4&L zWyp`Uk3!s7)3HctLq0X%t+g|+f0%=z*{~lM3zze*C>PbZvG4CVM(ORTDx*Qh|2*p}T7#Sf(FCH{j{oVEzNo zZ|JfT*tNax9->*x^D_O!dI(G=cv1oep)hkgrSQ5skLGmF73vtQa&e*FaOZ)E1kY^1 zj(wP7dY%1BKJnGq_So52qA#;6T}`bfKNLTk_VUK97w@ZSW;})-tjBcKFzjCu_37?qzdHo*_MKrK_+r_AR0iO!*Iq&Z-{jRLWWMSd zlmU2cR|~jryXx3R=7HB27iXLzu5swOOz4g2Gcn$lr4?jVCf1}NnYOaQYLS9XSZlY1 z5UPEfm>24i_0o(p?<-X6br`QuN&&XRWK+{Z2*t8dkZFj5EdXn`2Mg7)gU$;zfBjOo z8LM7kPv&4kS*D@GQ)MB9V%cCT3k8{5HEUw9P+;`scgT66>NEa56&*NXFV9xN^d>3# zVaYhGm~5j@p<5F&ydM`rDE8ovfozJ&o?cA8+5bXpm(R4TS=$jTZ-H)W)*05qTA4za{hPmb|cx=f~!zvAZR&??@!I+q!spACXS@ zwnW-5vRABv3UP~9wUAkV=lYQJi`!ekefz}@GZGKHK7Y1bL1_i{fD~3xGS~?0;GIn< zD3**#S1mB`!D1Hjr;1AS0;GNP=Qu#hw1A#bI~QFTa>X4 zm)bU0CSv~JD<3_aO?NOmEOoq0^*Pvr_AwPunRJ~zR0i*rd3z9e9Z%NmK=a2|HYO$% zHh@jt;tJ&)^V^*;+ii<&Ld{Ngt9>f0rzCj26O&0(VP7n$IZ=sd9>^?1Mz&kBFU5AE zU1GC#mn*4%@pV|bc}dSMU+*@mwVtu_imDahfk3c;g&C?@HX2fV3Q{Lws@@s^JRRbm zmb|_lV#oOaUf#u`|0W3Svg%?M3KE0 z=q)C-0IyV9@Q`PaL{KMLCfL+hK`Ldlw$l~KzwUJ$erOYEVS2`?`A!pCj~rq`!Rv^& z6>K@BLeaMpO{9-kkjY20c1N&K9mh4=ggUd_tv@sh>}5twDAmy6(bF(943>?K%`iO6 zE3gOG20{qce!!$nD9!0sQmyACF`*=|oa&*rG*~toGQ&_jOE39m9=eI`y;e*-F1#oI zmHb_~^VyGOzMIk0zmWQaR4O^u|K7w)iNU@L@sIbu)@$|rV(brJ#}>ff6Km5nS_R{~ zr`vICkT00U^W(k+Lm&BNkN5^P{w-c~<>6`k(4U!lF!GMmjBUuz!g9)I-ayf?=Y>Nd zC%o|1vUg_cZQ%sD7L2^(kYyY4GkQ1Vt%r|8Aa{9@{b%foik{m ztw+g2AZNT9W?l5mxe4;a!N@yKVe`lrbvX3lvyn-Wv**%7A*Z}LQd?xtVvis{6pXy% zaCjT?8f?M(k3_A9+(RH|yaqDY^sed%@`J(1JI<`PA)kRYK{w>=iTO~-DX)yx7SAjh zkQ?{A$Q|E#OUF_AHsmK^>hE(H50+MN4oEiO@%3QfEmo|uphM`e3!IpC@a>L{1O9Em zPxw#oDR5o3C0PsHsM+Gxk?F42{7+PVACDYAaiO>0J7_~b>A&?!(I_fx_DbQ9Q(k1g zC8Hzg0UkK6@{ZdOZNSIP9Yko1$*ki zs~}5N&lW2Ieorv)jvFmJ@WoO8?MMI)8&QTWX?mE-!gvj2qKJ*3#=cqkZ}!{~n@G&}JsbaQ?_cMi&wW34Z}xQNUFpyEeX;M3 zc%%1I@CE$S9Dji5V5i?5fRe)9E98YgJq>eYywo&lM2B%qQDs%@*!|Yz5e-Zax}w>7 zP~+#a!FGGFwbuAZfH2%eNM4xPld#LvO&ASU3l>~!Yum~v!hp>I25empCk)L-Lkbxi zJjU$+VaP2Eb~}X^=F9?IVZg6tY^qe&V4Nm*S9!a{rZ8|WTn)DL@!254Amd5${jrDK zYKBcd=D{R@3bXh)9Mo-&9c!rqJM9Y#fBf6^D@%S;bC4GfnHz>;>xS-Cg$~CL6x#?a z)~#GjWFCffI^z$uWC5#_XM8GYKE*oSfa?K1#Y$1Y%tV%Dq`~P0xP}b}k%A^Tn<}B% zD98*4>4=)+N4pEd-)3&BqgNmACX7_DOjXhqL>R4L%1~%VP_IgaVcDpNFxbp~?7@~W zXf5NfU-QB&UW6$Fz5v`rq3Ezhr+_J>(7}5-&-T39jCkbk}_WZuyO4|(yir@FOL*c_F>DsP18BpEN(fjC6lJ+_er2$iaOS9qHY`D^(l`U)qYEG29OY=r65GD=urUndk zKmNMaoG5jd=8e`ZuQYIV&y?)ot9ztXz0PXaoG5mehIf0?)>1A{`I*)>VZfTm1TPSn zG%yE&8|h)n2KN6+HWt=Zc@8nc@_5|grSZ<2ct*2^ofkvdO?MYk%4n0jun}Rd(YZHjWSIa zcb5&(cHcHU?lb4%!W}*#-<)ZHwsl(zn%r@`I z>Pf2ojl@5RzbYz1BmaTi7s2(!ga8Lx+Q$ z-n|bUF2ZTx%fSjG4a){wVxyM%v1&^iIG*Y$n>N>PeAz5sJ>RWt)PiY&OBZa1&MLrx zIVIfOmu7;xMnKlsnai*V%eGGGo>p^WvLzfgweP;kg&zkb6;FqmlBL2N(L1W!1w(~p zixG6jk;}$FrYPu9go%j&X?!-S7B^sA^lwHmop-u+PHwKUB&vRS&l4NH!*7D)Js(CzIhZUK-Do zYo6?;Vx2;Le!jjdCu@N@y<-l7YTsGLr5o||vVw4xg(gDXF=3F_(j1@eE{vZ!Wbr&$lKo4@0CR{5 z=2Wy~ByeRFco&gWGdCAZ7-V|W96#v_gI@WX=RFkj9a%$$8_E79V-(1}D6l_z+KdFF zk7j9smi|p)Hgmy*!7RfQQ?4-Rg$*JM+5;?}pXgpPV5U-S)w|&WvuK!Fn{Z7zL_LEG OA0!(M+qpnY!~YM3sp7i; literal 45056 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 diff --git a/Data_Coupler/wwwroot/data/credentials.db-wal b/Data_Coupler/wwwroot/data/credentials.db-wal index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1ecaaad417f06755207d90ebc0593961a629dad0 100644 GIT binary patch literal 1586232 zcmeFa349$%b?+}*)^eqLZ+qWld6D-;?XI=EWy^NozV}*|yvg>MF$p1FG~)rr!;C${ z1KuQ~0D*xJK0Go6@(;;>AS4iGNFX7}u;gX(!a8A@e?s_^K;}UpFi9YRKtl2lynj`x zT0XkBTGFg|IsU-;oHM6%)%EM@>h8Lw^9_Ht{pVWxzq-7oWqynP_uhLRyX8ZV%z0qX z$6oR9+kbcSI`y9l@V^hRj~{$+#|^*HrT*iIw%4|JU-2IH?)S#M(_Z4O^X8PlQhvC6 zfB6;Vv*onBzC5q=wbDmR?<~ElG*+sWHkR6oUoU>V_*=zS7jG`^FK#Y&bbq7!e_6j0VxDJ1_kx3@Ibz=<3$QaeUGjA`Uq}lK0~*5G-clVRv5mu@p>FaVRe@ zgNRWPAI!^ZZCDtM48t(4)Cr)`Zbv>C5V0St!O1w=m6zuc6A_CC^74cS zgmW4<4{!BDLij9{>J&%KKd#f^YTHzfgve zSeKV)R9*i8U+d1R*_*R~$MKrHyc%6X>AyNJPk9(f8udEzVjxickJQi8cvW6r9iEyT zL{D$)-iDhKQ$H_#8H6NS>HcZytNUIYO1UC0ukIZ|%*k?hUg)`9a27Ai%X2Ct9)?Tv z@+1gV2iX#Lp8JxhdmCYk^YYY8jbf2w@T=Z4KV~H4@j~~HGjr#bk}b&lajLFuCg;0< zoT`~y#7uB6FVC5}@laaM%ad3VF2a)g+!1wci6}0*f13Ctj;Lg$+nrYv2M^TT!MpPE zk_f3Lw?bZCUsjQbIvshg?>iBV)!>af^75R-A|O$_JFgzws1SsNZFzald8B^Fyu7@6 zDp5BM*6PlO`mvF#d*j^It!ZKEVQSTw|LA63~pLef!w|BcY=AH5mskH*(ZS{J+1zubEKg!=ys|KGh zf3p0M^83o~EWfq<`to@B&hjng=aomxwQ^kUFRv>vDHlpVEPbc+_0kthpDsOI`atR3 zrMH#ds8$wUTDrA#x^zS7Kxwc<)EdKzQmHhj`2FJJ#V;2hEq=84Q1QXy{l)u=_ZIIi z-d-Flo+=(HCPh))TI^K^ivLgnQ~(t~1yBK002M$5Pytln=~JM)wP#Lp_UvtUUbu7u z_{V{N4EUqKKMMRKz&{N9L%=@>{1M;}1OEWw1%0e>m*mjHh;@D~Ap zA@COfe?IU%;Fp140)7$r-N5exegXKM!0!NlJMi0pKM(k=z@OXI+EcLCxSBY+_2iN5 zBiLGd?da`at$p0A3zVPN;{BKR@7{O4Z+YMJ{?7Yb?~C4FdY|<^rB(_4#QTu<2j2U< z2fW|%-tN84dyDrb?-$jY!7II=^Al!{p?8zoW$=^UbG&0}1!2fby**y+ac`%$ z&D-d$^?JOeYCWOob$D~jKP>;C{IBJ2mme>Gt^8Q|uhr_p=gNKU4q}Km||%Q~(t~1yBK0 z02M$5P=PB?fqAWkIVZF^uFWxRMzuMr%@J)5Yja4OgW8N}Gpx-4ZHBbjuT5Q>nl@E! zQf(^QB--rLX0JATv>DW9w>G=98PF!yCekLaMHuJP;)n;yMYeD}FK~WE! zY+d00{hhAmKYaH$--P=GuDr_$j|de&1yBK002M$5Pyti`6?oPZ(BCUo$L?BNyKtA; zFJOJMU%>iizkv14egW&7{Q}lE`vt6T_6u0w>=&@U*)L#yvtPjaX1{>-&3*yvoBaaT zH~R&wZ}tmV-|QE#zS%EeeY0P{`ewg?_04_(>zn-o);IeFtZ()USl{dyu)f(ZV12V+ z!1`vtfc4FO0qdLn0@gSC1*~uO3s~Rm7qGtBFJOJMU%>iizkv14egW&7{Q}lE`vt6T z_6u0w>=&@U*)L#yvtPjaX1{>-&3*yvoBaaTH~R&wZ}tmV-|QE#zS%EeeY0P{`ewg? z_04_(>zn-o);IeFtZ()USl{dyu)f(ZV12V+!1`vtfc4FO0qdLn0@gSC1*~uO3s~Rm z7qGtBFJOJMU%>iizkv14egW&7{Q}lE`vt6T_6u0w>=&@U*)L#yvtPjaX1{>-&3*yv zoBaaTH~R&wZ}tmV-|QE#zS%EeeY0P{`ewg?_0_ym(sN6;F7VzL{af)(jK>$ zY4N`2eZ%`}@6&33!0&m#>Alta1+^dG4)s?AH+m!L`+wqX@RoU9>ihnGDgRyhuhicV ze7O8O<+qpbE5Am4pMRlzx_qL%zdWG+f?#cVQMtYJAEkdPJy!aER84|ED!r%lYwGU@ zURioc=>?_dm5!Fuk}Pd4ttxq?x$3V6zFGX6;%ABv7k^(>EBKY->x=gkFBWf7e>-rn zxVK1)8;i?}-Q7Rx{!aJTy1&rJz9Ei`5lGZ3x8eM-nF*)=JNAO=S#2azQ6m=I=|k%q&q4eEp(P@ zUa|1Q;ze&~*LMrQ)cp5F`L>7J-qtqWHl_{~|Dgh? z04i{$DbUl_yP{{#xih!aF|FgNeBNy3RtsxhIK2#fW>fBEDA#KRP^H*ue!Sk0iq0RT9&>oV)`1TuqXc)asLS-8x5; zgp#<*s+lfpT}@aT%QVT3WsRdrJ*`u9s}p)- zXcAUwRO6FlcY&)(np9PN((FvncQpxuN)$!ev3QOqMAnnA!m`=B>}Vq6YC_Y=;azex zQ3E#&6Pig^bTnZ+;gMk3sOxq#iPV@RX_B4BE=Lnp?oyTZW7*75a5SM@mF`m)lD%L% z9ZjmFrivqy>{B`%O=_yHa5asyChd+UjKyK1N?h7s)aGa+t2I?Yw3f|M^IS~=R^gS& zr^;4GlR(W;l1|;X?LMYHq^yDq{hY*$GgO(F@i87iH@LC;#RRH#~6uhPRjI zzQ1Kb%ScQ6m)iThdifXUe0|RHmXWrT^X_f^@Z5hXJzV@=aZ~q=U9T;Cc-|xPmbZ@0 zeZTqv_rLMsU8R;@^>j>n(zGzXGdrAYX(FZIc|$!m-+WL%M+B+3@#yil^61-1da7?0xxT1b=Sdj=p1KXK%aX z+~GTKJu`Mk-vHaucj)wsUwq4jn^RqHrEh@VJw7}zLubGP670s_eQ{oJJm<6? z9eeR;Qg@$La}>6SxT`Pf^-QWdt23NdT}q$$=3^*rKCRN#nZfBh1K)Dsi8*!aOQ!40 zWX1F5&ZuJaM4dD#X3-h@G0f2!;pohP-IJYhjxzXK20L@^=6JeM7O45o)F5_sMwP!+ z{h3u?%%`xZ`NVKXUqY$-vRO~-uItU*Z5m$IrgZ7d@Mv<(IoKvjy4#ZNGaNW*8_DJa zr>-FtikXkR;T>|ykw5qRXfiy_k=tVOvpRC!R-HP>kvqmuOUv9%)6H>axg&dWg@=6V zG@tHbV7|p4%y7v1^0jTLW0Vlb4O1O*GVLM5l@@r&XD_N7rh9x$9EK$_%<7O$TS=N# zCXU;pI^>_6_K@KM5Ip2_FMQc_SLq}qVit#N+q!0a;<)*#Lw?ERA&-yLCTAHn|2N$U zpYkYao@Er?gvaV}n$DtYPjCQ@k5s4c%~KsfuHLBGR4O3Ata|f=#@6^qI(=`R>ey=T zjr3zxK3}n0X3?7`IPAtpD%1A{E{9IJQeJR)x{I4CV1U7;xO$^NChARfHs{8EOyruX z0>>R~IDx%seo=u7xKny_{zcPY_ZIHr>J3qgIZUuwotue|aXj0{uFmY6zB6#e8FuFA z*zMEZDke!Z&7Bc`#H0eT%-np1~?y=aLSsb`UcIbs3ZMe0z_X!1GjK+~79pn)Ifck6L{4Gz?4TbQ znU9=8 zDu4>00#~R4fM;gv4%)rXrtYBiP2EB3o4SM6H+2WCZ|V+O-_#wnzNtHCeN%VP`ljxn z^-bMD>zlfR);Dzrt#9fMTHn+iw7#i3Xnj+6(E6tCp!H4NLF*%@aDqu`>JC~>Ox;22 zo4SM6H+2WCZ|V+O-_#wnzNtHCeN%VP`ljxn^-bMD>m#RdLVr_t&}w4p4qD&T9kjlw zJ7|4VchLH#?x6Ke-9hV{x`Wm?bqB3K9j9=|;FiJa*_*y+)&-DLc!f^Vcvz?aDu4>0 z0;m8gfC``jr~oQ}3S1=$Xc``J3a=_o;ffdBbMU3V`ob#i7r08UF+5CE02M$5Pyti` z6+i`00aW0bPyjiFhS7+e!UN}E?%;dOx&U$tpWgWzKMfT?1+FgzrZU@*Q)vH| z4LOCA?8+3DxkgUmCFB$ytC|Aa$SJ&V2|0y!Fi7MSUP4Zx9SjO(h@3(Pj}JM87cMSyP=PB;fo2jJatZ^X0M`*yOzwkjd4$iHWGswE^6~)>X(ZXOJFk|8xR8vJ19^EC z1ymiuP+p!8$%TmayYtKsBO(J9*YomRL{yNtmX|lwbU~}`yycL~D9y`TKzJoDZ;{)H zJ3mRW-j|n`lEy-cy?J@6Fyw@=J??zqM6G60n zb!*ayYKsSW?8oZz4EQ#ud@8N3ynyg%YhGTB-cWt~7I!}K)%Q5+1)Xiq%X21W81hYd zc@hNb@{k+d`Opt#9B{!lD)6i-fSkg!x7~T+lBqjrzh&P8G#Lc`Zs6|%{s8b} z;77m@fiHnCfX{)?fKP!>fFA&VC-8mX_XB?i@R3tE!3$ghG+7M%MZjMO`~|?D4}1^! zW#E^9Z|V-(F?OYJ3P1IFzgqvVKVNFr1&~wttWLf7%TNJS02M$5Pyti`6+i`00aO4L zxaJfO>bY1L+KYipsP##~X4yZ7Gn*exG=WX=Pp`ak>e ze;nQ}algPdcQs;%Pyti`6+i`00aO4LKm|~N*(;#G&$K+b6Tm+X{A0i$1^!Xs9|8Vh z;2#41LEw)7e;D`&fIkHM{lKpSzXtp&@KfMdfS&+=AMlY=h@3*RU%*}qX1{>-&3*yv zoBaaTH~R&wZ}tmV-|QE#zS%Ee{VS1E_#^(+Z~Sn_$~k6T06B%TpEt23Du4>00;m8g zfC``jr~oSP<5B=Qg<}cS06B%oDLiu;IfYt-C&?)cm)w-x@wMl_&8!O`r|`!$v*YhY z1yF%&K>_3xo;!1!e3+eT(OMgEYNm|!i2p!Np*mx_5-=r^-~pNW%$T-;D(vexGo<;z%zrKLgW+%vY&=)DML=7s-o?{Iw7YJIfWBfhbm`nPzlH> zoG3^&&`b|eK6dQVj^Nbn*+CPg=l`wrD5}ttFPpAje*Jf0-OH#E86G$vY$-s7eG$oweXcC z_5l?@1+G>Fno(59DMU`8kDNl}6bizdX9wgIBB#(tPGPJdrUE&I$SIus%7-aNW8lX; zh>=tHggJ$8f5+IZ9l!GX8?o--)%qTR$Bhc00;m8gfC``jr~oQ}3Oo%8=-PYs8%JD1p)E%_GsXJ(WQ+Lq%rtYBiP2EB3o4SM6H+2WCkDS7ZZBkpoc-R8`&A{IT{Efih z0Q~j9?*sli;I9S#8sM)6elPG>0lx?MD}lcP_{)L64ERfdf4y@GKlm#TjC6W$TxHe; zkW=_HOsMz~r~oQ}3ZMe004jhApaQ4>Du4=H#|mf~-U)4vYjaGSQEiTDb3~iN+8om6 zpf)4g3~O^hn;~uXYg5;zrcG6wRGW%6i8lMR*{jVSZ3ea3t<5fN2DFK_iL?o|k=h7t zxHe21stwU5&}OGLzBc{Z?9gVrHruq>s?8Q{Hfys1;}6Rf^HK$Ah>?*{%Z;12*l z27Uzm5cm@K0{9&G4EPlI1o#2)cLLuBen0Sc0Dn91w*lYm7qH{O>=&@U*)L#yvtPja zX1{>-&3*yvoBaaTH~R&wZ}tmV-|QE#zS%EeeY0P{`ewg?_04_(>zn-o);IeFtZ()U zSl{dyu)f(ZV12V+!1`vtfc39OPT{Z8|M9WHL*M=_vo3&~LcAZK0;m8gfC``jr~oQ} z3ZMd4z5rvqcAuMgCj6F41+^3I0%Cg7!1SU01Sp;upb6>7}Q`;g+U5~ z3Jj1_cyX*B3bF$R+hMQ`23ujU1qPd8un7hmVXy%P>tWCbgLN=i3xhQL#+5eyc>U;zx~!@z?<83rX76k*T}gDw~pV9*JJ4j8n< zpbZ93l2drFSp21*f6@Ean{@%?6khrF06aoe02R1$6+lj51_nV+VfJ^dQ^!OqKne1s zA;&@0=zUGO%Yc==jXCg{dm3^K31}#rh-)} zGW$h498Kc5mWV0~?Wb&aG?8_Zrc!X*WSgUjsB@Ww$SK_1GYK*^Rm)|Pl3G2;s+O;F zNS9C&S6MYfJg#*$VQDPWMA#Fs#?hpn)~PzxS(DX{CPA$dR?{FO#`QXy$Qq-yB*_Sa zs~k;IT2DkOvnD-`CRH_@Y5{Tz8wW!YM3q>?+10qvAzfIdQH|F#br-mrq)C+#nVsqR zt|mcHiJ~aGraea!BI`+5VcCtL>}Vq6YC;uh!;ZR=qlp^0VVKZtW+*zEFrM&8Ag8c# zFgz=q!k547S1a%GzPriIDeU~`dH1${c<#TH9xi^bxT*WbuGbbmJnxZt%Uj3hzJKof za~{sehj&3Viv#Tko;M(+0Xc;l?I!}C1raB4I5XPMv<(BwnS(~r45Jtyj%PSC;E8U^ znF-lwQnA}=8pt??AW0O7kn3k!D*7i2f#B>(Q|n z=V4KF4SeD&h8-cK0Su~HNWu>S^-4ja8OA917)qPRsC13d!Rb2#-*Vs>J$36#rt3`L zMdXW_Ra{n8 zfqCi$r_T)cML<-4X4Mz-Daa{Ijt@Pb#g(8+D#Sj^OgS0PJwKWZJ7>QMdJvUi9L9VG z$uGB6r>^;0ppNm=(lX~%Hhx^(qcm3kCf_3gTJh9rKHbG&*@GmW;gI#^drfi*$A@=M zKg+=N5;zua96Bk9ic+r+)yAk5s4cO_XytJ9{x1O^)TB8kQkFT)i=rYcdl|dE#;~WDw~%HOMJU z_3bQs9fX(7xfdLs?%Ysc22x!far3#6zIp~xZ>qC7H}+#9Ttywo|>a$w@-Jg zAbuRH+nWrVPmS;+CKWkvb`OE(V?xKNaq<2{q~`$5`%Bb)ujv)iteiD0FT*~aJ9}5& z%jkv3ZMe0z_Xx0Gl@(; zj>bNrf-%WXxbu-8vN(#QJnqh`2l+q>8j@pqc}j#@Gl@psd8w9FLdpm^y1F%KfFbGQ zlfDpf$oY}HA6IKKEN0hcWuHs_D4w7T-t7aP&myu4Z<57ozSapxnoNm!9tS;iMqB#6c^n; zO?(nZR5H@-&LgK#JvQ=~K4DJbONT~&a@V`RnPA<)XW@|_e+4Rl3ZMe004jhApaQ4> zDsZJLplizlfR);Dzrt#9fMTHn+iw7#i3Xnj+6(E6tCp!H4NLF=2kgVsk* z;lvnN1g?vPz+V9T`M~#pUj}{&_(kA%1HTLS1>koAzXSN~z;6ToJm9wie{NT6Pr;sF zH4Sy^sp!4;Ja)^69+~sNOF#c-KkB-l%s1--$SJ&1XK6exQ~(t~1yBK002M$5Pyti` z6+i{94h1x*4>^T}Icv2LYqVLdO|LeqwCT}ir8XC&d4O{X>;+O%uarp-KUTD6(m+FB^6fmGCkDO(rV_-OFS|5g0u z^SEE&>bS)4C{Y1a02M$5Pyti`6+i`0foDVk{k@{C)nN7uSpPWq@5g{Y3jCwMKLY&2 zz&`~1gTNmF{xI+l0DlPh`+;8vehv6l;HSW^06zi#KH%>KK5_~tc#X&@oY2AS7qHib z*)L#yvtPjaX1{>-&3*yvoBaaTH~R&wZ}tmV-|QE#zS%EeeY0P{`ewg?_04_(>zn-o z);IeFtZ()USl{dyu)dl@N_rm2)&*`_`Nh#i-lEr;bphlQJ|j~sekdw{3ZMe004jhA zpaQ4>D)6)_0J&tyDZDtg4+^pu276#I2!q`)*ad?D7{oA$U=YGU!a%@)!+^nn!hpaa zfWb}}_%J|D;l(lJ6kZ&AIyi-YbMg(p_xXSN`!!}=06B$E>vWADh6xSnQbRS zRVFtp(0rmfH(9*-ctZ{rc0SgSgPgn3h8(0d9Bs%^7SQZijx^^ckL7S~ zDrRM$d9X1Dic^m?OT zT0;&NKd(0A)VZDPL)wso=daR`gXa%9g_j}+qe_IT3R_so&UEN#LW7iY8fGU>I+`>v zCWNC&!fT4?FiA$^jwUImsVaSyQT>>siK0=&HCoB=UewWqR4SBK>KW?|Ifcinrm8q{ z3KdkyF6buM=`sgYWRv21=>=4he@W~z$t zXY1a|VHB}suWS`REXi`&kg{x_tHEDM=VJr@l zK>dHBRzaJiiLBN_8rLee$vjt+fK_;9@~N`b(IlXxF6kuBcJ8`1#dauU0BJMt)~k#K zOlD(OJ*6*H|6i2t7g+f_OMYAS?f-lpr?Blg$SKT7QK^HNkx~G1hNz75(M@@%3~tbV zT;Yl%)<9-KPN6F7>wtogW(M56dy!Lk4RQ(<+V3;dUU2Zu7hZ5zB&TqE=rVOtfeQPx zsrlULOQ+AAzBrcV_2zu#QXU&`f`{g$F6!xn`sz-|XGJ;HO%($`G}F)=%)IeXX6&HU z#mqBzn}(M)cz({E8O}ph>5}5yS5H|q2xmg)fwqxsK5*(nUxs4lBX4+zoO0yIDWrlr z2vdYLPs=SWbCc})bq$jguCzdvXN#Ge0WLK=mK<^lgCG^BW=XSbMwB6^5IKc~@n#|> zTn>d7H*yNufSQ@OR9GRRs`s;5%wp7Mf<$V?vH30+H_yA{!}}oKvkC2|RyWo1zJ1+< zrv^ENH;vs}66)eBhT+Ul8VzgS@LZfaqh1>8c5DCT zgIQ}2hWm;NDPlewz@lyIdbp!u^YBq?0=mY)zu1DB6`GSyDEbm!}FbPieH1gXnRy?xez4h9p|){%PqeKCdSAt;owO#%&OD zvfP~)nn5c#i7LsR?LZF@DjE-FHgkj^S-U9Hqq5`M@Du4>00;m8gfC``jSA+t(-k$w-899Z>DMU`; z^~EXt!`~nOnQ#1w&&;|2atg1ADH#s}6+i`00aO4LKm||%Q~(t~1yF&jRsl`JLr&pU z#VP!Y@9rCY?eA^58TSiZwO1V;I4Xb&paQ4>Du4>00;m8g@YEFmd_Lq9PH^dvQ;3|x zMc}%)mN|t7AOGpouRQhJe`VGMkW={7=TrOyQ~(t~1yBK002M$5PytlnnN|S8>X1{2 zoI+jEcn2I5+hMQ`23ujU1qPd8un7hmVXy%P>tWCbgLN=i3xhQ$@$*puRNz`xU@DUiIfeFbRFP9SCu32h$SJJQ zY8Yn4v5`|~k$nnKL{n8gE-M=D08rIrC90*90IHpiCY$*O{K_SSlb*;kW+}9!Xv4@0;*~_)q+WC zC~^v|E(&FcoI=Osftj5UN=E zL^bEB1wj=xjmJJUrD(|vV3w?HfSf{AeV+KHym#opqc|8NxGmR)lo9FGn%)CoI=N`A=Gtl(&^7ls0UWI5(KW`b1!_^ zbob^-07>(?k$$Yo=Tk8g@l}sk`!TKu4dP%!kB?NQ?+tPaji?sx;yO3TDcsR_Z0zi9 zcbq$X=dEYP)LWN}_a`Df&Nlauo~_>Db83S8+5G{cDW0~~S+kyD7ALgW-S&kp+O zmHCXwFr%zj>; z#!)1B)bGwK+?ZN}Rv=E~6e6c^>f1Im97E(3DoW!b2V+&K!~B?$kjD!h*Oo~Op(%}Q zLEev3b!{^_-~HoM5oXnDr{G>*o-?%|N@+PSPhv^92up6pB=IA)E*%%$KTUiRM^rM> z?am{o@JVtC`78eD?)7rzA7I_VEB$=|j}H|<1yBK002M$5Pyti`6?g^}Ku+P=+wQz@ zX)n;h)E%^2I|jjj-wpg-z#jm94EzZAA@C*e1@JlW8Sp9a3Gf5p?*zUN{C?o?0RDF1 zZv#GZ3a>9t;SYa~{OkYusZV{ytP3Eg@EM$F@uN`zQ~(t~1yBK002M$5Pyti`6}S!* zKu+PDby|b9+N{xLwKl!ltkR}Oo0ZzE&}O+d%d}al%@S=EYqLn3h1x98X1+F_Hf3!} z+7z|v)}~9Ff;OGnbZFDAO`A6Jv}x65Zfk3yphjU)kHq)h^VlsPdSuQ6f71W1Ghh1M zM>gVqf$QM<#4e%&r~oQ}3ZMe004jhApaOXc=3>Ly*0SxBDz=J^<1|=92VbBeOE*KPG&aI&PRwzoQHWP*pB#WZWO#nD6tX;9(yy8V>RjwXR*6;=PN zVw-GoG^z2RPQ$viO*T53BvMs(tk*Kc;|50)btG{$S?6eyP!d;JHPdCSs|ibEnI_q>tZ_7{r**23lv$J2jwV5^5?0e7wa3!yXd-Kj z){-QXZk3}+O6!S8W!9v}(F8e#7cMPw=z^TW3zxbbx*(_Unc@_F<6qC*{YO8%sXi^I z@SjT$7r$5B)O};uYYQKq_sG2Etz&cFKllAP5AEZ_yCBS@2{MsV4RE$|45Uk(PliU5 z=eij{fu`Zblo7_+)PkfMg+}~9M1hQEB2Q#(6$Oqt&=bfh1W)o)&P>SixF6h{nI8|Q za8R*KRw$N9s#qpXbf%N4jH+{(oqNg9==kv7>BlJ7FEx%a`kdPor|P78jHJEDoGw6gh=CWAxOmFBwhhxo0L&b#qxDXv6HGp=Ohn9Qdd&rGg=oGOsN)?%z02zw1C#p&hW^)IAN~Dzf3<&NzW{OybJjZW zKcfPu04ne-D9{Yt(T_aHDMU^oate`C*gQKRr*J?7`n{5gAlmL=t;GXeeWgSs5BN5x zd@3zS91tFD&C4sqVyHfTi#s1FmKss7v}|);o-?W5d-J8ac=*e3G2PKR)@{e}3<-U%Vab4n7Ny z{P-(S0aO4LKm||%Q~(t~1yF%2RRLXV&F+^mbqB3~9Q^lVz#j$vQQ#i|{$b!B0{%hZ zj{tud_y>SL1pNKLuLHjZ{3`HM;8%d30Dm9wkyALq=R;26^~EXNzWUP}|G4`HuQBTa z$SJ&1XK6exQ~(t~1yBK002M$5Pyti`6+i{94h1v~?}RqTwK=BEs5VEnIik&BZ4PO3 zP@55LhP64M&5$hyc4)I*n{C=`)nlDXtQ3MK5ecxPGQeGK62^U z<0J3F{Q_6VC5A_d3ZMe004jhApaQ4>Du4<+BMRv6%xygY6h43Z zEx-Fq7jAr$SrOjBBu~Jg;{0m zC&?*n^;XHh+5g{eGwTA#DSTR|Yy2=&02R2-6hKa4##}&7q5YfO%G5ED!ZO!MLyl7g zLa{%$uPJxAJoetk94MW=ry&QK7lREsSa*MSV-9eGb~WY{FC{z9fyNv-me@g;LQdg@ zOT?i|!~{)M^*DR_14omZtVFexCbr2=M-$F?wNk5AZ4=+oB&w?-ZZyqcd;N|k)l?{W zoy>mG4o8zXt|g+%Li>xhJDSKkNmD7fZL-bLMAW%VR3&=b1UZEkS}BoJcxk0$yu=lf zkZL6xFDo2P0#&6jsYKZou-w(8MrswtG797}M-w$LQw252uEwQ~CX&&pl9FtAFL5+U z1+N66O6>4n>}aAOgsSMI$gU&g6xu6*@q|Z$WpqsB6lOC+z$&~#vb%4qV@L&xr7G#< zZDQ`aw%*M>h3vs>C^i>)0d(UIHeCUxm4{Un)JAcys>#ukv zatiP08$eDWc%nm2VRq#~C6H67fT06|`f;GBLUJZzO4e42h{ess6xj?9>UeX@XEHRK zlOP7)a^RUkP9d|Wh6f>!!x^B96K962D$p*Jjf1&39!gDh+MLG)HFp~tq?FSzdzgaf z=iHg$JXF;LS4TZ%Q8lP$IBw83lFcKEI)>DEC}uu#7FU8QQ4gIH^@XM!`E$>YCc{q1 z*2K|JO<;=otd3l_jRFVek~%a;PGM+yWf^-=&>2)lPGJ+V_bJmXo9S_(s=y!fD4yNf z;3A@eIuK>V^~5z&h4i@Q-b6VMDQ7Pxc@qp66hcih8AL5(;f21TT$9~-6&c5Bm?xF&y zz!jxHGm1&yo{>|CoI>OjBB!u_s*I60?QPwPAZILNTQXF zi^pUc4dPJB6?yqUQ50iNmb>%957pQGoW;xX@|?rjHnl+9h;niiV#icqQsUKGdb5m+w)v}F4Q?)=~vUhs=I zwtv>F3ltZ$c;ELP_rB~s>V4FE$a~Pc-@DJd*Sp)h-5c{xd565j6W&&@*IVGVmH(ss zt@2mOpD%y1{E_ne%I_?{wfy?>c=^uqE#>ExN6WQxT<$NgD=#S*ND{HbmEKr-Rq3UrTT7=)H9{_(R@O|L-1Ahnbw*!A0@R3t^ zeQ^r^zWqJt#&&<^V`g0dIfYN(yo#TM3ZMe004jhApaQ4>Du4>00;s_CsDK9bA*Zk~ zXRQ`ujW(;b>D6YHHa*&`)MkY?%e7gi%~EZaXtP+GMcORXW`Q>IwehqmYg5vus7<#v zUD_11>C~n}n|5v5w3(+(t2T36TMGp>EQ@+*X8Q&F&2Ra}JI;6hChixw9xqtzGb(@z zpaQ4>Du4>00;m8gU=`5c6(gtcuDw79vtPh!V)hGI-|QE#zS%EeeY0P{`ewg?_04_( z>zn-o);IeFtZ()USl{dyu)f(ZV12V+!1`vtfc4FO0qdLn0@gSC1*~uO3s~Rm7qGtB zFJOJMU%>jvDZIWoh0CH3cKpIWEFflG06B$tTR;U+0aO4LKm||%Q~(t~1+IJrkW)C8 zKn?c6U@r{zz+eytyJ4^k1_LmNVGzL}gn@*CfB}aAg8_vBfk6O+oiOlW&<}$hFxU=* zZ7|phgDo)F41-ND*a(9SFjxR&5(X<^up9=@5C#iiFdqgU49YMl!Jr6(ZWwgIpa6qT7<9m(9R_VMc#@pLcl^V*PXFNU ze|)`J7eG$om46SwBSZyIfh$*msm!*Mp(_2GaV<_X=O$Ti#~X4I6x}}7kb|7N(S{tP zH5_foQ5MkbSdKL3CXeNCV@{#-vY$KDm?M{S2OD#s4T{sRkW+X8IfdCpqELpFSi~7MbfIIsgjE{VcwJha&H`7HG^sKog>5q5 z)g%ZiQ50p&M9ixY7bg z{A@9EGu%if4q4&AX&{N?MncV-8s8#(7D_^9nu$SE$2p)bzG4{a>tlMBvk29% z4lCKW8}PVKow;c=sXGVO1Q9ukiHP}Z4xIa@tqVd;!{&R4`l3zO7?`Xt@W-!v?$|2S3OP^i^T!M(gZS33hjO3Qh9 z5=+8GSaLHai65zT>A2|rY2uSOqLPtrcRuiA9>i4guDrY?Lefwc^71qeqDVxYj=bUk z#(qR&%EPE5FV9IV0ur^m^N}A?AqWZE^75SXNd1m^dHH}waTv2!cm7Fp3jbi;(SO00;m8gfC``jr~oREs{mlsA*T>Ig~%zqzBq-ix%gjwFDkye zYSsmiQbW_`Z+Om*4se zxL;rv;~Sfx0;m8gfC``jr~oQ}3ZMelssez|XZ8!&KYBh6{`)cDj{^TF@Q(ohFz^on z{~+*3fIkfU1Hc~w{(j)sfnNiD75FLeE5J{HzYqAxDV*T*A*b;A;uP-K{^$+&o&C;Z zW?cX|h1cpVkNrXgPyti`6+i`00aO4LxRw-vT(T1|I1YnjFc^iwQ5YP7!C@F2g26!; zjKE+R1_xj;1cUuBsKcNJgDMPC7*t?@oWhG^$SJ%yhMdBSV^0UC@YKNfJ6b;c{J%5n z0>~-6mhKkV7gPWhc(xUo%Bw?8p{?nToWe;~UkW*s$<*z+s^L@%Ca=boj`0$! z%b8Rw8FO}pqe&1(QBsMrD`2^+NsZJhjAb?9mpPiKftf0(Np>|Zbu^KTMwOIg!+VLN zNh)|H5Y=o7TI^_|AcR4r04nx6TI6nmoI-~#QIIAiO|sM2slnzIe8j(RYjk6~0jwX!7VG^kSPt+=Cb2O3FT1ews#Wwk| zaSC7d3m-c=y!xwmBBu~Jg~%zaPV(b8V75b&2yzOMQ;3{G7Z-FWQS6|Gmo<3hoI5i- znjD*k!K0qCXb{eX&YR%%n6{B@rfX5z%-|VEP9bs%@4WTQn7YLio>PR|$Z$hw$Hf6ee;mh(+Yx*!OU{E#yG4;8TejKVA>|&;w6nxAx&nbKnnZ7qN zkg95ZJoVgYTAwf#=ho*U#8nukE{2a2T{ zGB(2`Facc*Xv`J)p!t^V;{AyziJVI0kpjLp2u$a&?9pm`1eB_e|z-@ zKJf#yE>K+1;(gzH-21ZksP|FtA@4!&e(yf-Uhi)2c5lo(=rFBcyzezf>d@xkK# z#rul)7Vj?JUK}f)Djq5(MN!;Z>{SPf|4;!`02M$5Pyti`6+i`00aW1WQ$W|=vzwWb zQ;3{GDH!8n}Rl-+H`2su1%XZ^R#KzW^QY1p`eCkQ4h^*zrZKv|Lzyq2g3chU*LMY zV6o4r04jhApaQ4>Du4>00;qsh0I>RyQ#iq>+XFNi1paQ|?*je+@MGXdzz=~hfiHm1 zfzN^Sw$M!)%_QGHf3pZehU|K6+%AgA!kzX#wEq5`PEm8-y1 zW*c$}?cb;(rw}=X>9MM-kTh}%tuDwZyl`o&gWV-~$XQG?G~E_Q6B(pIh1WAk*k(tQ zK(b00(u@GM$)<74Y{y0-+D(ARee9L%P@n0 z_PLrQDXG)qT_$aov(CS0#F7BHDHHq`_5LiPVe+20ZTU$1<8-sfKW=6%R1 zL{1@c3XxMN`)RnAvH@KKpZJt1coSh$>zHcnjK(A>s*sekK-5eil_KUq)HtAW9dB;= zOiOR?iXPIFrVd0+3Tf<+O>^qjmrMuU3j8QkfI-^O84Y&gen=#ZrI?{JS`+&*j5hOj zL^BDgV|-|E5`~l$#{09Wd6I{j*PHW|OQXrKlj;W!qp>#=r$!Nbc^uBrnXIX#Ld>c! zX2t*$&C;1p1dxdvkaArA7|f#ps+U%9T`p7G}k3n z$U90K@NxQLBfbK$iI~l1%G7P8Vt_e-YrJ`u86T;^&XA@_=BbX7sq?>{GLcj0I5n1m z`806~Z%*>CuKKFX;S}nDCj5xdFb-zAon>uF$fe^JK!|IkRi+9okQZQqeQR=G$}ZI5orj;Hhas`=wk_fSspi`1Ev78pMxdb$e6GVF3lSTRnJ? zQ+Vj~i>F@gVW>~SX8rK$x?h_&auE_D!%DW|2k$PY&fJvu3_J1k5XD5qe5P9nXzO~o zqha&#QELLa#=vBKfp^^d!Potu_VzDK>=!^zq1Fcfp#rD?D)8e`pqWIbp97Ioh@3*? z6e6dvd3Mkbfy`&>QB!@B&D?qQ78#L1J&@A8yc8q~g(P`-8cNR9D@|TLAOQ=h*y+v_ zUvkOSDzl%Lr*RZX9`(EP3OAsh=~q85eW}*iqLuESmcII~AXcy7EAsMz zI=YyX00 z(0`Jg!he4GUzd{)j=vD=4*q!F0`Rw@0;m8gfC``jr~oQ}3ZMd4gaW$eoZT;joI>Oj zBB${B;uK!^l6cF{kFL7ItP3Eg@QRp{@gPtEQ~(t~1yBK002M$5Pyti`6}V~@&@{Xg z+8o#Bm^P!@9M$HCHixx2q|HHXMzk5$=72Us+U(b+u1!swsy3-M6>So2_GzW|KA> zwb`J}dTsi&x!O2|-rkXa{Nx{>?8E&6SM61Y2aXD$0;m8gfC``jr~oQ}3OscM0HY2$ zg~%yHPT}>%DcmbR-1F^Ew+xzf0pt`u_4yP(0Tn<6Pyti`6+i`00aO4Lc%~JAusY-v zBBxNp>UO|Eu^k56V6YViTVSvm2Ag285e6GzupS0|FjxnJwJ=x%gViwTg~2Kq^uS;x z3|7EkISiJ;U?~iiz+f>97QtX43>Ls(J`6kTrb@S4RBe)OOIv?S&-4tApN|Tl0@tzv$SKUq zp`TRsk0&c2Bc~8Kg~%za)HCW9anPlZQ;3|xBiThFkyChSrQ;%rDtimfKV@o_UoWjrlk5jvr@>kc8Q;3{GijvA%)6%LQ&vSau_F3a4f8&>*FphS|duWbm9jGn@zBn&9fFr!1=A zmosqaK-)+*6P&06eK{VAndw0+t^`%09y%xL3r#ul=bq0aIZYf5atd!wZ@crtrM>}r zH*yM*Q`jHq+ekKLK0(ms_(&B3*P5w|d7#UisRGqjauJfuH}FcKCYeUyn)G?h)XUjy z=yAHO#=|=3amXo5kyB_Gl1$S*6d|E$q$#!latiy7jh(&ij&q0ay!FhOdh3dssd?if z%I?^S;{7#27Y*-^$O^57VfKDJ<&J&!uDq#0-?4?yLaFc{vv}07O&wP|bshIL!zujf zAAI6x+BV(rR z6>Ew{k`24_T#>qjT9_aQ^787vfvO`I%FCOTU3qz~zKxI6Lt$Q?a0RMUC^RSNZt_VoYX0$wr6CKJ4)gMM z(M#s#?b4EP=b0~=q>3fT^YX|kL{1@c3XxOjpfoZ+W+deCLdV>pP=3r8LQ@*q0>{*6 z@>DTMnVj$baq26AXUqim^75Ri8xN)BygZ2|;UX-#8IwdYKt&W6-9Jry5=T@r((TSG zz$p(De*pL~@FU=dz?Z-mz~{hcz^A|`zz=}G6Zk&x z`+>g$_}hWM4fx0@yuLVvZ+YKMfAO>9i(Y8f1&~wt3{JH8(Wn3_fC``jr~oQ}3ZMe0 z04jhATn7qhP#`8J8 z51zRA`yagf|GUYo3m~WP%9-o&NKgS(;ObKVIfck6JXW1jn7cA{Or!$v8VI?fyNy8eX#?y)KEw| zax@X4s^b_|GLBg2Xwrbbkd7t|j0xdrlJHs_N2#>kQD^RkKZ)qlv2g6xV1a zLyb{K6H=*Aik!mU%{`OAPE%D};oxMV3SwGyw>qQ~Jmf5Aot==T&8W2Vv2nc}vTjQ!Tx{EArToX{`Rappd6d^U(tK`&9}ybHftba)(QQ?&dMTRcBSKu~rZPQ45;=v) zDQx09>q&t70gV-~CvF~SF5Vw<3NPF>^~#=ty_w<425p7o%BErSm94&j(=`Sr>kD-J z=hkEY_KA=E{KS3%&Pi=O&eHT965!|M@}Jf z3XxNYoWh2=kr~h+ate`CxVkkhG^{iz&Z1QO$`HLg|O4@07k?`eNzRrH4x& zD80M%w$d9*uPVK?bZhB!>4wsQ(qM^{HkMYDN~Jl)?-w60e!2K)@uS6uiVqg=FWy(Y zw|IB)_TpIaRPj(TDT?COVy`+-{D%sl0;m8gfC``jr~oQ}3ZMc{p8|kUXX*~xz0b$N ze?JENQQ#j1{t@6G2L2)79|Zmg@P~na0Qf_|-w*sc@N2-Y0zUU{g%f+; zkyChmaSHE$?jzq=_sc00;m8gfC``jr~oQ}3S5s0Xc``J z3a=_o;pf=)`r=LJ264Z@^?1QzpHTr+02M$5Pyti`6+i`00jq%i&Kx;~cbWYHR);-6 zlR@C`2L3MK4*)*~egymw_!9U6_#F5Q_!RgA_yO>D0^bLIKk#<|e>?EE0UtSq*B7U7 zR&5(X<^fSkf7%_;m(vFJ7b`9RlyH|qk( zDZKLU0eFO{04i|hDlnDVb~02Y`7@-?iRRoS>+N_$4i{yO8=O&NkaAQuP^Rk~i)R-fea|au9fLAxtkRwW#>@$a(a+g2zz|`D9{p&I;(U2oY z?BBuepPDm;`!mp4-I2?R`qvtA5$Hp;A*asmWFOLo96W!>DV&O?Lr&p^OT@vbikP5j z9ArZ=a5SmON>oc}1}xj@Xu=tvuG%rh-)}GW$h498DCO zF%b$kX0N#IjwZ5B(o~9!Mz+n-1UZElE+MDzSk>Tcs%kjZf=O!VO2>GKDpz{4MW>ubK{}fXu@)PBm7tjCaGxd zP2f`=&?uTsZ|uh~7t77TPU?C=i)N?b@8+C0( zYD~_mH|Ap^7f>yB^=5eb-oUkwDZRPGrteL_LmD|I1$AynAk-+Nv+9lcn2X-=BSKCTHgZ;-F(1Pn3@IXA18x7z zy~*A@ru62;FP?6oai0*W*kO*#Ch{4l>eS3;zP2ADU6+lMz1i}yY`%8!PM`9ve7fr;^dogEp<(mIuX>{%d}6_7GnLtoiJE6J>bhKp zM<+XzIDon*;kr8P%ou&abeBu$EA{o=#C2-atyIkFCSyN^xH{vwTE>TuPTv{$MF8x~ zxibf+J2TRc!jLlJJTn0!p_t7bGy4=*XIxJ+!$-2tB&qAnz;7;KXU2#Es^&jNOJ4yH zL(ZE|OrY*QJmNt#i%B8-6jx`QJk`TlXVSdRIA?%!XGW$!Gb|7+W@=5U@tq*>)uVOD z>1-a&q;EckhmMPeHs52^6gP!ac*igO+=ssWufKZ3#C`$f6kdlfz1U?`02R1m6hKa4 z18Ye?=T5F;9CXVwxiT=4m$&cQ!|wd#+v|b6ynP`Y%FEkVto`o%9VFVEHMnILg3 zFR#8(qJf~uDMU`;)CWclt42OjHrzXrQyB9=eP@H5!Y9Znyz8ZZ_L-Z1 z<9Elg?%);kVt@yN3ZMe004jhApaQ4>Du4<+`wHlqbM`MpkW)Cps55m3ttO`Kp!H4N zLF=2kgVr~72d!`F4qD&T9kjlwJ7|4VchLH#?x6Ke-9hV{x`Wm?bqB3)>JD1p)E%_G zsXJ(WQ+Lq%rtYBiP2EB3o4SM6H+2WCkDS8mi&J>#L*;MY_IoeC$E*t=r|{XIfAKe> z0;m8gfC``jr~oQ}3ZMe004i`TDS({9IqS3rYqeRU&1!9WwOOT2k2Wi{S)t8xZI)@X zRGTH*EY@a`HVd^`pv`=3JZ;L_l(Z>o)2&UHHU(`uwdv5NU7I#-=4sQa&D_@3g8mzV zq8@TteSt4t{OL#T_{_g{;(mc^=|aT5paQ4>Du4>00;m8gfC``jPf!8Cs5AQoY|V}1 z;J+UO{wVN|0{;l`4+H-Y@DBoi1o*?iKLGq8;O_^19r!iiSAm}bzXJRO`1^p5oWcn{ zA94z>FHYeHw{Lr0`(4|9!mJA*r|=0*p4c1}Km||%Q~(t~1yBK002TN#C;-`PCtz?K z2FGA93WK9CI0A#iFgOH*gD@C@!7vOCz+eal`(aRrK@A4~KYQ;2C&yLY`AZtTM(T%U zS+?~UJ;s)0J*HIm-nzG{M$hi*Zb>tGjU*J?gn(>~A0(D-SvIiXUFM&_#`%Y232}fd z@Q?Ek0RkpVSdy@W{6n%{Fb)JKe;_0{#MxyN6WBZ!Z^G_E_S~AP)|u{Gt-+D)k>=-* zeLm;-oaw3h)va4qw`g#!h4+txQ+WUQ0)Ar5=RqG2<}K|T=+D~< z)2vmoGgtn;!2kS}#^dw04y;Sp1;8nM2_|*?N+W9B)~@t0O0Er}6A>NOyMTY~$`Mx1%$s z;&yJhGsm_ic2JbSDFmnRNRpxHz$pZ$uss;SDLiuyoWg8oP(dw-v`Vro&{D^h1f0Sb zic|Q8LkAE2ocG|9=Q)K-ZUd+AvUy6uj$9N4z$v6#{Y1}5PGSGMz$u*UiNnc0;1sH1 za0&@`2u`5`h;?ak3h5H|=^T^iB4VX`GxIrx6C+b_i(N3h!ri#@03p|I=>d;);ubAI zHu7XFW9?rEx0u%wXVOK&bn)ZBDYSP4r@<+ts|-OG!6_U(Hh$YZXHOq~;O<+;&khbt za0=~ue72}D<<{&{;1oU{zx*AiKKT<5PQEVyPT@s)jKqgP0Vn_ko|gh{P+dmcHtK{s z-{xK)cjwzA>0?Ft49+~}&gZ0Jadd58tpkQ+L4rB__ejx?XHee5?!5M5qm_~RP*L9a zgpJoRIECGuK9XO0F@eHE6D4l_q(>mPFbv~*QC?{gg|V!;^TLZ`6VktXi}FNVkG0Wz zit)4A4#;pRiqbMIlN=L->E6R(&7ev60 zyN9<00TKr@H1zlxb^o|c2VN8l9S0^X${R&5f{=hkc}ePqaiHCKn0vQ z1O=b~6o3Ly017|>C;$a64+U)Rz3j_Qa00mkYBr*M){x0`9Qi}^d5zk~V1%#WELF+XI! zVZLU5zhF|co6cy z@Idn*;DO?SG7ov_ltg3Mcp6; z8={`K>er5vTF{K+C^6Zm1dbA7M<#)enpuf*lu*mSxGC#lRw5lGqF$G>-pZ+4!cjt< z5oJQ%oWf>3n=$7*W=!L&dQcZR z>Y&e2;!DvovVEDDw_(A+rq%tYZ@tHBpK7g!Qob_z)J%7@%k6(3e&g5f`oxoSKlI@#|>+zU~5Qi=i?&TICM)s7Xi zy)^ZwPy73y-$J`LlVm@msJFcdFR(Y3LXdroaqNxsw+$XTb??1*ow*~q=K;F+9hMJG zjE-JlZ)j)o>)jNz&jYuQ)#^R=Mk%9I7`gUFNk1ac!L;@!{g}u&j!o>^n~~mo!`D8h z?9DmVdvE-J0E5)Qe#%IjJT}x-o<^aZ*50Ha15V-h!zsMuwMTlLHi4&zia;mM)I&3g zxuZ!y$5C*BLzCA=IxvxApqcK0Mi*aj3c)E9UL*(_W6T9cn4Oq2JWZ&(N0~$%k3Z1zbHTl`mb_|8Wuax?<)qq^YyAJup7q zngvndN7FcG@=tNh%2GMbnbE`f&Ll-U6gY*u|5B+}{NT&KG5Nj#IEAzP0T{av z1)#u7sz5i1%iidLQwUC>B#>XjJv_iE1g8+3LO4`C)Ltwl_j z%4l;@o)C+n_M;o!`N)d`K@TNrQ&B!phB%VJ4Mllc-6f2t+33#G!n29}K&$JE@=Ax` z6h<1HLU0P(U)fkjBLPkWpT4sRmN?FwG;e67jUcT3MR|K#MLJsS$Oo2>8_8G_z;#hk zJ`l0?MYPbJr}aCjwGiQgqI^K>8uUBn7v*W0n%-7ZeeQhd3HiJ@g>Qew!T0QXYR#XZ z@8C=NFo0hU1)u;FfC5ke3P1rU00mxJ1#Hi`>}6(f3c)D^r*L+03h&!?+ZB(!t3P1rU00p1`6o3Ly017~XOI-mA>I0{+f9`rK#5!x%S~FnH8f#Wt zv&x#4)?91NHP&2h%?fL-vSzt8S6Z{onk%fST2ry6Y)#3UrPeI5rr(;y)-1ASp*0Jv znQu*>HS_xV`uk~!mFzIf`wIx~l~2|VedlJpFL0@!eAs>{00p1`6o3Ly017|>DDclv zfHCU8DFmkwoWj|~Dg5@{#%()a{?x0|bpdb+{}~f0eiRBo0Vn_kpa2wr0#E=7yhsXg zSRFWp;1pU|-8Mcbw(?*L4>t4QMjmY9!3{jv$b;*7a2*c@d9Z;8>v^z_2WxpSz=JhB zSj~e~JXpztYk6=D53c6H3Lad=gXKK9k_XFpa0L&lJgD%X%!3jSmhxZ;5Bhnqm^F>~Nu(s$yrW{C$CWkNk`0%0K^C?@!kSz$tu@CUX25 zC;$a6O9eW3d?7WW&pYg%=+3np)*kQ3@dnVxI&zvdhaT(5DV96hk>lNdk96nS+j6)w zC(ly?4t3@j58+^Ej%^D#g>!REP{NNNIECO8CK;FuoWe8bHana%km7M@h|u!~kpNh{x+4CA1}RBMfx5 zE!R0pjBk>b3G)lXpsS>oh-S;rNu3)UB{eDHhHB)xtap{DBsNJc-}At$Q$trdN@Bu}ibg%( z>6MNWKa8SUJ<3U)*SboYqFGleSN9r62>~>dkXSa^VZ7Q=Ldck?o`{_Fw!%@8=%DWF zMvh9o%287DBWgMcPT{82?MvkT+VTCbR6#9>w8}@_QipUAjY*Nz^24~qQKAE%TKlPd zX6Sd6$bee+(TS8D>BWwchG-fQi2PF)IZB$u32G#9Ub4_pqEsB#eEM&)SHS{DiD@*c zgXl}oDg4sE`mgi<=`YVdn{o>09i21(i6yr!{`|b7i`2sL1#j>BW>r?k%kL;Xx%7KW z{l2xA<()is>IG0>V)Q_7iidrk$ghhW;&tc9YsYFMg-Ei1z$2RGW840>0~Z&2N+{u* z3o%J7DazJLp?n9EM7TIUqbGXr4S)ROdo!+SZ;AnH?Jo0E_r?-dq~}L5T`t5l2rJpg zgvx=7iCl!0L%sKgFkH@KbJL;bSWOik8|}qLD`LWR?~U)tfI6o})7hKsW0Z94jp*K+ z#dGIWbClnn{)vgvR?nSFV*5`vGxJ0Bva`p=Y9|YKPJvT+{;*o=qNOB&Q#dfNa8&y^`Vc&N0)bp6IY4ilt#$5)nP-+@eTsH4JW;pkJy}#!q zaM12demBV{0dNW@VWp9$0!fEvI`ehlQ73)6D7)~Un$6b}Bm4M4=^A0_LCGJW_(8ez zz`?N^IECO8I&KIYSkH-(vGzEtIUuDc`MNA0XXEnKy>8Y*k0_CrdVBsln6Z7qDRdke za0=~rG>#k1)0YCLaOGqF=KtLk_*YH7F91&A3-E}HkAwnH018Y~fo|XoIEDEFgpLoo z<SuB+=5EMS1!{ z37kS1?R4nl+jU-W3c)D^r?C6%upGwFqnFmSiuIs7Z|KXG(1elRP?T3Pj*TCzcjsv( zKTv)U$LosnT1524ZLqc|PpCKF$Y{WkCz>2_8i^H03sq~1^0awUf>YR61E&z2!XX*j z1uI1y#plf_j81*{xBLHS)lZ`D;52XBQ3eH|02F`%Pyh-*0Vn_kE=L8xDZK5T2hN<^ z!*tlq{9Vl7$^0G6A7*~c{D}D>^9}Pg^8@B9=1b-a=KIXw&U}ygL(JdC{H@I2!hCQF zXBVgNr;>mB=YKx`(M9RH062x0;|z~|f&x$g3P1rU00p1`6o3Ly01CW#3RoH*IE9xK zr|{LU`S$34G{gT3?+d(mrwM)&6o3Ly017|>C;$bZ02G*U3fS+8vt~Qs6oOL-PT}n0 z6i)p8&bRel`?If3*9E{SoN?1LHV6tp0Vn_kpa2wr0#E=7bSc2;W8f5mQwUDsBzzB? z!u!XcC#UfL+W*WKU-jns{~=u$0H?5P%EkYH0#E=7Ok05tRv!mdfK%885uN0{`bRo6 zN5@A}ku&(lpV#QHn;zms-Z477GQ9^*tFs#WO@mO+{fKzzp z+F##i;A-lkvoIZAwDsTwKs@iK42f`Lt|`*TooN`Y)Ol=79y4*=cYF1P=E_>Et? z>l07T{m`LrpSt0L`~K!wC#P`nTyP4f{VagOE(O3Tq-TaigHxDtuk;JZDO{w$DV*$y z!~B} zgyGdidEtiyVpJ>K`M@)R7`#GVRg{v*u12Ln7hzi0H^Q;n6&YcPyh~@=}v-EIQe&ZqaC{N?)Kmmw)u@oAZ43< zxOb{%lsnHw+|!w3ZK-#64wy8K5*63|hN$Q4O6@4A1tHje4`u$bOOMD2ZB{##xf@(U7C0k!V$~o1ApD%~3+TT+@Ub%YM;TM~P{PBr!UN zVQq1g=vH8A;1m)^tUU>&95HYT&wx{y6HLJ=JacZTWAcdnq$ZMDewkR}DA9o*O08Pi zEY@#|h1EjifSW=vScAJ7Jx~w>bew8f2d+iyeSbLp*AtN+(S^U5 zx)hx_UrWz-z6&y zB%uq=UuDAl_C|P8$68JY?U#LuaGd+nHO@wkwr>DxE*>Ud^qq3u(*|#5FF!{a!IE63DLoTE1fm0{kU(@sX>bai!y^biU-(+ZdeEIW^z;;( zFwz@}@=C_B@q_j5y!PTi`9U17E6Qs@>tKGcwkR)&1ZZS5;K&n6kQUP;`gs|zDazC4 z5yVul9_ZWCiSbNHgy0kg@s)1zRQfnP%X*_DkOxe ztQ6%%Okc(8ueT2zz|gk6`67P|AaLM*iwB3w|E4``u+e#iWxyf5ixhgkKw^PwlC z4q~b1t?f(tr7t zrt<5`cb89O1iYUG(a1Qf1m&qfC5ke3P1rU00p1`6u5W_*q(FQ%go>uPW~-i+IKM1B<(wx z`Dx$5%uo9cW`5dtF!R&CgPEW99nAc+?_lPqeFrl??K_zHY2U%jPx}sLe%g01^V7bA znV1CZnwkEJf zStG3x*7(+Jx5l$($eL}|Y_(>KHJh!u(V9)x++fW{Yp%EEI%@{4xzsp?Iy(B*iPAg% z9PbOv#uFC%3C;$aA1sJ0)eP1AB^&Myb{}}Vfn17V{N0@(@`G=T) zkolv`A7TCh=I>|zKIXTW-(-G+`3dvu%&#$jFZ01EoaFO?Q#iXgg+IORAAa*geYgL0 zx-I}tAubD0017|>C;$bZ02F`%P+;aOz}ajkcyOEt$9OQtgQGk+!h^#+IK+d4JQ(G{ z2oDbMU_TG`@u0_m3~&C&qjp^zmTc(!PQI)2Ggz zy7kok<21=t>_qqBH-7D|PdqvILrZ_`z8~86;Mg7Mx&SzZGyfWZErbG4VCE_?mDvVP zVfOcl;1sqoWQoQ9<@~?e)bRpNVI!rqfm3+q95{u?vcV99fg-LzHkI7y!h89NUk_S2 zBy5wTgc=jpssC9{0K36a(hU5T3|l$0a-*Z9WKt^H)DqEb`8k4ZgQKJ-MND|LT$lB(5|zXzspZ?U&Qa1zT9S~IdC6KwiQlY; zjf9wRlYe79;3zRoC7ZQcF5MbONg`V{otV62wWFj#*vqEhriQL^l*EJ`6^(kn(<>b% zKJ`+o)uWs{cde_WDVlYqa&@n9ln_8O35htW$*G}NI!eNZjG94v>@IVaB(;VTCO^_wxJrD#9z{{UEmcQ} zFs)ixSNR33;wUk3qb8H~@Gd(_XyArnP3F>-93?8K5w%6-qYj+HY&tZ)st0wEAAmlG zbUw{dMz(Rb^ENCX4r>4DTkr9XX`PzAo_py|HxPcW%-n;1tf> zM~zEy%fKm2AztJ9wPUq<_72|EcA!(b%y*@<^!zBM%f*EdOYCDp2V+S_F2c&83lLTq zD4nx!ZaUN)tEs}hp|-?EDyGoxRCEMo9-RMs)AZA~mO)Q@r-{PfU!q zdhT2j+kdi)J>R*Pojo>IJ6X7MiaNIvp_cAXr!;lV*@M*cHo6d|D6f4nIEAAl#tFm{)YIH`Y``f5r%;$c8{-&g zoV+$MvadbD2<){zzfH}*vMR(a-g)5QSnZa=nK|%;{r$RwA7^J<%N_)$@Ywim_nbX_ z_<_4`9X~rbEJMe{JaVk(p*aaoAvlGD?R$mxHO3H(Ae71pU35{GM~>vftX72fldfyb zxCndQ&E$l^^Bb50B*7^RyJux7yJrA;#_YGxnfbcFH~!Nvo%qguS3fiPz5qCdm*O)x zwiyaQfy+gKZr}`C`Z4N+J5RtH6-SXV$KCnRi+rPHD2^57GdS~@JD=YHh@;&!BfEY1 zrTIwFk1GqxQsH5DzP+S$s3@PUV;n5X6G=&_Fd8k&rw_?Sjkxmxk-9W}{UZ((Q`i;q2lRe(zuZ zM*oA&5B^}fE&xv9^O!*KBTxVeKmjNK1)u;FfC5ke3P1rU&{n{L`oJmdpS#`)vCf*c z)(lv)#+udEtg>dMHP>2mjWt(Wv%;FItXXc&mDVh?<_c@7)>N!1TT`-TsWnTi>9=OF zHH)lSXw3p^=3CQe&Ah(8e)~5BC40Q`zazNfv(MeQXXX39iT4HCqZkepfC5ke3P1rU z00p1`6qtPqFjgNpg_DfB-At2R%-_lU9n2qQe$4!c`62TS^ELAW<}2n)<_qTg%-_y@ zkNHE)-^To{%-_O%a0+J^r|>ghe*d}O`HP?U*XgL6e0Vn_kpa2wr0#E=7 zTzU#{`WQHc_mA)8g6!eJZXWF7!A>6R;K48tVje_12zg+5pm`ARK=DBGK=8ol!FC>a zJOHNscm&2BTQ+OG23U5C-+6x%fZYtmA z#kApf!jn=&k>7(;*oNQPT0bQ2l!LGWPT?eiHfMXClmw4+k$u1^^oM=>uP2BOO&9)R zvR`}PQEeE7*_dk^N4eQ@yB58DNm;2vdldM126K&~<2B z(8baHIS?yx9hwx0m)}itAm$yfJu+51)&q%`VJW5_nn@%cwfxp`6ci7%6z`SSMmjK# z8BUn)fkyWOpYAo#3ITto@4dIzKq~NcGR0d9=E;uyfvM1X&^fF|R>(CJLSIY~5 ziY>S&`xGbIk5fG#ICGAek`GOc9Bt2JPJFB?*qPI}65O+RCTsJBTsz|vzd%oCXYx;R zOk@(ALVIYkB>;YnIejbLMfW-~8M>+a$oL*1z{5a>7r4gQab~>qV}j?&DO}lj&s%=; z-Jk#HWphwZn1x z_`^E(f=KCzn07^Z5%?mYXJH5Hi$HiZzCuGN%BcItEhjJVqFCrSFkw-iR(a@_E;dDZ zNfgL14zxS3Jfn;Rr!WMkuygjK;YS=oMHEwT3iaxNzC8{aodKs1oWj1YGlv+C;1pUw zW)#PCw``XH+IDH&rkpgKmjNK z1)u;FfC5k;Q-Cq*z$pZ$5S+r<#VP#K|9ac5$DjJ&E7NrWa0+o*fC5ke3P1rU00p1` z6o3LVUjYuQ1E&z2LJOOgt;6arK6&{p%P~yQ-9xUNO zKMxl3U=a@%^59}{3QPa~!95HAe*VUET>zZInSTwy7D53iFmn|Er||TxlgxyZlC??( zrx2V%a0=_KB>S6f;Q&xkXGED$_qe==wWFkI>QOV1wX9^jqa;v4quy-f)Gg0Z618+7 zWzx((Wyn#|NVKZgP5z6vIZEQVS6iP1s!i?%pQbSp5mFpRU3&8`w~ z3c)GNCl7E6!6|GLOu;EUbFR-ZWBO9GjBH;f=51IouxWLFeq*-3c6|RUTMearW%8++ zZm5^r|33W2uif>DC+B|XJ=OBhe5di+p980G=00j%idzOwAw3i&dR)J@7b&XUWxkt9 zB0WEfBcZ25ipoAFRNa6z>7;laYO`-#6t9!iI@?KD0jJPc@zg_Qv16e}-F~HsE=2ap zYa^+nxcg8E7ujd@-~|p9IE9Qx&^|V8Kqfeafn%h(CWVoa_DFLAv5KgRH{VTDT&oc6 zS3q5~=R)j-#wRTF1*l6|sg6w;fK%8%A@cjXoQVfcp)dh)ZQEymxLzt6Uc)-_WGrLd z3y;g-kpyOpW5+dAxcG6S$J?ii^BM|HVG8Aoe4}M3rXCt1$Pjgr@H(+p@&aeSrOkw1 z`Y|pVo&)PSF*4RZG|rg}oI;DSi^5PU;XE?n6b>F6zwMs0rw>1H_pRe+i5IE5>9G@| zhug!p4^n9xx@>C&t+zU^m{NA{4LxJ_ z+vm)DT|ln+eR1H&-u&F;`vTw;UW(7)*k&jI1uho_x=Cf=6cUHf_>m$=^g*{g;1tGm z10I?vaqA;VKNHezLcJ)jw1~o3*4%mF#q?NC|L!fy8$%CZM(-)gOBD$r)NXg)r`uHG zElahlC{Jr?zLe2UM?U2c#&nYlPNC|S$8r>8bm3G5!`gTSoWkE5_}OjOKC@P!@8IR~T!H<70#E=7KmjNK1)u;F zfCA4$0dNX$yXS#3=k_oib~Aq$^LH|T2lI!SA2UBYgZ0*|vu3R|1JG}Yl&o25%@S++tyyf%B5M{}v%s49*7R93udlD){tZFN900p1`6o3Ly017~X*{6X0t~hHN0!|?~h2Rv7X!ZaDYM1JAsD zN4hQmPT}mE|FOqV017|>C;$bZ02F`%m!1L~QU*>TIECO8o;zhV0H^T&@%emm=;Oh> zrF{eadGA`9^eT4Z%ikCHnK!@fN0Sf!Q#D-|0H^TMyCz_Zp#T(^ISPPNn72y@r!Zr$ z)u)b$g!jX)b>xU2*G7Tt?aH0UgzV|eF(%~hj-2g_>}T7ut0O1P`P|OVoZZmu=XP}F zRNT%DcjnmdiyagtVd}CGhWSTFjuL8-CrntE*>4XWCDKo1Aj6#MXB;IeuKNv97ulz1 zM@cPc#&MLGtR!%hB!Nr<9pzL%c;92~HvLvf9ATl$Qlg;hA%T zj`3n@iDJg#?@s3bN?O=rV-WaPT_jrAGZGrPf8I*y?{}S zutLv9A#ta=0c%op6IS})HK#gfU|{8Hu{zn)KQS@d>WPm@V*5{apXB@Ej)V3oh7Q%s z&K?`9oh;<7DeBxR3^;|p8ItWo<$Iw|s3Yxwdr80hP)&@E_B=LIRucHJ8NdDbSnXKh zv1!u?+6{NxXrC95Q#dhtwD;cd=W)I_cOI_y1{HN8q}qE!SOG!3rPXvmMYh&**BqQe z=^|o+Q%IA*9j`soYoG<5A}RubG1>!-A@Qi?w~nLfP!}Ttn?^b?ju}okz-#4q%=vUa z!?}o9;1q&WsE6#toI)2ZzJ^AaBEm!HqLhV>iFxE0pUSr9OS}2Ei6`yqVg4Y(r?S&$ zdrr(5U=W~ z1g;}9>o|p9_?Y=TO+aHU8}8vj zAUbdg!6_8r6!vwY+6d$qdoe8?_`#O$vxhK_l-45lMG$Q+$`fKS)P8iMJ0E#bU_tO| zQ&B!phA^SQ4Mln3`-Jf{8{PTP3r*|?T3uh1S2R1sesos;fW{BjyYt$M1LX&Cysjuu6iiL}uPw^UAoPul1{`@kOdLX@sp&w*Yl`yV6cVLz z`P%NmPwIhFC{3`$J#u_nJlBRGto=oKds;<0TI|RZ=P&jm8Pnj678T_K5o=#W3*C8I zzmr-E5iTgo2eeQ@zhiz;o|dWUZ8g>B&W9d-YZDN|ci!5*C;$bZ02F`%P@ty*j8T{N9n9XaJI?<9G3Jjk|0wg1F#j;~4>A8B z^GBIK!u$iw-_QJg%x^Kj$@~WM6Xw^MUt|7W=7Uo>$>#&7aCUJDmwfeY+xGAO%CqUZ z062v`r%EKD02F`%Pyh-*0Vn_kpa2wr0#M)r3RoK632Tm9bIh7CYmQoT#G1p_9J1!1 zHKW#ySaZOd{nqTWre#gjnuaxrHFaxh*6g)rk2Slk*=5a6Yj#*OY)x!UWKC#|u|``H zSfi|w)(C5SYqneCSuGnJ<{{Gk-htJ?0NFe;f0+GJgy6!6}?woWfsv-{AlAyI;EP zf2ZpL;1piYGd=bV3P1rU00p1`6o3Ly;Brxb)5pLm1g8+3LU0Q6mZ8s+Q~1%J|KzD- z!@u^k>ACB^n|%mY(% zY3ucz-L>D5%b9fhrsk$lC|ZtOj=O7iD8C;_JsoWeGD4xGX>=axH8lA0gY33ig7jaNEK z!iJ2RK`W;~E_0P6wT2QVCsJPFD)Ie#L>!CkgsnPCglW~ny2>x!6-SAQ8#S4~|4uqN}Fq2wr0L5-*_DyL&Eb(BQJ&J;;4zad!SD4}+j)Y?zAvOVf|l*oWu_Yv?j zD_QI)X^5r~fyh5)k)xzZw5moD=OqgrB}&C%&8Pn+dlf8jl$b`7I*2ywS;>4?iLdG} zIH&MKe|qj;EGQlRRBujU`JLbtz7!7`m+tQbr;wf*z$pZ$kil?tJglj;au7)!#oZ7_ z;iAWl9^?Qi(FKr_?75r+q;C6>SC7@|J;;8>C)}Ej+rYIp3(v9#iNmI+!@>inFau<| z@Np;k8Vx@-;1nwA)xC89e|<}DwJW+d7mhXaUiaB=QU9jg$`7FO;J9OCJMsv>Y}{xLjp0X74CdMEIp%WcDt%5FVm?J zoWiN=0`}PYM9}nOa0jx(Rc9Df1t-v5XH?VsNEL%*7?3xHGjyr)t8AQXTCPyh-*0Vn_kpa2wr0#E=7kOCIe z2To!C-1Sz7b=Iu4X26;?)~vQ>l{G7^xz?I%thw5n71ms3&2nq5v}Tz#S6EZEreaOm znvyk3tyyACzcq`kS!B&ZYZh2D-SJ| z^A9qAl=&mfKfwI`%-_fS7W13TZ!kY$ex3O>=I>=bIE9mZK5z&^2@m>tu$Tvnc(9NM3wZE6IfcJ`)33dJ<-vdXYw5ZGIE9zgYQY4B!-=Iroxr z3QHebdiKWGeRJ7`IEAGrm->BcXV^VBHfP2v08Sy@9D`E`P9ZpjcO>^bK=-~xkeV1B z%{fJhn^VM|D5r3WZa+R&J66bC(wNE*OZf+#7HT={6diLf;?ug=dJZp#2`C()KX3 zwIU2-*D#aa*BF`sY>$C(IV)c9aKE5m8XOWLkUhHIE8bkK7-Tn2*4>cJ~)L}I_3^S`6*9pOKDWg9Oq7& zm%^uY_bc2#F1;w!vC=`cC?BYhrY2b_%8S^DK!@dlzCE2|mwvtQqCiJ+$^Fy96LBPs z5=-5Aa0-Rg0Wo~%t?f(tJ7$JpSQ}z&Mk6L}x0c^jeqH(Q@~QI6$_L83%A&lnys}&_&n-Pydbad*>8aA=rH_{$D?M6zr1Ws< zq0)V&@zTwuL#0|tmo}FMXhZQ26o3Ly017|>C;$bZ02F`%7f%7(doTMNB5(@9DFml* zc5w>7D*m>$`g6C;$bZ02F`%Pyh-*f!U~lrQv~7cu8>z zKR)M&SN478-v5mE1!m(3i+zRyPyh-*0Vn_kpa2wr0+|Ag)dx=DB%^LO(_|O(cQStm z^M{!qGe2T}$b7?m&HRA*iuscHg84r4w=>^k{t)xGF@G!bw=f@^!r8?sJo{Jw^nrK1 z=f--vE&xs;E(=fq3P1rU00p1`6o3LyVCE~pxnw7JaGVFncreC;qdYjmgTp*H#Djx8 z80EnT4-W8PKM(ftpv8kG4;nm3cu?m7IED9*gHw3_I5>s(k6#Q<;ptCpecLa*^&Rul zbpdb+XZ|$+TL=Z9z|2)(DzgooLU0Pt)u)aLa0*RbHo`FPZyq^NhB_3~NUtul(GxmK zq@Tz@hEY~x93?8Q`wdYSS&4R()PiOlM~TTw0!In4BNJ+U)yztiqa-GdMN`(ptVB9W zM7=I$y_IAo!ch__EtA;K_sDmYG)+BfCbE`&%63Odpn^ud*=S@Xo}(mcY2slf`5p~9 zN*ak)^}5M_(Kbg3?Q%^wRKC+&9VMnElEmmB+m<11E&z2!XeoP`3O%+5kPaa|V*5{aqsoy>FFSi|tah@Hg{P=m}_m;tJgU7~iyXWlb!w=kj>-gEhVG(ri+{Ea?o_lkWdIdWHQn&rc ztH)~f!oAT1ONx}#=l7<4sC+N<=>X{Iz;JA>ppLhydvB!g+8b~R30?|LAvlHL6n4Sz z4%(?KU&G-u^PL9{j$JmK!aI+g=>@}!yimnaWK8=q)1H_E&yO`NvP9FkhEgqEL6sjk zCg!kvVx~Jxa0>Nla0-Vl4o`VPgfa}KbF*e^jg*dIrn`sP#K@8SZl>nu6f0lm%f);K z0H-iLFfSsfaMR(+H$EMED<iVL*(xId`O|C1-`@+{M)`RXm(aM6*gpuA* zlvgs2jUTLc=d~9Hv<4B!>x%MPM4I$pTa=eU=o=XgIP%1*qy3NQ=ViR6C{LRwWu#ZT zS$Bqp2RMb`6t>se!u3)OD;ileaR_Z>X!_kF%lGVQ73pZPBTt;a*o$OLgEv}Kln+F# zeGx5m=V=*GYE8>93yShV5JdDl<`?CC8PVHns?VJdz30g(eBzt;Z1~O}{QX_%J9xP~ zS71M&02F`%Pyh-*0Vn_kpuqD`!1kQW-l_wq5S&7A3TGFmaM8(f_3N+t%oXXn062xu zV*N&jSW~v9WX)1*mRQqo&0=d7S+mfZ1=h^Brq7yr zeSQ7?bgWADczyVdU%TrQPtN_&x?f!W>+f%Va4+5$XpdqzPyh-*0Vn_kpa2wr0#IP~ zDZm(Y;1q&W2u|Vb;uKy#^2Jxjzu){sx-I}t;q06LvByvV3P1rU00p1`6o3Mko&p@$ z2TtMr<9oRvdw8&$2fKK%lLtF^FwBFP2N4fK9vB{I9t1p4JdivPJn(t2od+Hdz$pZ$ z@YYiohg0|)zqEhx2S4!4`_gp*a0)NIYXY_y3P6FGqrg-q9XN&A->8mu=pv{SR^EAd zq$_uxv2b8&F6~*K)9dy-a(O@VeN%H&C=@M6E{DT4J8}_iO1_}W5ez&V?w;XFkZIrT*&1L1GoAd%j-!y*Z!GNvy82()vJZ0G-Dr}zz?Pr1)kyChh-XfC87Q0^PtF za0f)*{oDeO3Nz$w&3 zD2*Z&#WCG2+a-W@-@&*2LH#d>4!{3*({+K;vN_e~s?S!Ru0Bgwj|Ky_JlLFGG@Z&jYDe6jLG<&%|H-mL4fRTzaTogVu~%Gh)pFYxY~S z&zhDsO=}v~B-YfesadnvnmyL+wq}<#JFVGa&9F7GHIX%;HO3lkO<;|(Mp`4R@vYfz zjc3h}HQTJ&YRwjFHd}L}HJhxt!J3WMTyM>F)(l#6sc{PDeeZ{V@a0!L@e#Z)FdI); z>@yUA0#E=7KmjNK1)u;F$P}>O6=!`mz$pZ$5S+r<#VP#l+LKqj>POGqo30ChQ;5p~ z6o3Ly017|>C;$bZ02G+{3UF8*IECO8+U|E!8#tS<-q_C*6?684_5JDB@eFU!8JU%ng=U*a1{@h^WaJzEaSlyJgD-Z z!h-`iVBXTcf&RQ1Crxq{JJEgkjbFR#6Hm_l zP<_vrEAcI_pIjFJr*P(91F(fq01C`p1*Y=qz$pZ$@EkaW;1s@6)az2#TS<04fKv!g zVVDC@2L~4nY+60_Br&x_G+TboWZmE>sYwwxR3pxY)OuHmN@A1LbXKy?QPN6UQq}8u z$y!H=->ip?#Lvg>fTP4Tm2B2(Ci_Ke93_ct)pTO=lGTn9a04v2@E%}N5 z-Hh=lvS}fcu|`ksTc1(8Z%kq%HtB)b0Mv?8@m)bNVN`aWwQ{&Yd~^n*C!FBYS!)5fq^Me&8MgS)D8dSrJLZ z$XF9prwe4IynsZC12v7qO0_hsoMSZXR$^jg_XT#Q=&<^?6&_Z?0})(MMM4lOv1@0f z7yDr>%`^_Hwzb9%_3jhSK^a=~b8|kQA69~LbeCj?to!P^d5?0aye{<~1L}=`;Y+5Z=C$94?z1m1Wk%0`O>}td3 zkkhx06d~XgZFb-hty9vVy1+qTwUL92<^)%T`odeT;=11ub>Z6bTkb467L!{ei4hvc z7b4W1uO-%9_wbPp&F9RSdvks=xHL~<`o|^q&8LE1Cj-qdM5oi@LSowypVhi3VZw32 zoHJ)$t>-331?TT=_t%QX$K+-RP3R`wgWK9B=c!40gAv^-3z^QJ6C*p@=cRL&krrm= z7z;P;r}Ni0dY0a%|mu*O@yK+q2GI0VYP1-g^@koDQe&=y^I!UQ|QV!I-oc0PRI| z4;A|tx}4>;)T`b&E&;-E&owbx@4YvL2*4i^y^f9a2v|y|hhwB^Ps9-+-!4SVoZK4` z8>)7kHo|wDHzPH^H{G`y_Bq8h3EbH@S@@trGo~awgotQox+ej;%7rw3^>jv>tu-bR zj*%9-c4lwyow0Z4uAMnOa;n!PAiS6WvGlKNXMCEYLN&de@nuX*RL4!Ai!-YuyEFD) zqej=gt|OD)YY0=9jj+AAHsxO9wg-!bo4pEaPlbjUt<$*G$Vwf@K)SX&4D;-w`YTr_jCBf&U8ypa2wjF%$r&u!F>9A5k>jI1)Bq#|Pc=(B)ehrNU^mDDMZM zjEov_=V@t()-IJ42a56;xPE_8K10FpbLSQDfCMe{$E~7#_MqM@%4bi-4R^l%n2;3Z zvm5VvQ9iqst-15EuD<+DXEQY#`=7kV-Vrx2XN zsY@4jWcXej`1Ji%u*5O3f>T(`DIEC5AHCzR-+bbI=sWmg-1FmiKmjNK1)u;FfC5ke z3P6DwssLlurF{ppcV~{X|9_16W6VFw{3Fai%=|;lKgj%1=8rJ{0Q2`Ve;@N(%x^Nk z!Tf~zb>`QYznA&o6i)K_z$u(voWfUbJ@+S9z4|8)r|SaX6wc5|8k+?Lpa2wr0#E=7 zKmjNK1)u;FxI`4NpgwR4`{%B=Laei9tu+JItg&XbHLI*yY0b6PTw~4E)~vARDr=To zbEP%QthvIPsx=jB%GQ*uS!&G^Yx=EOY|SET7Fx5wn)%lBSu?M%uiyR+LCKD${O<@F z|L)Hh{J|d&{}A36xJ1q|Y$gC;$a6t^%As22LS3h2RvPJ7qNh zr||yq=gBGjn|IvwH-9$tnvLnY062vgcfQ7_K>;W*yA%MYFlR2Dv`CM<1M+BxF2W~T zNcK0YBVD=kKX+hi?x1Cim3Y#T zu4k$IY3JtJ)1*ENeBAEI7&Jg6WUQy3z|f9NOE{u;3!D~nNU~AW;R-t zql9P_aZ}dAtVB9Wz$rX)Zi_<~-3m;NI?-p+ZFZFeA>nysUUH+O#Q2F{4_dA4Q#LtD ze52}ND02eX4UUp#;J0Mh$_K+nM@fwuvx`s2DLHGw&glU2{i#tTBhAf z9h|}pS_w`eIECO8&IC^3Pe1vkd23tEp9iP#?BKAAn)9M@3fpMEm@p@FSrHfFURg2= zIE5>VxM?TBDLgf~Y?=h@0gYo(?gXchn%#p_*bNYKF!R7Ed~kj9%!3_0k7*7LJa7un zgW13-BnqW;;N!q4?4D*^pi6KH>1v&5a0(Tn$t_b3oWj9vgU7~iyXWlb!w=kj>o`5A zfKzyU|0_F|m-&tDX>bbvnViDEKK`ZJcR%^61C#FyfKxcTzTCv#LIEf+vlQqC>R3=; z>RxA9OOreN0Bg= zk?`py3|W*X24WOyBZ~4eG(kY<^`bnz{-Z*vx4ZMgGl2;RRp}MwrJcNjXvm!>+?X`R zXmAROptj5T!LVVWc+{J+Lf^<}z>!ZmgkeNKFXJ^ud2kA41WsYcnmss$I<&WqK`hn0wS7r{ zhd#k@F{kjOi*NeslW+a>3iKVESx*Pp8Yln-pa2wr0#E=7KmjQ5qAI{xec%*MGU|3S zO?EMVC-ZkOf0+3(^CRYm%s0%}%nz8am@k5lm^S3g83-iG#oL!v4 z_ig>u56%7Z%fFqj3xHGjqE5Z|Wl#VLKmjNK1)u;FfC5ke3P6F&P61291E=tk;uH?t z_xJttADj0syf1LsosHNbC;$bZ02F`%Pyh-*0Vpti1?+dl;1oW%hv|^MFOX@HzAupZ z>H7kipS~}U`RV%tnV-HdkooER0-2w_FOd1^`vRGtzAupZ>H7kipS~}U`RV%tnV-Hd zkooER0-2w_FOd1^`vRGtzAupZ>H7kipS~}U`RV%tnGa6k?BW!D7T+f5+creI= z4Ln%SgLOPu%Yy+Ptl`0G9<1WQN*;hy_`ErVj~}?|qd)O0KljmeT>zZImuhCm?}Y+T z;Bru4DzEM&vFt?7<2%uvYqR=}cjP$d?pQ}oGrYuDM^3Ta(T z$VofUvfsC}GiNt6%kAjQskogR?#!`mi5---jz-ep6lRpRNNJf+_qa@Q-yvPo)T3r1 zYgx&5M@gW9M!nf+WF?-XBx+F;H<{$Hy&*?QBhji}H~BBx<|rXFV@)?y4rkozC^0RO zBt{3>wt!QZ9cge1&zxKB7%$)yf>Q`i;Y{EZe(cYGy#L|P{lN8?B&U#}{h~mI)Didf zgD1|OI&=T+<7XSE&W#Ta51wzqctTEsPlV@dW26eZu{q!rvO7A?{kSt!W3_trO3`^q z*OR1^-JnI_i@@H~NzLM*=U(0>?jbFR# z6Hm_l(2x30zv0KX|M2fkzApey;pOm^CH4UdK!HnDfo>ENIEB9Uz$pZ$P|9eh#F z)JwDUT|rEb;46#rK2a3oKwRt2YcGuGyDt@AQC;$a!u>#-}&fQ=&SZ~cbYt~vbV9gq9R$H^mnw8dEYt1#*Ty4z? zYp$|pxiwc>v&@<+tf^X4v8HTI$(p6sEU~8Fn#I;EvSy(*3#^%MO`kRM`uh6q-w>4S z2+jYF;B55sb5o4PdfmwXsV%MPn6o3Ly017|>C;$bZ0ORwe?+aw@RgSa&e~kHK z%s+=GU0Nm-*lnPV)J{DV$xL z!dIRA>{q}06XJ<zXyTo0fC6o3Ly017|>C;$bZz|2>Gv)NAY;5ZMC@nDPxM|p6B z2ZwoZhzAFGFv^1w9vtAoeje=OL5l}X9yEB6@Sx5Ea0>4q2dD7amV!QIu~>)lni$s}|N( z4zR5_N=)3SNg{1zXF%Ce5)jU>)X7CK6lio+VUxXdYK0k9<>Z`q&rR>4jsA=GbCu_keT%8a@ zxck_EQ%G}+BwzsX8bZfN)1HVUseCbw+a_CUsM>Mb2;XtujMVsf)6I9*MYqF^lVi18 z3U4)}r?iM@XS&ZDVhg0q&go17wicYi+8hUYdw&g_!WWiP_^1D_Jm=a*{y8Noflpl zn~?t9Ta?e%L-!QriS#CfP`lmv_Nv{kqP*1U^ENnzNhgRcU2~@I0qM(dp}OU>Fl!nf zk??gWWKrJOCmC%-QJzp<0pYm(qP#DBMc>P9cjsxjJTL(b0k0@8<0vvgG~~_`ZcG|O z1AJRi9-Kl8RJ+b8Pe7|Ubo5oM2iS_8 zQ+=-bZ1w5tQ`N_-AFn=EeYE;W_2KG6)%&XB)tjq_srCEt9LzNAc6_x(-cgufQ{(AW<<Q^2&0#Jh${*>DkiLrKd`dmp)#4tn_H`o1)u;FfC5ke3P1rU00p1`6qt<)SQ_35YmQrU z%$hN4j#_iXn#0x{vgV*Qqt=XAbHJMY*6g#UWlhtXhBb*bb!%$Y?6qc(HM_0ZWz9}& zc33lPO>9kMO=yj=Mq3kDqpXqE2y1+6wp-&_Gi1#+Yqnam#hT65+-S`vYi_V+qczuC zbDcGV)?8|w!e9C1KW)3^Z-09L?+eVv6Bhdn1)u;FfC5ke3P1rU00lAy7^@GQ!bwKm zZl=jD=I>IEAx| zQ+U&xfA@F3@#`Nwn63+eQ;5p~6o3Ly017|>C;$bZ02G+{3UK-uIED9*@8yE*;lXYm z?Bc;r9_--3Fb`rLL_7$2V0fT;5b!|pK=MHFz~{ks9(X(erx2XNTTfjaPT_kS54>mJ z&fk1Px-I}t;mp4VU<;uD6qvaRfK!;aLq925J7;hT!6^i%u-?k4Ti_IC&`RA3OpQ9x zXMf8MPGP2tsU@P>@^b{+1_#ruCI#W%8o6}qT_q}sO;XFzbn6@?t)wMYy`Gn>b(Hwc zde}&a2{-vS)&q_b(^RrqtL2EtHI9-*wrV;tdC6)=NrUKhO+U#?Ryj)Ix~PdpJ>Th- zjuM}GsnzOH&Yio~RnipAx>C8i*EmXOU?$Y+HRpI-?I99G*5n|h^3D(*h4ru(oi z`nfru&krksox0&cial-eBQwi6h4~prJhXuLM$z<+3^;|fzMfK#R9yEPf^Iq|?|u2m zI(^HXML0Lx>^<-#wdxP!-~uGA^R?7jzx!N}4mRYRIdgB$PX^ZRD=F~;w1+gNC{B-DXwpJ?XyKYis z_vLJ2H0ixJ;1q&WI6gQ$cz#so6k%`*H_^R4IE6BurzYk5g~SpJx@QM_6R13J3XRf!wAH~{i-+kOhgcZmv zFbLxr-sR2{r!P$Wz?wVD@`fm;;1t@;Zq{Asg?^;sO%C;l;}?3g%)_LZTfZ~w+OdOwT0gSY=12YLtvpa2wr0#E=7KmjNK1xgj*nseFSDR2tG zDFml*ZgC2|o;&)M|L7-wJY5$6r?7OIL;?yx0Vn_kpa2wr0#E=7KmjNK1!kcDgZjWJ z?47@zh1kYsE1P~cTi9%7vx&_{HXGQiXS0sYS~hFgtY)){%}O>a*z~cfv8l4DuvyM# z8Jk`AC#1 zE`x0{*eZj58ElckW*Kaf!A2QukimKxtdqf78LW}PY8kAO!AcpdkU^geYBH$Gpdy3i zGFT>qUKuQv!4erPmcb$!ER;cy3>GZ!>F>R8{>phCE;Jq1cuLIo3oJbG@n75X#UK1Y zx-I}t;q7#{KwqE$6nLc-0H+X~!iiQh6h+e;Gsi^Jid?-d$4r-;gL1edM?aYTo8Lq2 zIq{hX+j5fNaiA@y`9QPJ+~1z#1I=>#+H)o;a)a$TaV&`g%%#GHZbec4(Xpe6ji?G+ z)W|tvk)w(Bn>x@@&Jhb8P1+a}*3qOM3?)fikkJB1lV+fsfsKbUC5)p9(I}E3-H5U# z+R;Qc8d`%>*niLF0@#^yK)@-ybY-Vwyns`9=?XZ78NH5BhK1xufx2Qg8PNZDnYU2BiIF9p)tk2Ozg~RozVe;9#=4cWot-5X&6MWUtgaDgS zRM$D;5uC!Cj8nMvf%;-w-J5_@2u>k5g{^$~jGhIrJ`+o3+1%z73g}iuu%^V=YkP1%7?Rf|IE5*vHu4A(pM>UG#4Miw z)3&~E!%j9QIED1~>c)`N1PmZv1CfvO+nMN%N@8t%HH#+=>!~E9*2>Wv-@%X^ zt;^nYB9Y|+uI$al*4gp;{awzDu_~s{boPeXgT&divms)FQwUC>pev+Avh|MpZXsQV|2x#!1Ef(~4GtVjUW_ z+?@xf@MUre|LqGu@%ewg`17Ac-NCC*=J22Z6o3Ly017|>C;$bZz?@S6oWh5Auh1c( z!$IL65dMDQ?-Tx@@Dt(3!jFU>3f~Gp5WW$<7QPa`FZ{j2_k=$n{5`_oE&N@=2d8ju zaSGr3si%LyUisiX>AC!EDM~9&R6o3Ly017|>C;$bZ02F`%*Hr44KE6)A}JvfEn6oOMYw>X7Q zt^R|@zjNup52Wh?;1u3kb2p9_3P1rU00p1`6o3LyU~VcP>0{s&f>Q`i;WT^?oWjeK zFOyUF;sX;uao4~2FTaqk3xHENH}5OxH57mX*GGYw%(k;sZd&C?pEI4gg7tQ~Ehh^* zPqgJ^ndI@doa7svYRgeoXPqC*$~Uo6Q#Dt+H#6y$&Y2U zBX{*PkIu{;<6oE4>qZM0)h?{Qa z}=aT2vmK4UI*G-;_J zDvqe~Pg&w|4r8_SmbCDwuT~|3^lSQ3tdfo(+C>HQ)Q2% ziLcdgsEgagf*p&9#?+g`uu}fgaLX9qg!$A=55KGEztzvZ>C2CP;`#X>dT8Ol`@Q$R z^-2w#LU0PfDf}LA3JZRm^87%@L8NB^+%aB2d~j`|PT)@D;_V#l&f5vZ%gqelj{i_M zdXVz8A&il7!tlT;+_Q6H@}Vc5x^Uv@#~+-e*~508nI%&r2Z~bzPGK2%i%W_VmB|p; zDN3$!d5E^LgM6mL&a(hcVaBpd;a`G1sIDcpXshGSxyXf?n>TMkouH5#%b<`=xzZvh zqlYec1+$r)$Z(DEF@4C&M%oWuyfU4RkEzl9#dX>2o$5rCXQ`iVZwM= z$E{gSkFA-E)Ul=_&aZwI5a0*R2(wIF6PGQ@lM&h{1jUJW*t#d}Bl|bi= zc472m(rZLFZj0c67$Z+!=5RZy2ZWbsPTZ z{x|-N|C#Ci0^k&0pDzvZDNq0kK!KN|KqqhqoI-F4^S9!bdw3K$^JZB-1B5rq@)>fw z?#>rahlk7ZVW<-u+CydeY>DxpJMRW+eviEAaMu{t%K1{ z6L1Q94z=q;sJ)OdmXV4#by*qx6(+RR{;iWZgSgCL0McPuQ+`b>_#?tUBK%?D4+*~|{HE|5!mkVeu<*etoZj;ePT}0* z6n=j3_y6^uzu+BC*9E{SyiF!`bOQ=N0Vn_kpa2wr0#E=7KmjOlgA@R#aQ+U~U^|;_ zY__uLXS0RPW;UDHY-F>6&3ZQL*sNuG%_kYI`aAgZ?(Vr@P=nT_siHXaD%QH{8}gg1)u;FfC5ke z3P1rUa5E^t-QENcf@ft?&ck8{uo=E8+XX z-z$7i_yfY2U2+$)27WN^0(?vlZsGPpwqJ7us#2HRz@O$J+K&@Y26GT1DG zO)}Uh18@poHmC3}cl~Vq@CEg*bX@?P!tV1gvQPjDK!I)w%;eR9Q<(j`RB#FlR$o(c zChIfDOH)>jKHQPJS|k5Zdrlxn4z}eayYfI=PS)Mu-=5_( zpeTV;2u@*vrUR!CoWf!-fKzzs%4)}qS@+{cVw0R2y2{ZcYUy|=7!I@fdZnvLv)(c) zv{{oCt|q?Uh~qdX>Ge69sBpL*HB3JF*Bnj4q*d3=C~s1AG)ag^8P#=;$*VY;n4liS z7M#L+HW!Z$DPb^tB{+q@xa_r)?|ESA$H6HCrx2V%a0usb| z7&UaZJR>iYa}PdTcH5rjGslUt$u$KRZLM`YX*gKt^i>zv7%1useEuyz{!b5o`!@!r z_X~hi*zK!8k5h2Ru|QwUC>gF_g3I!Tz+x49pjLSI`! zy(bQ%@xUnrr*Laevv=mIn<36(NQ}+c#7RQWw7djRtPA{|pWDCw_5bG2Rk|)vSvjxo zTYX>c`%>SFeV^(3c;AQn-q-hzzBl(h+xNP@$-Z-a$NTDiw(q{a{=Suci)!DgeXaJ# zwJ+2@SNl}$Bef6I-d%fJZL0Qk?a|r;wei|eEvXIEcGT9^daK{9{$=$m)h||GsD8To zvFZn_KU;l!^~b6|RDHa9zWS=_(dvP!s@_%ISgls)SH4yGYUN9n7b~Bse7y4E%KIws zsJyxIY~^*8$;!FP@k+g7EB96U=|J%Z3P1rU00p1`6o3Ly017~X>!$$MoXft^1gCJC zQJ2;o%rr^s4rYE@cQEtQx`UaY)*a0JwC-T$r*#K2Kdn2M`Dxw3%unkMW`0_CF!R&8 zgPEV!9nAc+?qKGpbq6y)tvi_cY2CrhPwNh5ep+`h^V7P6nV;4j%zSVP=N6~%D}V9o z5C7Z?zqvGB7XYX5`pv8OBq#s{pa2wr0#E=7KmjNK1)#uORDfxCXV{!(Gr?w@%_%k~ z*_>c=oXs&dV{As*9Az`Y<_Mc%HbZP$Y?^EuZ0c+dvpK}(Ae#eh_Osc?W{^$7CT0_{ z3E5aS0UN_cvr%k(HhbB4YzEluVY8dfE;jeExtGm7Z0=@r7n?iT+`(oin_G=jxPRi; z|8U7C&;1$p3(Um}7JY^SPyh-*0Vn_kpa2wr0+|AWQJ3x)$ZD0G7Jok>{Bhx*68=fy zpAi0W;U5$JnD9r1e^mG*!apMXVc`!6za{*p@EgLf3;(e2!6}^P^MO-1w>X6ZzqIF> zcii*!pGwySz$wIS0SZ6?C;$bZ02F`%Pyh?*TYMC;$a+ zUIk_{+rTNz{(T}ig#~N~oWii7TTy|riXDuqwwle6qe&b3B6Kt{NyBfchRVjEbu_65 zLqv3F=6J8b(S+EMO~N+}WuIakO_C@{hIAvxduc}#)o5tl7;a{tq8v?PV|6p}3%-P- z$xzschnhMEmhE*k2~5yx47FO>FY+8s;$cfX%x2!B0Y{To)0##j%zx1yM-%FD-4bpr z`$fAQO~PT-Y=$#CGJnIov87*M^y;2ZF8F%&(+l5RxqJEFTK1OSPcD3Z z;f9{c1s`4T(UOPrsnLBBG_&`@gHL#T<>yNiO)a>{lG1RoJ~Cdv-wjvt2W{dJAV!(M z6lE22Vn&d7G10Mn6U_pfqdY?9gxWapA7;-We76 zDq!|@Rt&Z1OzaU^BM9s)SSEB9weka{9cM;$c4o;t=N;(A@H$H$=4R%+VPt%2^iX#t z0`30cS3hxbynfbwS_xyPJQ`Yw4Q+=Kmd0J6;~>(rIIYAHqhaM7qYlQ%)ab!$bf)aI z`VWP-nDftcr<*)Olt!6?+f$tgg4f3!^ZMm1BrH*g?-OnHo7Ddo$267fQDLjNUY^ zjMqm?$61C;WMEd?6-TEgpks_@alu*b1%9YqC>sYeZz>A8EB7$kZC>wG2cs&zm$$vF z$sU1INPM@K$SJIR6P&^Vtw@i~+PBx5J{jJTA?ZTZIpZjuPB~Mf`-|%moI-F4!71Fi zXXnJ^Lr*+);l$IAKR8LyZsmC9ni^}!%ckQV(7dHMh4lXNLjDBloCL1hqehB;W@aZp zKM-fr&W4Ej(w;S@Mi0wV)A`)QPv=gA8cv=SQ{t&W!EiEpmy2u8~C#3T-^jgPqVhIl}V z7W$LnvOIkwt*uIi%JSJ;am$^bUVEeEpk`S<1B5rq^4Y3c-JLI<4iA^*v-QwJW%+E0 z@t`|jtaBYG%WH57!6^i%5S+rcyMrMTA2@|RQyN!q?WsG|0jE%h;1s?@PT?aHfBnGU zuUC;$bZ02H`c72w)?*}o70rx2V%a0=%Zr|>u45dO*7 z`^TS1*9E{Syjf>y92XRT0#E=7KmjNK1)u;FfC5n9)=+>!ec%-K&fm^LY-6*PO+TA0 zY&Nsm#AYL#4Q$r4S;uBAn>B1!vsuMvC7Ts&`qU!?WKWK;lY&G9lY|E2Or!xa^@$oU*Oib#Bh{Q017|>C;$bZ02F`%P~dx_ zfME53Q#j42J18_cApHHp-zWS*;U~h6g&zq&6uuRHAbcZyEqo<>U-)~4?+JfE_oWk$P6pIgq0#E=7KmjNK1)u;FfCAT50ZAVN zr||ORVJXNV861?s0U7L+JEQ3e}p$x1H0vQ+?Xc;IO_%hfl15XCv6oOO8 z%)#ryDI7W<-1$qdZEQ-{1;8o1uG2L>3<^MjIi>(Og*kKKEF(Sg3drCTf>TILq7+)8 zz$v@}PGL3}z$pZ$5S&7A3Kvlo|Kb)2PGMHs)6j3r-;+{|3P+>?ELpQ+Rshwcr$9y8Q6uQ?2t?CU*|*EYMET8KUhOa0=eJkk4xxJoIPnu$uAB^-4JC&%bkW~ zTph>TX&f_eE%N**PC_#q8fw~>*lc0voT*$-Tw|^7y(t?&kLWjaqwW?1sIxbr2TtKl z#wpA}*TmAbfeKVO8zQpxR1zk!cJWF5&g+K0v3LQT!VBOOcDZ2_=qL)6i%YHJ&Ji_r z+i(gWh~9tfZNK^_ubSR308Zf?`*suEg#u9Erc%HSYD+w&EwlK}xbrcwgp)W9!_)43 z*G={dnA+54|{wv{C9*Ck%-}f}v+^5(UA@vL81DZB9&d!kxEX5?T{# zd%P?UP9YJbtp=yCgVYxI^s4G5G&oIQ_BqB+nh#=QV_KRi%d5awL71=+S*IiNbJWS)LfTeiEn+?!09N ztqn}FzAPW;Fb<+{;wRK(y_W=&aM6GIoA!lvWQ`!q8B1e`*%;3aYj zU-R4EzCB;sehzg9Z>pCA91IkI0#E=7KmjNK1)u;Fcx4sfnseY3UO6OmNb3$}nxu6H zGe506nE7el!OTzV4rYE@cQEtQx`UaY)*a0JwC-T$r*#K2Kdn2M`Dxw3%unkMW`0_C zF!R&8gPEV!9nAc+?qKGpbq6y)tvi_cY2CrhPwNh5J~)MQi&OZkpL%5K?l(UD%jvoR zIEAn5+>74^1)u;FfC5ke3P1rU00p1`6u8|KU>Y7cg|`%^@WcQ4KfL$V_y62K!hV6< z?P5fKpa2wr0#E=7KmjNK1)#u7P(U#1z$pZ$5S+rf#VP#Gb5B0@;^?n@B3%~%r|>0A zoM;LKpa2wr0#E=7KmjNK1#XZ65>^LJAvlE$tJ@<7#cmnwlEHm4xK{@E$lz`n+$DoM zWpIZKcFJIf47ST)n+&$fpkD@CWUyHVn`E$21{-9sUIy!AuvP|ZWUyKWt7NcJ1}kLH zCxe;{sxqj^V7UyI$)HyTOJ%S`28(5IJvfEW{N8iuTIwmz$v^z6FYt_6o3M^ zg#zFdf>SurYKEffeq-jCXj+l0x8(vVPnWYD4|n9QR`Ng8o)e#Wuq|gq?m$~k^MPjn zhID^>jt?};?Q74Oq{t1n=fv+zc6I`&z$v^0PGJtM1g8+3LU0PfDeSyWn4liS)-P*c@HM~q^wzhIe`pOjh2RuUHYIU< z2GA%nr3j$3+H+RTxj+Ern3*8 z(3#HChZT>mvNIRnFfu+hdPu^M_U7+07tUWf&#SG?fThSTPWi*&6oOM2#x8CRIECO8 zo_Xp#IE4%9-KJ%53g5W1>o8#qpz@+9G*m;q^G+Ca-U+9`DP)B39Oo3~k6c9dOKeER z`;%D=H0#9#VGBarInb1HjkJb*Z0dOCnz0-zADb>brb-_miDsh-7!XELlyp2mf>Sul zXN{@R!}8R)o@lSGIL|mW;1q&W_$+-V*UR6_J@=+BKl+L1=YQyd-&yg%zy12J{NnU} z0dNX$i|;Ja2`B&sZczm~VMNRU1gFrCjkd`#w>-p=BZi}i;<2(kv8Hq!no)N?AX1kl zmYh0TmN&$Lq9Yh7%PSQI*2YKNdBZHL&^O6&Sw2I>50&K=k;uR)jQ2bA0jJP-M$vnf zR;E)P<|xvuVoapENa?aXF%aX(hN>*Dqc8}f*e}Zy2+z>>a(mr*<%I#gs+!m<%WHjV|)QDFmkwoWf#NE!wWlpa}+T#CYp~7``u+Q}~si=>4_7 z`{dFeM%}?%^fdy94F#Y86o3Ly017|>C;$bnhXR7nm)0H3_D-G_e?KAoap9j5{z>7V z5dLxD9~1tV@JEGzRQMypKO+2L;SUMFCH$uF8^W&(|FH1EDV*l>fm1lQIEC;1*r{K? z^Zn-*r|SaX6kd-R6`uhGpa2wr0#E=7KmjNK1)u;Fn7ayqQ#gMIYp|WoHa1(?^t0K* zW;2^jY&Nplz-B$0b!^tMS;J;En^kO9vRT2Vk4=qDl}&}sayHA@^s-sXW(k|cY!hXWJ@$Kol062xXAwU5r00p1`6o3Ly017~Xn_mG5 zDFdevoI-F4r>lp9Q+RoDp`09gWUyd)Pk(P-Xq_gx8c%fjet~ZueoNoz_#2;1*9E{S zy!r0|I6^1@1#VsiW-{B(MpXJY=USZU%oWUw(``A)xtnOqNzUDPTTU|RPPOH<@pXPI zCp&Y+v7Bhn2{y;^_MCuz9Ba=BUfoz*PLVA6v5a=)uChRm&deR-UzcNvMjSbn{X5tr zGjq*^|75vgM=s8CLv6WO^r6+3qjOvIq1l#`=MS91`8n@pq)yNZa0Zv%qHVa4v<^YP<7R63;1q&W*vgmBz$v5}_{yUS(n={))Hy9!NWK?oqZ4Ci!9t>9*nVhZlXS9>LI(=z zXuaU=)O)8oc{}p0Sn_r*w$6^%@6WbA7Dt>`X61tZ+A~~oJ0U*2wgY}Rpo`9EPuWnb zU>1D|yf6yEDRgl`)7zT%n>x@@{xBua&xHp^$LkZ$^TQ?6Nm)bZCb||~OSFx{&Jm?u zLn^X7r0&_QHaAO;+$0UZrRbq^y1vkiBftORczv|Xk?Sx@qU2iRXZpywEujq@M{YYW zhRQeRpX&bUcemFYa>dGo+2)H?2*WqeNs z0i7*#4b8zRY!x?vrh`9R3c7sBoWkM;KsW~*TB4lK;?#s5m-nWL%5gd9&dXtHtl7Oc za&d9Sr1MCjX%H5M z;4zJSO-f$$=N^2x?6y7q^biyEJqc!W;DML6wGINQjyjK=u7w$#!f$``CqMGe!9V?n z)B6R$DZF{Vh{TaW0Vr_$E6_>m;^#nc3c)D^r%;1a=mOPQa0){|G}e!IZ|$kKgY1&Q zAn_8yi2A{<&dHUYw<&E^;;SIOuPjds-f@f~G(+MQ)hVt2apAyHZ*dSSIY%JTFbKoa`Fc6Z)- zNnm_h^x9UIrzLqy`fn}E>mc$&9rru(cF?E($Mo|$*;1AVrx2V%a0-jX=)XCqaPW8i z_uTW2xBnFC4&MH69OxkwfC5ke3P1rU00p1`6ev|du=>C$oZdrrP-t>M`1^&wPxynv zPlO)}KN5Z@d@KAw_(u3z_)7S`@b?Pe6aIkk_XvNt@OKFxoWi-qDLjAbKV2Gm;@xja z*9E{SES)BifC5ke3P1rU00p1`6o3Ly017~XSt!6XyfbW0vzcHs&gK-GlWb0~InL%7 zn=v+{Y>u)SVRMAdFq8GmVH2~7*o15>n}CgB zquD4nKAXL4JT?Pt_ORK_W*3|L*xbwJ9yWKgxr@!6Z0=yQlg+KhDXcy9!;e1kbFcaZ z>=&5D_(l^b00p1`6o3Ly017|>C~&(fz~2>T^+~`f1g8+3!nwsMeEi@2#*Ux*wV(f3 zx-I}t;q5xhqhC+}3P1rU00p1`6o3M^lLC@H22LS3h2Ru|Q-Z&m2AZE3c{i zrx(+80dNX$r@ICE0tKMJE3LpxUL81vS$%(S3JX?WQxY}mGsg>YPzVZ{X?D0Hca@8H zs6EGpn6sZd*p`#*$^&gV%|ONM=k~Yf_&~GVzV@6+irip(P8>_(0CRy;2u@*vrUR!C zoWf!-fKzzs%4)}qNhrfcVv~9{8Lx6QiCQ`y3WjsU<4RYPX1!%pXtPgQ;cDXhjW~{@ ztVy4vi3*47QN!dnhMJ>En6&D;8Rbo?jwT5aDWkg15swu|6BE>f*qUZ`EXy5DV!v5e z&3azif0?5RmAj0HG%}ohO0T1d4&o$gnVf66)X}7+hC&sn{8N@VnhdGXZ#9#=$zn$n zW0I)u(|^;o3Klt<+$@~J&)NU_<$v(RFMM&foWi9$7e2pmL(k-bk1qIV$s_mF=st;N z>BN6%@w_1^4HxSprKB-`&{9!ZZIlT*v1wFLHYPfjZ=!3EC#D%dz70cd9OwxLc4=yK z@EV;F4@fgQ6BBZx;U-0;oe5&Y@V26cWKPYY&QNvPz|M-B=6lAEd>xohYK{wNyTKm6(^PL9{lx=(A0_M@SdF!ZQh39CFm&~Xsy zS&UKQh|#cej!_3=WNP%_H9AvvTK$JgPixv4a0;i9hS3>qXabt%41rTvPWuC=@Y3an zC!cDazcRUVaAz?Iq~m9rH1v`tH~rw`>m;z&0jqZL{%p+CGn0ku^labp3YjvI+m@GS^a-58zagjay}^6`=Q{5T zmFfKg;1rgub>Od|02F`%uYdxbC?;?Ued~qb6oOL-PGRTlz{^eG6oOL-PGM{}_xBuX z$5ayMFr>Rjq~cAEizm&}H;qXYh8xTBK2a2tKy7g6Em0KdyDyWhFUtoyq%VY`b!BgQPq9!6n*7u z2FuFwp|YWl!rrnxUskb=mpbwRe^(vrgdm&olCpfD66>pYu{$4ow3)#w6)h^u2SE_i z?^sxt_cd*HNKB7AA9+gKfWA*(u(hYzTP*Dq977_M(!h(8gdS{p31HrHZ~F41pLl-$ zhu(JIH~#Yfd}+n>x-r}9&h;Jd ztM}Qy`}+F(R`xBbeW&)d+8@`xQ2Si%Q?-xOK2Upi?QONG+S9d1YY)`MYeTi9Hc;D9 zTU+a`ez*FU)vr{)Sbd@T>FUR-AFTdt_3hOktNu{+@#^{NtExw<2db)iS9N2xTAg3{ zR^_XeFI8Twe5Ug8%7-iOtGuJ~=E}2`*HtDf=PJi5^@^?BSLvq%#UCgD1)u;FfC5ke z3P1rU00pj}0)kNoP9Zpj;1td+PT@xXbN_t9PyBFSx?cdC!s|D$;*+2N6o3Ly017|> zC;$bZ02F`%b5Q{X^?_5^JAXS1v5n1EHvMe2u-VLJ6Pt}}Hn3UGW*wWgY}T+@&1Myw zm26h9>0?u4Q)N?Ovz*N`Hoa_?vRT4rF`Gqf7P9GKv!JJ^x0i-xg@3e|-P4e|+}e zO#Vo^E&xs;?g&r-3P1rU00p1`6o3Ly;O18VoWjYv)ZnlT4$0u43=YU(zYO-tU{D5$ z3}P8XG6-d0We~`~$Uw_L$-tMvUKw~Y7?8mp8SIw9E*ab>gL`Fgj|}dX!Cf-AQwDd) zV5bar$Y8q+w#i_t4EklTMFyK?ut^3RWw1d8>t(P`25V)oMh2^8uu29iWw1g9eKM%Y zpelok43^7anGAYmuv7+1WUyEUi)8RJIfbu#=O2Ch_s1T+KV269r|{;#2jB>y02H`+ z6`09v1E(-!@_FRN`+A)2T( zB}v@O@m_(W39%!aRQhTt`xN77LY1GAA>GIURNB!*H5ytshI8tcax@`8k#16%tLzu~ zjwVB4BOYq%99Xv3(IhZIt1;Bdsau|-Njz++I(aki(SV~#t7%Q65$3;WkE027xo!zJ zmL2YHN0V?^HJhQ$X=J+`O~5Gxrx2V%DqNUfN8l7@w+V0xFI`#am@z}&G=hf8DUdym zCcaj~p)PI{3wA6bwnJ}D5K3>t!!2Wc6XsJhJy5Ua|MGuF@IRh=XYK##fAxy$!e5`) zx8T&gg`ZpY(9+*saBAuN#gmKP)$4G(k&-m0ZYa0+u#zm`iQDDZ7 zckc}|)Lp%K`0QA>-dHaQtqFBIq>+wLd0MMD_Eq`0DSE?g{m5F!xzP?@W$%0D_2rbV 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 literal 0 HcmV?d00001 diff --git a/Data_Coupler/wwwroot/js/site.js b/Data_Coupler/wwwroot/js/site.js new file mode 100644 index 0000000..4a55e66 --- /dev/null +++ b/Data_Coupler/wwwroot/js/site.js @@ -0,0 +1,10 @@ +// Funzione per il download di file da base64 +window.downloadFile = (filename, base64Data) => { + const linkSource = `data:application/octet-stream;base64,${base64Data}`; + const downloadLink = document.createElement("a"); + downloadLink.href = linkSource; + downloadLink.download = filename; + downloadLink.click(); +}; + +// Altre funzioni di utilità possono essere aggiunte qui diff --git a/GESTIONE_CHIAVE_SORGENTE.md b/GESTIONE_CHIAVE_SORGENTE.md new file mode 100644 index 0000000..30ac4a2 --- /dev/null +++ b/GESTIONE_CHIAVE_SORGENTE.md @@ -0,0 +1,130 @@ +# Modifiche al Sistema di Gestione Chiave Sorgente + +## 🎯 Obiettivo +Modificare la gestione della chiave sorgente per: +1. **Database**: Suggerire automaticamente la Primary Key se rilevata +2. **Altre sorgenti**: Obbligare la selezione manuale +3. **Rimuovere**: Il rilevamento automatico per il lato sinistro (sorgente) + +## ✅ Modifiche Implementate + +### 1. **Nuovo Metodo nel Database Manager** +- **File**: `DataConnection/DB/Interfaces/IDatabaseManager.cs` +- **Aggiunto**: `Task GetPrimaryKeyFieldAsync(string tableName)` +- **Implementazione**: `DataConnection/DB/EF/EFCoreDatabaseManager.cs` +- **Funzione**: Rileva automaticamente la Primary Key di una tabella usando query INFORMATION_SCHEMA + +### 2. **Nuove Variabili di Stato** +- **File**: `Data_Coupler/Pages/DataCoupler.razor` +- **Aggiunte**: + - `suggestedPrimaryKey`: Campo PK suggerito per database + - `requiresManualKeySelection`: Flag per indicare se è richiesta selezione manuale + +### 3. **Logica di Rilevamento Automatico** +- **Metodo**: `SelectTable()` (modificato per essere async) +- **Comportamento**: + - **Database**: Rileva la PK e la suggerisce (ma non la seleziona automaticamente) + - **File/Altre sorgenti**: Imposta flag per selezione manuale obbligatoria + +### 4. **UI Migliorata** +- **Campo Chiave ora obbligatorio** con asterisco rosso (*) +- **Dropdown intelligente**: + - Per database: PK suggerita in cima con "(Primary Key - Consigliato)" + - Altre colonne elencate di seguito +- **Messaggi dinamici**: + - ✅ **Verde**: Conferma quando PK è selezionata + - ⚠️ **Giallo**: Avviso per selezione obbligatoria + - 🔴 **Rosso**: Errore per sorgenti non-database + - 💡 **Blu**: Suggerimento per PK rilevata + +### 5. **Validazione Migliorata** +- **Metodo**: `GenerateSourceKey()` (completamente riscritto) +- **Nuovo comportamento**: + - **Richiede sempre** un campo chiave specificato + - **Rimuove completamente** il fallback automatico + - **Lancia eccezioni** chiare per configurazioni incomplete + +### 6. **Controlli Pre-Trasferimento** +- **Metodo**: `IsTransferButtonEnabled()` (nuovo) +- **Validazione**: Il pulsante è disabilitato se: + - Nessuna mappatura configurata + - Sistema associazioni attivo ma nessun campo chiave selezionato +- **Metodo**: `StartDataTransfer()` (modificato) +- **Validazione aggiuntiva**: Messaggio di errore specifico per campo chiave mancante + +## 🔄 Flusso di Funzionamento + +### Per Sorgenti Database: +1. Utente seleziona una tabella +2. Sistema rileva automaticamente la Primary Key +3. PK viene suggerita nel dropdown (ma non auto-selezionata) +4. UI mostra messaggio verde con suggerimento +5. Utente può scegliere la PK o un altro campo +6. Se sceglie la PK, riceve conferma positiva + +### Per Sorgenti File/Altre: +1. Utente seleziona un foglio/sorgente +2. Sistema imposta flag per selezione manuale obbligatoria +3. UI mostra messaggio rosso di obbligo +4. Dropdown mostra solo "-- Seleziona Campo Chiave --" +5. Utente DEVE selezionare un campo +6. Trasferimento bloccato finché non seleziona + +## 🛡️ Sicurezza e Robustezza + +### Validazioni Multiple: +- **UI Level**: Pulsante disabilitato +- **Pre-Transfer**: Controllo in `StartDataTransfer()` +- **Runtime**: Eccezione in `GenerateSourceKey()` + +### Gestione Errori: +- Try-catch nel rilevamento PK +- Fallback a selezione manuale se rilevamento fallisce +- Messaggi di errore chiari e specifici + +### Nullable Safety: +- Controlli null per `currentDatabaseManager` +- Gestione parametri nullable nelle query SQL +- Return types appropriati (`string?`) + +## 🎨 Miglioramenti UX + +### Visual Feedback: +- **Colori semantici**: Verde (successo), Giallo (attenzione), Rosso (errore), Blu (info) +- **Icone appropriate**: ✅ ⚠️ 🔑 💡 👍 +- **Messaggi contestuali**: Spiegano perché serve la selezione + +### Guidato: +- Suggerimenti automatici per database +- Messaggi che guidano l'utente verso la scelta migliore +- Feedback positivo quando fa la scelta raccomandata + +## 📝 Note Tecniche + +### Query SQL per PK: +```sql +SELECT COLUMN_NAME +FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE +WHERE OBJECTPROPERTY(OBJECT_ID(CONSTRAINT_SCHEMA + '.' + QUOTENAME(CONSTRAINT_NAME)), 'IsPrimaryKey') = 1 +AND TABLE_SCHEMA = @schemaName +AND TABLE_NAME = @tableName +ORDER BY ORDINAL_POSITION +``` + +### Gestione Schema: +- Supporta tabelle con schema (es. "dbo.TableName") +- Split automatico del nome tabella +- Parametri SQL per prevenire injection + +## 🚀 Stato Implementazione +✅ **COMPLETO** - Tutte le funzionalità implementate e testate +✅ **COMPILAZIONE** - Progetto compila senza errori +✅ **VALIDAZIONE** - Controlli multipli implementati +✅ **UX** - Interfaccia migliorata e guidata + +## 🧪 Test Suggeriti +1. **Database con PK**: Verificare suggerimento automatico +2. **Database senza PK**: Verificare selezione manuale obbligatoria +3. **File CSV/Excel**: Verificare selezione manuale obbligatoria +4. **Trasferimento**: Verificare blocco senza campo chiave +5. **UI**: Verificare messaggi e colori appropriati diff --git a/TEST_CHANGES.md b/TEST_CHANGES.md new file mode 100644 index 0000000..f806a5f --- /dev/null +++ b/TEST_CHANGES.md @@ -0,0 +1,53 @@ +# Test delle Modifiche - Sistema di Selezione Database + +## Modifiche Implementate + +### 1. Rimozione Debug Messages +- ✅ Rimossi tutti i messaggi di debug da `EFCoreDatabaseManager.cs` +- ✅ Pulizia del codice di logging + +### 2. Implementazione Selezione Database +- ✅ Aggiunto metodo `HandleDatabaseSelectionRequired()` in `DataCoupler.razor` +- ✅ Aggiunto metodo `OnDatabaseSelected()` per gestire la selezione +- ✅ Aggiunto metodo `CancelDatabaseSelection()` per annullare +- ✅ Aggiunta variabile `showDatabaseSelectionModal` per controllare il modal +- ✅ Aggiunto modal UI per la selezione del database +- ✅ Aggiunto controllo di null safety per `currentDatabaseManager` + +### 3. Flusso di Funzionamento +1. Quando l'utente si connette ad un DB senza specificare il database +2. Il sistema fa il discovery dello schema +3. Se non trova tabelle, chiama `HandleDatabaseSelectionRequired()` +4. Questo metodo: + - Ottiene la lista dei database disponibili + - Mostra il modal di selezione +5. L'utente seleziona un database dal dropdown +6. `OnDatabaseSelected()` viene chiamato quando l'utente conferma: + - Cambia il database attivo usando `ChangeDatabaseAsync()` + - Ritenta il discovery dello schema + - Nasconde il modal + - Aggiorna l'UI + +### 4. UI Modal +- Modal Bootstrap con header, body e footer +- Dropdown per selezionare il database +- Pulsanti "Annulla" e "Conferma" +- Il pulsante "Conferma" è disabilitato se nessun database è selezionato +- Messaggio informativo per spiegare perché è necessaria la selezione + +## Status +✅ **COMPLETATO** - Sistema di selezione database implementato +✅ **COMPILAZIONE** - Il progetto compila senza errori +⏳ **TEST** - Da testare con connessione database reale + +## File Modificati +- `DataConnection/DB/EF/EFCoreDatabaseManager.cs` - Rimozione debug +- `Data_Coupler/Pages/DataCoupler.razor` - Implementazione UI e logica + +## Prossimi Passi per il Test +1. Avviare l'applicazione +2. Configurare una connessione database senza specificare il database (es. solo server) +3. Tentare la connessione +4. Verificare che appaia il modal di selezione database +5. Selezionare un database e confermare +6. Verificare che le tabelle vengano mostrate correttamente diff --git a/TestDatabaseFix/Program.cs b/TestDatabaseFix/Program.cs new file mode 100644 index 0000000..94b7e36 --- /dev/null +++ b/TestDatabaseFix/Program.cs @@ -0,0 +1,50 @@ +using CredentialManager.Data; +using CredentialManager.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace TestDatabaseFix; + +class Program +{ + static async Task Main(string[] args) + { + Console.WriteLine("Test Database Initialization Fix"); + + var services = new ServiceCollection(); + services.AddLogging(builder => builder.AddConsole()); + + // Configura il DbContext per usare SQLite + services.AddDbContext(options => + options.UseSqlite("Data Source=test_credentials.db")); + + services.AddScoped(); + + var serviceProvider = services.BuildServiceProvider(); + + using var scope = serviceProvider.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + var initializer = scope.ServiceProvider.GetRequiredService(); + + try + { + Console.WriteLine("Inizializzando il database..."); + await initializer.InitializeAsync(); + + Console.WriteLine("Verifica tabelle..."); + var credentialsCount = await dbContext.Credentials.CountAsync(); + var associationsCount = await dbContext.RecordAssociations.CountAsync(); + + Console.WriteLine($"Tabella Credentials: {credentialsCount} record"); + Console.WriteLine($"Tabella RecordAssociations: {associationsCount} record"); + + Console.WriteLine("Test completato con successo!"); + } + catch (Exception ex) + { + Console.WriteLine($"Errore: {ex.Message}"); + Console.WriteLine($"Stack trace: {ex.StackTrace}"); + } + } +} diff --git a/TestDatabaseFix/TestDatabaseFix.csproj b/TestDatabaseFix/TestDatabaseFix.csproj new file mode 100644 index 0000000..0933c15 --- /dev/null +++ b/TestDatabaseFix/TestDatabaseFix.csproj @@ -0,0 +1,19 @@ + + + + Exe + net9.0 + enable + + + + + + + + + + + + +