From b4498a4e0bc49e9b8b2bc8c7e9d29b8e32ac3222 Mon Sep 17 00:00:00 2001 From: Marco Cavallo Date: Wed, 4 Feb 2026 23:44:51 +0100 Subject: [PATCH 1/7] feat: add DbInitializer with Bogus-based seed data - issue #13 --- src/PTRP.Data/DbInitializer.cs | 247 +++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 src/PTRP.Data/DbInitializer.cs diff --git a/src/PTRP.Data/DbInitializer.cs b/src/PTRP.Data/DbInitializer.cs new file mode 100644 index 0000000..289035f --- /dev/null +++ b/src/PTRP.Data/DbInitializer.cs @@ -0,0 +1,247 @@ +using Bogus; +using PTRP.Models; +using PTRP.Models.Enums; + +namespace PTRP.Data; + +/// +/// Inizializza il database con dati di esempio per l'ambiente di sviluppo. +/// Issue #13: Data seeding per testing e demo. +/// +public static class DbInitializer +{ + /// + /// Popola il database con dati di esempio se vuoto. + /// Idempotente: non duplica dati se già presenti. + /// + public static void Initialize(PTRPDbContext context) + { + // Se ci sono già pazienti, assume che il DB sia già popolato + if (context.Patients.Any()) + { + return; // DB già seeded + } + + // Configura Bogus per italiano + Randomizer.Seed = new Random(12345); // Seed fisso per dati riproducibili + + // 1. Crea Educatori Professionali (5-8) + var educators = CreateEducators(); + context.ProfessionalEducators.AddRange(educators); + context.SaveChanges(); + + // 2. Crea Pazienti (10-15) + var patients = CreatePatients(); + context.Patients.AddRange(patients); + context.SaveChanges(); + + // 3. Crea Progetti Terapeutici con relazioni (20-25) + var projects = CreateTherapyProjects(patients, educators); + context.TherapyProjects.AddRange(projects); + context.SaveChanges(); + + // 4. Crea Appuntamenti per progetti attivi + var appointments = CreateScheduledVisits(projects); + context.ScheduledVisits.AddRange(appointments); + context.SaveChanges(); + } + + private static List CreateEducators() + { + var educatorFaker = new Faker("it") + .RuleFor(e => e.Id, f => Guid.NewGuid()) + .RuleFor(e => e.FirstName, f => f.Name.FirstName()) + .RuleFor(e => e.LastName, f => f.Name.LastName()) + .RuleFor(e => e.Email, (f, e) => f.Internet.Email(e.FirstName, e.LastName)) + .RuleFor(e => e.PhoneNumber, f => f.Phone.PhoneNumber("+39 ### ### ####")) + .RuleFor(e => e.IsActive, f => f.Random.Bool(0.9f)) // 90% attivi + .RuleFor(e => e.Specialization, f => f.PickRandom( + "Disturbi dell'umore", + "Dipendenze", + "Disturbi d'ansia", + "Psicosi", + "Disabilità cognitiva", + "Disturbi della personalità" + )) + .RuleFor(e => e.Notes, f => f.Lorem.Sentence()) + .RuleFor(e => e.CreatedAt, f => f.Date.PastOffset(2).UtcDateTime) + .RuleFor(e => e.UpdatedAt, f => f.Date.RecentOffset().UtcDateTime); + + return educatorFaker.Generate(7); + } + + private static List CreatePatients() + { + var patientFaker = new Faker("it") + .RuleFor(p => p.Id, f => Guid.NewGuid()) + .RuleFor(p => p.FirstName, f => f.Name.FirstName()) + .RuleFor(p => p.LastName, f => f.Name.LastName()) + .RuleFor(p => p.DateOfBirth, f => f.Date.PastOffset(65, DateTime.Now.AddYears(-18)).UtcDateTime) + .RuleFor(p => p.FiscalCode, f => GenerateFakeFiscalCode()) + .RuleFor(p => p.Address, f => f.Address.FullAddress()) + .RuleFor(p => p.PhoneNumber, f => f.Phone.PhoneNumber("+39 ### ### ####")) + .RuleFor(p => p.EmergencyContact, f => f.Name.FullName()) + .RuleFor(p => p.EmergencyPhone, f => f.Phone.PhoneNumber("+39 ### ### ####")) + .RuleFor(p => p.Notes, f => f.Lorem.Sentence()) + .RuleFor(p => p.CreatedAt, f => f.Date.PastOffset(3).UtcDateTime) + .RuleFor(p => p.UpdatedAt, f => f.Date.RecentOffset().UtcDateTime); + + return patientFaker.Generate(12); + } + + private static List CreateTherapyProjects( + List patients, + List educators) + { + var projects = new List(); + var random = new Random(12345); + + foreach (var patient in patients) + { + // Ogni paziente ha 1-3 progetti (storico + eventualmente attivo) + var projectCount = random.Next(1, 4); + + for (int i = 0; i < projectCount; i++) + { + var isActive = (i == projectCount - 1) && random.Next(0, 100) < 70; // 70% ha progetto attivo + var state = isActive + ? TherapyProjectState.Active + : new Faker().PickRandom( + TherapyProjectState.Completed, + TherapyProjectState.Suspended, + TherapyProjectState.Deceased + ); + + var startDate = DateTime.UtcNow.AddMonths(-random.Next(3, 24)); + var endDate = isActive ? null : (DateTime?)startDate.AddMonths(random.Next(6, 18)); + + var project = new TherapyProjectModel + { + Id = Guid.NewGuid(), + PatientId = patient.Id, + Title = $"PT {patient.LastName} {startDate.Year}", + Description = new Faker("it").Lorem.Paragraph(), + State = state, + StartDate = startDate, + EndDate = endDate, + SuspendedAt = state == TherapyProjectState.Suspended ? endDate : null, + SuspensionReason = state == TherapyProjectState.Suspended + ? new Faker("it").PickRandom( + "Ricovero ospedaliero", + "Richiesta del paziente", + "Trasferimento ad altra struttura" + ) + : null, + CompletedAt = state == TherapyProjectState.Completed ? endDate : null, + Goals = new Faker("it").Lorem.Sentences(3), + CreatedAt = startDate.AddDays(-7), + UpdatedAt = DateTime.UtcNow + }; + + // Assegna 1-3 educatori al progetto + var assignedEducators = educators + .OrderBy(x => random.Next()) + .Take(random.Next(1, 4)) + .ToList(); + + project.ProjectOperators = assignedEducators.Select((e, idx) => new ProjectOperatorModel + { + Id = Guid.NewGuid(), + ProjectId = project.Id, + OperatorId = e.Id, + Role = idx == 0 ? "Coordinator" : "Assistant", + AssignedAt = startDate, + RemovedAt = null + }).ToList(); + + projects.Add(project); + } + } + + return projects; + } + + private static List CreateScheduledVisits( + List projects) + { + var appointments = new List(); + var random = new Random(12345); + var faker = new Faker("it"); + + // Crea appuntamenti solo per progetti attivi + var activeProjects = projects.Where(p => p.State == TherapyProjectState.Active).ToList(); + + foreach (var project in activeProjects) + { + // 4 appuntamenti canonici per progetto attivo + var visitTypes = new[] { "Intake", "Intermediate", "Intermediate", "Final" }; + var startDate = project.StartDate; + + for (int i = 0; i < visitTypes.Length; i++) + { + var scheduledDate = startDate.AddMonths(i * 3).AddDays(random.Next(-7, 7)); + var isPast = scheduledDate < DateTime.UtcNow; + + var appointment = new ScheduledVisitModel + { + Id = Guid.NewGuid(), + ProjectId = project.Id, + VisitTypeId = Guid.NewGuid(), // TODO: Link to real VisitTypeModel when available + ScheduledDate = scheduledDate, + Duration = 60, + Location = faker.PickRandom("Sede Centrale", "Comunità Residenziale", "Domicilio", "Teleconferenza"), + Status = isPast + ? faker.PickRandom(AppointmentStatus.Completed, AppointmentStatus.Missed) + : AppointmentStatus.Scheduled, + Notes = faker.Lorem.Sentence(), + CreatedAt = scheduledDate.AddDays(-14), + UpdatedAt = DateTime.UtcNow + }; + + appointments.Add(appointment); + } + + // Aggiungi 1-2 appuntamenti extra casuali + var extraCount = random.Next(0, 3); + for (int i = 0; i < extraCount; i++) + { + var extraDate = DateTime.UtcNow.AddDays(random.Next(-30, 60)); + + appointments.Add(new ScheduledVisitModel + { + Id = Guid.NewGuid(), + ProjectId = project.Id, + VisitTypeId = Guid.NewGuid(), // ExtraVisit + ScheduledDate = extraDate, + Duration = 45, + Location = faker.PickRandom("Sede Centrale", "Domicilio"), + Status = extraDate < DateTime.UtcNow + ? AppointmentStatus.Completed + : AppointmentStatus.Scheduled, + Notes = "Visita straordinaria", + CreatedAt = extraDate.AddDays(-7), + UpdatedAt = DateTime.UtcNow + }); + } + } + + return appointments; + } + + /// + /// Genera un codice fiscale fake realistico (non valido ma con formato corretto) + /// + private static string GenerateFakeFiscalCode() + { + var faker = new Faker(); + var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + var digits = "0123456789"; + + return $"{chars[faker.Random.Int(0, 25)]}{chars[faker.Random.Int(0, 25)]}{chars[faker.Random.Int(0, 25)]}" + + $"{chars[faker.Random.Int(0, 25)]}{chars[faker.Random.Int(0, 25)]}{chars[faker.Random.Int(0, 25)]}" + + $"{digits[faker.Random.Int(0, 9)]}{digits[faker.Random.Int(0, 9)]}" + + $"{chars[faker.Random.Int(0, 25)]}{digits[faker.Random.Int(0, 9)]}{digits[faker.Random.Int(0, 9)]}" + + $"{chars[faker.Random.Int(0, 25)]}{digits[faker.Random.Int(0, 9)]}{digits[faker.Random.Int(0, 9)]}{digits[faker.Random.Int(0, 9)]}" + + $"{chars[faker.Random.Int(0, 25)]}"; + } +} From d31c53fd2a4aec296a572d191ad36b0912ecfc5e Mon Sep 17 00:00:00 2001 From: Marco Cavallo Date: Wed, 4 Feb 2026 23:45:11 +0100 Subject: [PATCH 2/7] feat: add Bogus package for fake data generation - issue #13 --- src/PTRP.Data/PTRP.Data.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PTRP.Data/PTRP.Data.csproj b/src/PTRP.Data/PTRP.Data.csproj index 53d1a12..e9c5e5b 100644 --- a/src/PTRP.Data/PTRP.Data.csproj +++ b/src/PTRP.Data/PTRP.Data.csproj @@ -7,6 +7,7 @@ + From bb1a9f4f3d6456673ad4f13a253180c5854f37ee Mon Sep 17 00:00:00 2001 From: Marco Cavallo Date: Wed, 4 Feb 2026 23:45:41 +0100 Subject: [PATCH 3/7] feat: integrate DbInitializer for development data seeding - issue #13 --- src/PTRP.App/App.xaml.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/PTRP.App/App.xaml.cs b/src/PTRP.App/App.xaml.cs index 99a61ed..12e61ba 100644 --- a/src/PTRP.App/App.xaml.cs +++ b/src/PTRP.App/App.xaml.cs @@ -45,8 +45,8 @@ protected override async void OnStartup(StartupEventArgs e) // Costruisce il service provider _serviceProvider = services.BuildServiceProvider(); - // Assicura che il database sia creato (senza dati se primo avvio) - EnsureDatabaseCreated(); + // Assicura che il database sia creato e popolato con dati di esempio (Issue #13) + InitializeDatabase(); // Risolve MainWindow e MainViewModel var mainWindow = _serviceProvider.GetRequiredService(); @@ -139,19 +139,22 @@ private void ConfigureServices(ServiceCollection services) } /// - /// Assicura che il database sia creato - /// Le migrations verranno applicate durante ConfigurationService.InitializeDatabaseAsync + /// Inizializza il database creandolo se necessario e popolandolo con dati di esempio. + /// Issue #13: Data seeding per sviluppo e testing. /// - private void EnsureDatabaseCreated() + private void InitializeDatabase() { if (_serviceProvider == null) return; using var scope = _serviceProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); - // Crea il database se non esiste (senza dati) - // I dati verranno popolati dal pacchetto di configurazione + // Crea il database se non esiste context.Database.EnsureCreated(); + + // Popola con dati di esempio (idempotente - non duplica) + // Issue #13: DbInitializer with Bogus for realistic fake data + DbInitializer.Initialize(context); } /// From 84292efd6d1bdb5dab10382d3c74cea2683b0cd1 Mon Sep 17 00:00:00 2001 From: Marco Cavallo Date: Wed, 4 Feb 2026 23:51:31 +0100 Subject: [PATCH 4/7] fix: correct DbInitializer to match actual model properties - issue #13 --- src/PTRP.Data/DbInitializer.cs | 127 ++++++++++++--------------------- 1 file changed, 44 insertions(+), 83 deletions(-) diff --git a/src/PTRP.Data/DbInitializer.cs b/src/PTRP.Data/DbInitializer.cs index 289035f..234c27a 100644 --- a/src/PTRP.Data/DbInitializer.cs +++ b/src/PTRP.Data/DbInitializer.cs @@ -25,12 +25,12 @@ public static void Initialize(PTRPDbContext context) // Configura Bogus per italiano Randomizer.Seed = new Random(12345); // Seed fisso per dati riproducibili - // 1. Crea Educatori Professionali (5-8) + // 1. Crea Educatori Professionali (7) var educators = CreateEducators(); context.ProfessionalEducators.AddRange(educators); context.SaveChanges(); - // 2. Crea Pazienti (10-15) + // 2. Crea Pazienti (12) var patients = CreatePatients(); context.Patients.AddRange(patients); context.SaveChanges(); @@ -54,7 +54,7 @@ private static List CreateEducators() .RuleFor(e => e.LastName, f => f.Name.LastName()) .RuleFor(e => e.Email, (f, e) => f.Internet.Email(e.FirstName, e.LastName)) .RuleFor(e => e.PhoneNumber, f => f.Phone.PhoneNumber("+39 ### ### ####")) - .RuleFor(e => e.IsActive, f => f.Random.Bool(0.9f)) // 90% attivi + .RuleFor(e => e.DateOfBirth, f => f.Date.Past(40, DateTime.Now.AddYears(-25))) .RuleFor(e => e.Specialization, f => f.PickRandom( "Disturbi dell'umore", "Dipendenze", @@ -63,11 +63,20 @@ private static List CreateEducators() "Disabilità cognitiva", "Disturbi della personalità" )) - .RuleFor(e => e.Notes, f => f.Lorem.Sentence()) - .RuleFor(e => e.CreatedAt, f => f.Date.PastOffset(2).UtcDateTime) - .RuleFor(e => e.UpdatedAt, f => f.Date.RecentOffset().UtcDateTime); - - return educatorFaker.Generate(7); + .RuleFor(e => e.LicenseNumber, f => f.Random.Replace("EP-####-##")) + .RuleFor(e => e.HireDate, f => f.Date.Past(5)) + .RuleFor(e => e.Status, f => f.Random.Bool(0.9f) ? "Active" : "Inactive") + .RuleFor(e => e.Role, f => f.PickRandom("Coordinatore", "Educatore", "Educatore")) // 1/3 coordinatore + .RuleFor(e => e.IsCurrentUser, f => false) + .RuleFor(e => e.CreatedAt, f => f.Date.Past(2)) + .RuleFor(e => e.UpdatedAt, f => f.Date.Recent()); + + var educators = educatorFaker.Generate(7); + // Imposta il primo come utente corrente + educators[0].IsCurrentUser = true; + educators[0].Role = "Coordinatore"; + + return educators; } private static List CreatePatients() @@ -76,15 +85,8 @@ private static List CreatePatients() .RuleFor(p => p.Id, f => Guid.NewGuid()) .RuleFor(p => p.FirstName, f => f.Name.FirstName()) .RuleFor(p => p.LastName, f => f.Name.LastName()) - .RuleFor(p => p.DateOfBirth, f => f.Date.PastOffset(65, DateTime.Now.AddYears(-18)).UtcDateTime) - .RuleFor(p => p.FiscalCode, f => GenerateFakeFiscalCode()) - .RuleFor(p => p.Address, f => f.Address.FullAddress()) - .RuleFor(p => p.PhoneNumber, f => f.Phone.PhoneNumber("+39 ### ### ####")) - .RuleFor(p => p.EmergencyContact, f => f.Name.FullName()) - .RuleFor(p => p.EmergencyPhone, f => f.Phone.PhoneNumber("+39 ### ### ####")) - .RuleFor(p => p.Notes, f => f.Lorem.Sentence()) - .RuleFor(p => p.CreatedAt, f => f.Date.PastOffset(3).UtcDateTime) - .RuleFor(p => p.UpdatedAt, f => f.Date.RecentOffset().UtcDateTime); + .RuleFor(p => p.CreatedAt, f => f.Date.Past(3)) + .RuleFor(p => p.UpdatedAt, f => f.Date.Recent()); return patientFaker.Generate(12); } @@ -95,6 +97,7 @@ private static List CreateTherapyProjects( { var projects = new List(); var random = new Random(12345); + var faker = new Faker("it"); foreach (var patient in patients) { @@ -104,56 +107,33 @@ private static List CreateTherapyProjects( for (int i = 0; i < projectCount; i++) { var isActive = (i == projectCount - 1) && random.Next(0, 100) < 70; // 70% ha progetto attivo - var state = isActive - ? TherapyProjectState.Active - : new Faker().PickRandom( - TherapyProjectState.Completed, - TherapyProjectState.Suspended, - TherapyProjectState.Deceased - ); + var status = isActive + ? "In Progress" + : faker.PickRandom("Completed", "On Hold", "Cancelled"); var startDate = DateTime.UtcNow.AddMonths(-random.Next(3, 24)); var endDate = isActive ? null : (DateTime?)startDate.AddMonths(random.Next(6, 18)); + // Assegna 1-3 educatori al progetto + var assignedEducators = educators + .OrderBy(x => random.Next()) + .Take(random.Next(1, 4)) + .ToList(); + var project = new TherapyProjectModel { Id = Guid.NewGuid(), PatientId = patient.Id, Title = $"PT {patient.LastName} {startDate.Year}", - Description = new Faker("it").Lorem.Paragraph(), - State = state, + Description = faker.Lorem.Paragraph(), + Status = status, StartDate = startDate, EndDate = endDate, - SuspendedAt = state == TherapyProjectState.Suspended ? endDate : null, - SuspensionReason = state == TherapyProjectState.Suspended - ? new Faker("it").PickRandom( - "Ricovero ospedaliero", - "Richiesta del paziente", - "Trasferimento ad altra struttura" - ) - : null, - CompletedAt = state == TherapyProjectState.Completed ? endDate : null, - Goals = new Faker("it").Lorem.Sentences(3), CreatedAt = startDate.AddDays(-7), - UpdatedAt = DateTime.UtcNow + UpdatedAt = DateTime.UtcNow, + ProfessionalEducators = assignedEducators }; - // Assegna 1-3 educatori al progetto - var assignedEducators = educators - .OrderBy(x => random.Next()) - .Take(random.Next(1, 4)) - .ToList(); - - project.ProjectOperators = assignedEducators.Select((e, idx) => new ProjectOperatorModel - { - Id = Guid.NewGuid(), - ProjectId = project.Id, - OperatorId = e.Id, - Role = idx == 0 ? "Coordinator" : "Assistant", - AssignedAt = startDate, - RemovedAt = null - }).ToList(); - projects.Add(project); } } @@ -169,12 +149,12 @@ private static List CreateScheduledVisits( var faker = new Faker("it"); // Crea appuntamenti solo per progetti attivi - var activeProjects = projects.Where(p => p.State == TherapyProjectState.Active).ToList(); + var activeProjects = projects.Where(p => p.Status == "In Progress").ToList(); foreach (var project in activeProjects) { // 4 appuntamenti canonici per progetto attivo - var visitTypes = new[] { "Intake", "Intermediate", "Intermediate", "Final" }; + var visitTypes = new[] { VisitType.Intake, VisitType.Intermediate, VisitType.Intermediate, VisitType.Final }; var startDate = project.StartDate; for (int i = 0; i < visitTypes.Length; i++) @@ -185,23 +165,22 @@ private static List CreateScheduledVisits( var appointment = new ScheduledVisitModel { Id = Guid.NewGuid(), - ProjectId = project.Id, - VisitTypeId = Guid.NewGuid(), // TODO: Link to real VisitTypeModel when available + TherapyProjectId = project.Id, + Type = visitTypes[i], ScheduledDate = scheduledDate, - Duration = 60, - Location = faker.PickRandom("Sede Centrale", "Comunità Residenziale", "Domicilio", "Teleconferenza"), Status = isPast ? faker.PickRandom(AppointmentStatus.Completed, AppointmentStatus.Missed) : AppointmentStatus.Scheduled, Notes = faker.Lorem.Sentence(), CreatedAt = scheduledDate.AddDays(-14), - UpdatedAt = DateTime.UtcNow + UpdatedAt = DateTime.UtcNow, + Version = 1 }; appointments.Add(appointment); } - // Aggiungi 1-2 appuntamenti extra casuali + // Aggiungi 0-2 appuntamenti extra casuali var extraCount = random.Next(0, 3); for (int i = 0; i < extraCount; i++) { @@ -210,38 +189,20 @@ private static List CreateScheduledVisits( appointments.Add(new ScheduledVisitModel { Id = Guid.NewGuid(), - ProjectId = project.Id, - VisitTypeId = Guid.NewGuid(), // ExtraVisit + TherapyProjectId = project.Id, + Type = VisitType.FollowUp, ScheduledDate = extraDate, - Duration = 45, - Location = faker.PickRandom("Sede Centrale", "Domicilio"), Status = extraDate < DateTime.UtcNow ? AppointmentStatus.Completed : AppointmentStatus.Scheduled, Notes = "Visita straordinaria", CreatedAt = extraDate.AddDays(-7), - UpdatedAt = DateTime.UtcNow + UpdatedAt = DateTime.UtcNow, + Version = 1 }); } } return appointments; } - - /// - /// Genera un codice fiscale fake realistico (non valido ma con formato corretto) - /// - private static string GenerateFakeFiscalCode() - { - var faker = new Faker(); - var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - var digits = "0123456789"; - - return $"{chars[faker.Random.Int(0, 25)]}{chars[faker.Random.Int(0, 25)]}{chars[faker.Random.Int(0, 25)]}" + - $"{chars[faker.Random.Int(0, 25)]}{chars[faker.Random.Int(0, 25)]}{chars[faker.Random.Int(0, 25)]}" + - $"{digits[faker.Random.Int(0, 9)]}{digits[faker.Random.Int(0, 9)]}" + - $"{chars[faker.Random.Int(0, 25)]}{digits[faker.Random.Int(0, 9)]}{digits[faker.Random.Int(0, 9)]}" + - $"{chars[faker.Random.Int(0, 25)]}{digits[faker.Random.Int(0, 9)]}{digits[faker.Random.Int(0, 9)]}{digits[faker.Random.Int(0, 9)]}" + - $"{chars[faker.Random.Int(0, 25)]}"; - } } From 2217fcc7a7f6c073846147f7db3ef296af6bd543 Mon Sep 17 00:00:00 2001 From: Marco Cavallo Date: Wed, 4 Feb 2026 23:55:52 +0100 Subject: [PATCH 5/7] fix: correct many-to-many relationship handling in DbInitializer - issue #13 --- src/PTRP.Data/DbInitializer.cs | 36 ++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/PTRP.Data/DbInitializer.cs b/src/PTRP.Data/DbInitializer.cs index 234c27a..7d4d38f 100644 --- a/src/PTRP.Data/DbInitializer.cs +++ b/src/PTRP.Data/DbInitializer.cs @@ -28,17 +28,17 @@ public static void Initialize(PTRPDbContext context) // 1. Crea Educatori Professionali (7) var educators = CreateEducators(); context.ProfessionalEducators.AddRange(educators); - context.SaveChanges(); + context.SaveChanges(); // IMPORTANTE: Salva prima di creare relazioni // 2. Crea Pazienti (12) var patients = CreatePatients(); context.Patients.AddRange(patients); - context.SaveChanges(); + context.SaveChanges(); // IMPORTANTE: Salva prima di creare relazioni // 3. Crea Progetti Terapeutici con relazioni (20-25) var projects = CreateTherapyProjects(patients, educators); context.TherapyProjects.AddRange(projects); - context.SaveChanges(); + context.SaveChanges(); // EF gestisce automaticamente la join table // 4. Crea Appuntamenti per progetti attivi var appointments = CreateScheduledVisits(projects); @@ -114,12 +114,6 @@ private static List CreateTherapyProjects( var startDate = DateTime.UtcNow.AddMonths(-random.Next(3, 24)); var endDate = isActive ? null : (DateTime?)startDate.AddMonths(random.Next(6, 18)); - // Assegna 1-3 educatori al progetto - var assignedEducators = educators - .OrderBy(x => random.Next()) - .Take(random.Next(1, 4)) - .ToList(); - var project = new TherapyProjectModel { Id = Guid.NewGuid(), @@ -130,10 +124,21 @@ private static List CreateTherapyProjects( StartDate = startDate, EndDate = endDate, CreatedAt = startDate.AddDays(-7), - UpdatedAt = DateTime.UtcNow, - ProfessionalEducators = assignedEducators + UpdatedAt = DateTime.UtcNow }; + // Assegna 1-3 educatori al progetto + // EF gestisce automaticamente la join table TherapyProjectEducator + var assignedEducators = educators + .OrderBy(x => random.Next()) + .Take(random.Next(1, 4)) + .ToList(); + + foreach (var educator in assignedEducators) + { + project.ProfessionalEducators.Add(educator); + } + projects.Add(project); } } @@ -154,7 +159,14 @@ private static List CreateScheduledVisits( foreach (var project in activeProjects) { // 4 appuntamenti canonici per progetto attivo - var visitTypes = new[] { VisitType.Intake, VisitType.Intermediate, VisitType.Intermediate, VisitType.Final }; + var visitTypes = new[] + { + VisitType.Intake, + VisitType.Intermediate, + VisitType.Intermediate, + VisitType.Final + }; + var startDate = project.StartDate; for (int i = 0; i < visitTypes.Length; i++) From d06d8173b70fe1105b9aded7574ef5e22c20581b Mon Sep 17 00:00:00 2001 From: Marco Cavallo Date: Wed, 4 Feb 2026 23:59:22 +0100 Subject: [PATCH 6/7] fix: correct enum values to match VisitType naming convention - issue #13 --- src/PTRP.Data/DbInitializer.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/PTRP.Data/DbInitializer.cs b/src/PTRP.Data/DbInitializer.cs index 7d4d38f..c615bb4 100644 --- a/src/PTRP.Data/DbInitializer.cs +++ b/src/PTRP.Data/DbInitializer.cs @@ -159,12 +159,13 @@ private static List CreateScheduledVisits( foreach (var project in activeProjects) { // 4 appuntamenti canonici per progetto attivo + // IMPORTANTE: Enum values sono UPPERCASE_SNAKE_CASE var visitTypes = new[] { - VisitType.Intake, - VisitType.Intermediate, - VisitType.Intermediate, - VisitType.Final + VisitType.INTAKE, + VisitType.INTERMEDIATE, + VisitType.INTERMEDIATE, + VisitType.FINAL }; var startDate = project.StartDate; @@ -202,7 +203,7 @@ private static List CreateScheduledVisits( { Id = Guid.NewGuid(), TherapyProjectId = project.Id, - Type = VisitType.FollowUp, + Type = VisitType.FOLLOW_UP, // UPPERCASE_SNAKE_CASE ScheduledDate = extraDate, Status = extraDate < DateTime.UtcNow ? AppointmentStatus.Completed From 5dc75ea1fd8d421a3ac048c5522a2c556b6164c5 Mon Sep 17 00:00:00 2001 From: Marco Cavallo Date: Thu, 5 Feb 2026 00:02:42 +0100 Subject: [PATCH 7/7] fix: ensure database schema is recreated if incomplete - issue #13 --- src/PTRP.App/App.xaml.cs | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/PTRP.App/App.xaml.cs b/src/PTRP.App/App.xaml.cs index 12e61ba..41252e2 100644 --- a/src/PTRP.App/App.xaml.cs +++ b/src/PTRP.App/App.xaml.cs @@ -141,6 +141,9 @@ private void ConfigureServices(ServiceCollection services) /// /// Inizializza il database creandolo se necessario e popolandolo con dati di esempio. /// Issue #13: Data seeding per sviluppo e testing. + /// + /// IMPORTANTE: In sviluppo, ricrea il database se schema è incompleto/obsoleto. + /// In produzione, questa logica sarà sostituita da migrations. /// private void InitializeDatabase() { @@ -149,12 +152,34 @@ private void InitializeDatabase() using var scope = _serviceProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); - // Crea il database se non esiste - context.Database.EnsureCreated(); - - // Popola con dati di esempio (idempotente - non duplica) - // Issue #13: DbInitializer with Bogus for realistic fake data - DbInitializer.Initialize(context); + try + { + // Verifica se il database esiste e ha lo schema corretto + // Tentativo di query su tabella critica + _ = context.ScheduledVisits.Any(); + + // Se arriviamo qui, il DB esiste e ha lo schema corretto + // Popola con dati di esempio solo se vuoto (idempotente) + DbInitializer.Initialize(context); + } + catch (Microsoft.Data.Sqlite.SqliteException ex) when (ex.Message.Contains("no such table")) + { + // Schema incompleto o obsoleto - ricrea database + System.Diagnostics.Debug.WriteLine("Database schema incomplete. Recreating..."); + + // Elimina e ricrea il database con schema aggiornato + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); + + // Popola con dati di esempio + DbInitializer.Initialize(context); + } + catch (Exception) + { + // Altri errori - prova comunque a creare/aggiornare + context.Database.EnsureCreated(); + DbInitializer.Initialize(context); + } } ///