From 17aa861c770363d1bb1ce3cd09708bd922cca6cf Mon Sep 17 00:00:00 2001 From: Marco Cavallo Date: Sat, 14 Feb 2026 18:25:07 +0100 Subject: [PATCH 01/31] feat(calendar): add DayViewModel for calendar grid --- src/PTRP.ViewModels/Calendar/DayViewModel.cs | 88 ++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/PTRP.ViewModels/Calendar/DayViewModel.cs diff --git a/src/PTRP.ViewModels/Calendar/DayViewModel.cs b/src/PTRP.ViewModels/Calendar/DayViewModel.cs new file mode 100644 index 0000000..4057bf0 --- /dev/null +++ b/src/PTRP.ViewModels/Calendar/DayViewModel.cs @@ -0,0 +1,88 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System.Collections.ObjectModel; + +namespace PTRP.ViewModels.Calendar; + +/// +/// Rappresenta un singolo giorno nel calendario mensile +/// +public partial class DayViewModel : ObservableObject +{ + /// + /// Data del giorno + /// + [ObservableProperty] + private DateTime _date; + + /// + /// Numero del giorno (1-31) + /// + public int DayNumber => Date.Day; + + /// + /// Indica se il giorno appartiene al mese corrente visualizzato + /// + [ObservableProperty] + private bool _isCurrentMonth; + + /// + /// Indica se il giorno è oggi + /// + public bool IsToday => Date.Date == DateTime.Today; + + /// + /// Indica se il giorno ha almeno un appuntamento + /// + public bool HasAppointments => Appointments.Count > 0; + + /// + /// Numero totale di appuntamenti nel giorno + /// + public int AppointmentCount => Appointments.Count; + + /// + /// Appuntamenti del giorno (per determinare badge colore) + /// + [ObservableProperty] + private ObservableCollection _appointments = new(); + + /// + /// Indica se il giorno è selezionato + /// + [ObservableProperty] + private bool _isSelected; + + /// + /// Colore badge principale basato sullo stato del primo progetto Active + /// (per visualizzazione nella cella calendario) + /// + public string BadgeColor + { + get + { + if (!HasAppointments) return "Transparent"; + + // Trova primo appuntamento con progetto Active + var activeAppt = Appointments.FirstOrDefault(a => a.ProjectState == "Active"); + if (activeAppt != null) return "#28A745"; // Verde + + var suspendedAppt = Appointments.FirstOrDefault(a => a.ProjectState == "Suspended"); + if (suspendedAppt != null) return "#FFC107"; // Giallo + + var completedAppt = Appointments.FirstOrDefault(a => a.ProjectState == "Completed"); + if (completedAppt != null) return "#6C757D"; // Grigio + + var deceasedAppt = Appointments.FirstOrDefault(a => a.ProjectState == "Deceased"); + if (deceasedAppt != null) return "#000000"; // Nero + + return "Transparent"; + } + } + + partial void OnAppointmentsChanged(ObservableCollection value) + { + OnPropertyChanged(nameof(HasAppointments)); + OnPropertyChanged(nameof(AppointmentCount)); + OnPropertyChanged(nameof(BadgeColor)); + } +} From c9d1fd78721cdc2684282c75847c9799024f285b Mon Sep 17 00:00:00 2001 From: Marco Cavallo Date: Sat, 14 Feb 2026 18:25:29 +0100 Subject: [PATCH 02/31] feat(calendar): add AppointmentSummaryViewModel --- .../Calendar/AppointmentSummaryViewModel.cs | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 src/PTRP.ViewModels/Calendar/AppointmentSummaryViewModel.cs diff --git a/src/PTRP.ViewModels/Calendar/AppointmentSummaryViewModel.cs b/src/PTRP.ViewModels/Calendar/AppointmentSummaryViewModel.cs new file mode 100644 index 0000000..e4ed847 --- /dev/null +++ b/src/PTRP.ViewModels/Calendar/AppointmentSummaryViewModel.cs @@ -0,0 +1,125 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace PTRP.ViewModels.Calendar; + +/// +/// Rappresenta un appuntamento nella lista giornaliera del calendario +/// +public partial class AppointmentSummaryViewModel : ObservableObject +{ + /// + /// ID dell'appuntamento programmato + /// + [ObservableProperty] + private Guid _scheduledVisitId; + + /// + /// Tipo appuntamento (INTAKE, INTERMEDIATE, FINAL, DISCHARGE) + /// + [ObservableProperty] + private string _visitType = string.Empty; + + /// + /// Display name del tipo appuntamento ("Prima Apertura", "Verifica Intermedia", etc.) + /// + [ObservableProperty] + private string _visitTypeDisplay = string.Empty; + + /// + /// Nome completo paziente + /// + [ObservableProperty] + private string _patientName = string.Empty; + + /// + /// ID paziente (per navigazione) + /// + [ObservableProperty] + private Guid _patientId; + + /// + /// Titolo progetto terapeutico + /// + [ObservableProperty] + private string _projectTitle = string.Empty; + + /// + /// ID progetto terapeutico (per navigazione) + /// + [ObservableProperty] + private Guid _projectId; + + /// + /// Stato progetto (Active, Suspended, Completed, Deceased) + /// + [ObservableProperty] + private string _projectState = string.Empty; + + /// + /// Display name stato progetto ("Attivo", "Sospeso", etc.) + /// + [ObservableProperty] + private string _projectStateDisplay = string.Empty; + + /// + /// Lista nomi educatori assegnati (es: "Bianchi, Verdi") + /// + [ObservableProperty] + private string _educatorNames = string.Empty; + + /// + /// Data e ora programmata appuntamento + /// + [ObservableProperty] + private DateTime _scheduledDateTime; + + /// + /// Data e ora formattata ("14:30") + /// + public string TimeDisplay => ScheduledDateTime.ToString("HH:mm"); + + /// + /// Stato appuntamento (Scheduled, Completed, Missed, Rescheduled) + /// + [ObservableProperty] + private string _appointmentStatus = "Scheduled"; + + /// + /// Indica se l'appuntamento è già stato completato (ha visita registrata) + /// + public bool IsCompleted => AppointmentStatus == "Completed"; + + /// + /// Indica se l'appuntamento è stato segnato come mancato + /// + public bool IsMissed => AppointmentStatus == "Missed"; + + /// + /// Indica se l'appuntamento può essere gestito (registra visita, riprogramma, segna mancato) + /// + public bool CanManage => !IsCompleted && !IsMissed; + + /// + /// Colore badge basato su stato progetto + /// + public string ProjectStateBadgeColor => ProjectState switch + { + "Active" => "#28A745", // Verde + "Suspended" => "#FFC107", // Giallo + "Completed" => "#6C757D", // Grigio + "Deceased" => "#000000", // Nero + _ => "#6C757D" + }; + + partial void OnScheduledDateTimeChanged(DateTime value) + { + OnPropertyChanged(nameof(TimeDisplay)); + } + + partial void OnAppointmentStatusChanged(string value) + { + OnPropertyChanged(nameof(IsCompleted)); + OnPropertyChanged(nameof(IsMissed)); + OnPropertyChanged(nameof(CanManage)); + } +} From c848b77db344df6e3edafb9cfda61eaff91e398a Mon Sep 17 00:00:00 2001 From: Marco Cavallo Date: Sat, 14 Feb 2026 18:26:20 +0100 Subject: [PATCH 03/31] feat(calendar): add CalendarViewModel with month navigation and filters --- .../Calendar/CalendarViewModel.cs | 419 ++++++++++++++++++ 1 file changed, 419 insertions(+) create mode 100644 src/PTRP.ViewModels/Calendar/CalendarViewModel.cs diff --git a/src/PTRP.ViewModels/Calendar/CalendarViewModel.cs b/src/PTRP.ViewModels/Calendar/CalendarViewModel.cs new file mode 100644 index 0000000..e702afa --- /dev/null +++ b/src/PTRP.ViewModels/Calendar/CalendarViewModel.cs @@ -0,0 +1,419 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using System.Collections.ObjectModel; + +namespace PTRP.ViewModels.Calendar; + +/// +/// ViewModel per CalendarView - Gestione calendario mensile appuntamenti +/// +public partial class CalendarViewModel : ViewModelBase +{ + public override string DisplayName => "Calendario"; + + #region Properties + + /// + /// Mese e anno correntemente visualizzati + /// + [ObservableProperty] + private DateTime _currentMonth = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1); + + /// + /// Display del mese corrente (es: "Febbraio 2026") + /// + public string CurrentMonthDisplay => CurrentMonth.ToString("MMMM yyyy"); + + /// + /// Giorni del mese visualizzati nella griglia (include giorni mese precedente/successivo per completare settimane) + /// + [ObservableProperty] + private ObservableCollection _days = new(); + + /// + /// Data selezionata dall'utente + /// + [ObservableProperty] + private DateTime? _selectedDate; + + /// + /// Appuntamenti del giorno selezionato + /// + [ObservableProperty] + private ObservableCollection _selectedDayAppointments = new(); + + /// + /// Indica se ci sono appuntamenti nel giorno selezionato + /// + public bool HasSelectedDayAppointments => SelectedDayAppointments.Count > 0; + + /// + /// Messaggio quando nessun appuntamento nel giorno selezionato + /// + public string NoAppointmentsMessage => SelectedDate.HasValue + ? $"Nessun appuntamento il {SelectedDate.Value:dd/MM/yyyy}" + : "Seleziona un giorno per visualizzare gli appuntamenti"; + + #endregion + + #region Filters + + /// + /// Filtro per tipo appuntamento - INTAKE + /// + [ObservableProperty] + private bool _filterIntake = true; + + /// + /// Filtro per tipo appuntamento - INTERMEDIATE e FINAL + /// + [ObservableProperty] + private bool _filterVerifiche = true; + + /// + /// Filtro per tipo appuntamento - DISCHARGE + /// + [ObservableProperty] + private bool _filterDimissioni = true; + + /// + /// Filtro per educatore (Guid.Empty = Tutti) + /// + [ObservableProperty] + private Guid _selectedEducatorFilter = Guid.Empty; + + /// + /// Lista educatori disponibili per filtro + /// + [ObservableProperty] + private ObservableCollection _educatorFilterOptions = new() + { + new EducatorFilterItem { Id = Guid.Empty, Name = "Tutti" } + }; + + /// + /// Filtro per stato progetto ("All", "Active", "Suspended", "Completed", "Deceased") + /// + [ObservableProperty] + private string _selectedProjectStateFilter = "All"; + + /// + /// Opzioni filtro stato progetto + /// + [ObservableProperty] + private ObservableCollection _projectStateFilterOptions = new() + { + "Tutti", + "Active", + "Suspended", + "Completed", + "Deceased" + }; + + #endregion + + #region Loading + + /// + /// Indica se i dati sono in caricamento + /// + [ObservableProperty] + private bool _isLoading; + + #endregion + + #region Commands + + /// + /// Naviga al mese precedente + /// + [RelayCommand] + private async Task GoToPreviousMonthAsync() + { + CurrentMonth = CurrentMonth.AddMonths(-1); + await LoadMonthDataAsync(); + } + + /// + /// Naviga al mese successivo + /// + [RelayCommand] + private async Task GoToNextMonthAsync() + { + CurrentMonth = CurrentMonth.AddMonths(1); + await LoadMonthDataAsync(); + } + + /// + /// Naviga al mese corrente (oggi) + /// + [RelayCommand] + private async Task GoToTodayAsync() + { + CurrentMonth = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1); + SelectedDate = DateTime.Today; + await LoadMonthDataAsync(); + } + + /// + /// Seleziona un giorno e carica i suoi appuntamenti + /// + [RelayCommand] + private async Task SelectDayAsync(DayViewModel day) + { + // Deseleziona giorno precedente + var previousSelected = Days.FirstOrDefault(d => d.IsSelected); + if (previousSelected != null) + previousSelected.IsSelected = false; + + // Seleziona nuovo giorno + day.IsSelected = true; + SelectedDate = day.Date; + + // Carica appuntamenti del giorno + await LoadDayAppointmentsAsync(day.Date); + } + + /// + /// Apre form registrazione visita per appuntamento + /// + [RelayCommand] + private void RegisterVisit(AppointmentSummaryViewModel appointment) + { + // TODO: Aprire VisitFormView con dati appuntamento precompilati + // Implementazione con NavigationService o Dialog Service + } + + /// + /// Riprogramma appuntamento + /// + [RelayCommand] + private async Task RescheduleAppointmentAsync(AppointmentSummaryViewModel appointment) + { + // TODO: Implementare dialog riprogrammazione + await Task.CompletedTask; + } + + /// + /// Segna appuntamento come mancato + /// + [RelayCommand] + private async Task MarkAsMissedAsync(AppointmentSummaryViewModel appointment) + { + // TODO: Chiamare service per marcare come Missed + appointment.AppointmentStatus = "Missed"; + await Task.CompletedTask; + } + + /// + /// Riapplica filtri e ricarica dati + /// + [RelayCommand] + private async Task ApplyFiltersAsync() + { + await LoadMonthDataAsync(); + if (SelectedDate.HasValue) + await LoadDayAppointmentsAsync(SelectedDate.Value); + } + + #endregion + + #region Data Loading + + /// + /// Carica dati del mese corrente + /// + public async Task LoadMonthDataAsync() + { + IsLoading = true; + try + { // TODO: Sostituire con chiamata a IScheduledVisitService + await Task.Delay(300); // Simula API call + + // Genera giorni del mese (con padding per settimane complete) + GenerateCalendarDays(); + + // Carica appuntamenti del mese e assegnali ai giorni + await LoadMonthAppointmentsAsync(); + } + finally + { + IsLoading = false; + } + } + + /// + /// Genera griglia giorni del calendario + /// + private void GenerateCalendarDays() + { + Days.Clear(); + + var firstDayOfMonth = CurrentMonth; + var lastDayOfMonth = firstDayOfMonth.AddMonths(1).AddDays(-1); + + // Determina primo giorno della prima settimana (Lunedì) + var firstDayOfWeek = firstDayOfMonth; + while (firstDayOfWeek.DayOfWeek != DayOfWeek.Monday) + firstDayOfWeek = firstDayOfWeek.AddDays(-1); + + // Determina ultimo giorno dell'ultima settimana (Domenica) + var lastDayOfWeek = lastDayOfMonth; + while (lastDayOfWeek.DayOfWeek != DayOfWeek.Sunday) + lastDayOfWeek = lastDayOfWeek.AddDays(1); + + // Genera tutti i giorni + var currentDay = firstDayOfWeek; + while (currentDay <= lastDayOfWeek) + { + var day = new DayViewModel + { + Date = currentDay, + IsCurrentMonth = currentDay.Month == CurrentMonth.Month, + IsSelected = SelectedDate.HasValue && currentDay.Date == SelectedDate.Value.Date + }; + + Days.Add(day); + currentDay = currentDay.AddDays(1); + } + } + + /// + /// Carica appuntamenti del mese e li assegna ai giorni + /// + private async Task LoadMonthAppointmentsAsync() + { + // TODO: Sostituire con chiamata reale a service + await Task.Delay(100); + + // MOCK DATA per testing + var sampleAppointments = GenerateSampleAppointments(); + + // Assegna appuntamenti ai giorni + foreach (var appointment in sampleAppointments) + { + var day = Days.FirstOrDefault(d => d.Date.Date == appointment.ScheduledDateTime.Date); + if (day != null) + { + day.Appointments.Add(appointment); + } + } + } + + /// + /// Carica appuntamenti di un giorno specifico + /// + private async Task LoadDayAppointmentsAsync(DateTime date) + { + IsLoading = true; + try + { + await Task.Delay(100); + + var day = Days.FirstOrDefault(d => d.Date.Date == date.Date); + if (day != null) + { + SelectedDayAppointments = new ObservableCollection(day.Appointments); + } + else + { + SelectedDayAppointments.Clear(); + } + + OnPropertyChanged(nameof(HasSelectedDayAppointments)); + OnPropertyChanged(nameof(NoAppointmentsMessage)); + } + finally + { + IsLoading = false; + } + } + + /// + /// Genera dati di esempio per testing + /// + private List GenerateSampleAppointments() + { + var appointments = new List(); + + // Appuntamento 1: Prima Apertura - Active + appointments.Add(new AppointmentSummaryViewModel + { + ScheduledVisitId = Guid.NewGuid(), + VisitType = "INTAKE", + VisitTypeDisplay = "Prima Apertura", + PatientName = "Mario Rossi", + PatientId = Guid.NewGuid(), + ProjectTitle = "PTRP 2025-2027", + ProjectId = Guid.NewGuid(), + ProjectState = "Active", + ProjectStateDisplay = "Attivo", + EducatorNames = "Bianchi, Verdi", + ScheduledDateTime = new DateTime(CurrentMonth.Year, CurrentMonth.Month, 3, 14, 30, 0), + AppointmentStatus = "Scheduled" + }); + + // Appuntamento 2: Verifica Intermedia - Suspended + appointments.Add(new AppointmentSummaryViewModel + { + ScheduledVisitId = Guid.NewGuid(), + VisitType = "INTERMEDIATE", + VisitTypeDisplay = "Verifica Intermedia", + PatientName = "Luca Bianchi", + PatientId = Guid.NewGuid(), + ProjectTitle = "PTRP 2024-2026", + ProjectId = Guid.NewGuid(), + ProjectState = "Suspended", + ProjectStateDisplay = "Sospeso", + EducatorNames = "Rossi", + ScheduledDateTime = new DateTime(CurrentMonth.Year, CurrentMonth.Month, 17, 10, 0, 0), + AppointmentStatus = "Scheduled" + }); + + // Appuntamento 3: Dimissioni - Completed + appointments.Add(new AppointmentSummaryViewModel + { + ScheduledVisitId = Guid.NewGuid(), + VisitType = "DISCHARGE", + VisitTypeDisplay = "Dimissioni", + PatientName = "Anna Verdi", + PatientId = Guid.NewGuid(), + ProjectTitle = "PTRP 2023-2025", + ProjectId = Guid.NewGuid(), + ProjectState = "Completed", + ProjectStateDisplay = "Completato", + EducatorNames = "Verdi, Neri", + ScheduledDateTime = new DateTime(CurrentMonth.Year, CurrentMonth.Month, 23, 15, 30, 0), + AppointmentStatus = "Completed" + }); + + return appointments; + } + + #endregion + + partial void OnCurrentMonthChanged(DateTime value) + { + OnPropertyChanged(nameof(CurrentMonthDisplay)); + } + + partial void OnSelectedDateChanged(DateTime? value) + { + OnPropertyChanged(nameof(NoAppointmentsMessage)); + } + + partial void OnSelectedDayAppointmentsChanged(ObservableCollection value) + { + OnPropertyChanged(nameof(HasSelectedDayAppointments)); + OnPropertyChanged(nameof(NoAppointmentsMessage)); + } +} + +/// +/// Helper per item filtro educatore +/// +public class EducatorFilterItem +{ + public Guid Id { get; set; } + public string Name { get; set; } = string.Empty; +} From a38569a006ce13dd04d1f9aceec92dbb88d558b3 Mon Sep 17 00:00:00 2001 From: Marco Cavallo Date: Sat, 14 Feb 2026 18:26:39 +0100 Subject: [PATCH 04/31] feat(visits): add OperatorCheckboxViewModel for visit form --- .../Visits/OperatorCheckboxViewModel.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/PTRP.ViewModels/Visits/OperatorCheckboxViewModel.cs diff --git a/src/PTRP.ViewModels/Visits/OperatorCheckboxViewModel.cs b/src/PTRP.ViewModels/Visits/OperatorCheckboxViewModel.cs new file mode 100644 index 0000000..4063829 --- /dev/null +++ b/src/PTRP.ViewModels/Visits/OperatorCheckboxViewModel.cs @@ -0,0 +1,48 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace PTRP.ViewModels.Visits; + +/// +/// Rappresenta un operatore nella checkbox list del form registrazione visita +/// +public partial class OperatorCheckboxViewModel : ObservableObject +{ + /// + /// ID dell'educatore + /// + [ObservableProperty] + private Guid _educatorId; + + /// + /// Nome completo educatore + /// + [ObservableProperty] + private string _fullName = string.Empty; + + /// + /// Indica se questo è l'operatore che sta registrando la visita (io) + /// + [ObservableProperty] + private bool _isCurrentUser; + + /// + /// Indica se l'operatore era presente durante la visita + /// + [ObservableProperty] + private bool _isSelected; + + /// + /// Display name con indicatore (io) se è l'utente corrente + /// + public string DisplayName => IsCurrentUser ? $"{FullName} (io)" : FullName; + + partial void OnIsCurrentUserChanged(bool value) + { + OnPropertyChanged(nameof(DisplayName)); + } + + partial void OnFullNameChanged(string value) + { + OnPropertyChanged(nameof(DisplayName)); + } +} From fe25acf34ed1ffdf470e21ccf68176d6c02b809f Mon Sep 17 00:00:00 2001 From: Marco Cavallo Date: Sat, 14 Feb 2026 18:27:20 +0100 Subject: [PATCH 05/31] feat(visits): add VisitFormViewModel with validation --- .../Visits/VisitFormViewModel.cs | 320 ++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 src/PTRP.ViewModels/Visits/VisitFormViewModel.cs diff --git a/src/PTRP.ViewModels/Visits/VisitFormViewModel.cs b/src/PTRP.ViewModels/Visits/VisitFormViewModel.cs new file mode 100644 index 0000000..031eb02 --- /dev/null +++ b/src/PTRP.ViewModels/Visits/VisitFormViewModel.cs @@ -0,0 +1,320 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using System.Collections.ObjectModel; +using System.ComponentModel.DataAnnotations; + +namespace PTRP.ViewModels.Visits; + +/// +/// ViewModel per VisitFormView - Registrazione visita effettiva +/// +public partial class VisitFormViewModel : ViewModelBase +{ + public override string DisplayName => "Registrazione Visita"; + + #region Read-Only Info (da appuntamento) + + /// + /// ID dell'appuntamento programmato (ScheduledVisit) + /// + [ObservableProperty] + private Guid _scheduledVisitId; + + /// + /// Nome completo paziente + /// + [ObservableProperty] + private string _patientName = string.Empty; + + /// + /// Tipo appuntamento display ("Prima Apertura", "Verifica Intermedia", etc.) + /// + [ObservableProperty] + private string _appointmentTypeDisplay = string.Empty; + + /// + /// Data programmata appuntamento + /// + [ObservableProperty] + private DateTime _scheduledDate; + + /// + /// Data programmata formattata + /// + public string ScheduledDateDisplay => ScheduledDate.ToString("dd/MM/yyyy HH:mm"); + + #endregion + + #region Editable Fields + + /// + /// Data effettiva visita (non può essere futura) + /// + [ObservableProperty] + [Required(ErrorMessage = "La data effettiva è obbligatoria")] + private DateTime _actualDate = DateTime.Today; + + /// + /// Ora inizio visita + /// + [ObservableProperty] + [Required(ErrorMessage = "L'ora di inizio è obbligatoria")] + private TimeSpan _startTime = new TimeSpan(9, 0, 0); + + /// + /// Ora fine visita (deve essere > ora inizio) + /// + [ObservableProperty] + [Required(ErrorMessage = "L'ora di fine è obbligatoria")] + private TimeSpan _endTime = new TimeSpan(10, 0, 0); + + /// + /// Note cliniche (obbligatorio) + /// + [ObservableProperty] + [Required(ErrorMessage = "Le note cliniche sono obbligatorie")] + [MinLength(10, ErrorMessage = "Le note devono contenere almeno 10 caratteri")] + private string _clinicalNotes = string.Empty; + + /// + /// Esiti e obiettivi (opzionale) + /// + [ObservableProperty] + private string _outcomes = string.Empty; + + /// + /// Stato presenza paziente + /// + [ObservableProperty] + [Required(ErrorMessage = "Selezionare lo stato di presenza del paziente")] + private string _selectedPresenceStatus = "PresentCollaborative"; + + /// + /// Opzioni presenza paziente + /// + [ObservableProperty] + private ObservableCollection _presenceStatusOptions = new() + { + new PresenceStatusItem { Value = "PresentCollaborative", Display = "Presente e Collaborativo" }, + new PresenceStatusItem { Value = "PresentNonCollaborative", Display = "Presente ma Non Collaborativo" }, + new PresenceStatusItem { Value = "AbsentJustified", Display = "Assente Giustificato" }, + new PresenceStatusItem { Value = "AbsentNotJustified", Display = "Assente Non Giustificato" } + }; + + /// + /// Lista operatori disponibili (checkbox) + /// + [ObservableProperty] + private ObservableCollection _availableOperators = new(); + + /// + /// Operatori selezionati (presenti durante visita) + /// + public IEnumerable SelectedOperators => + AvailableOperators.Where(o => o.IsSelected); + + /// + /// Numero operatori selezionati + /// + public int SelectedOperatorsCount => SelectedOperators.Count(); + + #endregion + + #region Validation + + /// + /// Errori di validazione + /// + [ObservableProperty] + private ObservableCollection _validationErrors = new(); + + /// + /// Indica se ci sono errori di validazione + /// + public bool HasValidationErrors => ValidationErrors.Count > 0; + + /// + /// Valida tutti i campi del form + /// + private bool ValidateForm() + { + ValidationErrors.Clear(); + + // Data effettiva non futura + if (ActualDate.Date > DateTime.Today) + { + ValidationErrors.Add("La data effettiva non può essere futura"); + } + + // Ora fine > ora inizio + if (EndTime <= StartTime) + { + ValidationErrors.Add("L'ora di fine deve essere successiva all'ora di inizio"); + } + + // Almeno un operatore selezionato + if (SelectedOperatorsCount == 0) + { + ValidationErrors.Add("Selezionare almeno un operatore presente"); + } + + // Note cliniche obbligatorie e lunghezza minima + if (string.IsNullOrWhiteSpace(ClinicalNotes)) + { + ValidationErrors.Add("Le note cliniche sono obbligatorie"); + } + else if (ClinicalNotes.Length < 10) + { + ValidationErrors.Add("Le note cliniche devono contenere almeno 10 caratteri"); + } + + // Presenza paziente selezionata + if (string.IsNullOrWhiteSpace(SelectedPresenceStatus)) + { + ValidationErrors.Add("Selezionare lo stato di presenza del paziente"); + } + + OnPropertyChanged(nameof(HasValidationErrors)); + return ValidationErrors.Count == 0; + } + + #endregion + + #region Commands + + /// + /// Salva visita + /// + [RelayCommand] + private async Task SaveVisitAsync() + { + if (!ValidateForm()) + { + // Mostra errori (già popolati in ValidationErrors) + return; + } + + try + { + // TODO: Chiamare IActualVisitService.RegisterVisitAsync(...) + // Creare ActualVisitModel con dati dal form + // Collegare a ScheduledVisitId + // Salvare operatori presenti + + await Task.Delay(500); // Simula save + + // Chiudi form e torna al calendario + // TODO: NavigationService.GoBack() o chiudi dialog + } + catch (Exception ex) + { + ValidationErrors.Add($"Errore durante il salvataggio: {ex.Message}"); + } + } + + /// + /// Annulla registrazione e torna indietro + /// + [RelayCommand] + private void Cancel() + { + // TODO: NavigationService.GoBack() o chiudi dialog + } + + #endregion + + #region Initialization + + /// + /// Inizializza form con dati da appuntamento + /// + public void InitializeFromAppointment(Guid scheduledVisitId, string patientName, + string appointmentType, DateTime scheduledDate, List assignedEducatorIds) + { + ScheduledVisitId = scheduledVisitId; + PatientName = patientName; + AppointmentTypeDisplay = appointmentType; + ScheduledDate = scheduledDate; + + // Imposta data effettiva = data programmata (modificabile) + ActualDate = scheduledDate.Date; + StartTime = scheduledDate.TimeOfDay; + EndTime = scheduledDate.TimeOfDay.Add(new TimeSpan(1, 0, 0)); // +1 ora default + + // Carica operatori assegnati al progetto + LoadAvailableOperators(assignedEducatorIds); + } + + /// + /// Carica lista operatori disponibili + /// + private void LoadAvailableOperators(List assignedEducatorIds) + { + AvailableOperators.Clear(); + + // TODO: Caricare da service reale + // MOCK per testing + var currentUserId = Guid.NewGuid(); // TODO: Ottenere da context + + AvailableOperators.Add(new OperatorCheckboxViewModel + { + EducatorId = currentUserId, + FullName = "Mario Bianchi", + IsCurrentUser = true, + IsSelected = true // Pre-selezionato + }); + + AvailableOperators.Add(new OperatorCheckboxViewModel + { + EducatorId = Guid.NewGuid(), + FullName = "Laura Verdi", + IsCurrentUser = false, + IsSelected = false + }); + + AvailableOperators.Add(new OperatorCheckboxViewModel + { + EducatorId = Guid.NewGuid(), + FullName = "Giovanni Rossi", + IsCurrentUser = false, + IsSelected = false + }); + } + + #endregion + + partial void OnScheduledDateChanged(DateTime value) + { + OnPropertyChanged(nameof(ScheduledDateDisplay)); + } + + partial void OnAvailableOperatorsChanged(ObservableCollection value) + { + // Subscribe to changes in IsSelected + foreach (var op in value) + { + op.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(OperatorCheckboxViewModel.IsSelected)) + { + OnPropertyChanged(nameof(SelectedOperators)); + OnPropertyChanged(nameof(SelectedOperatorsCount)); + } + }; + } + } + + partial void OnValidationErrorsChanged(ObservableCollection value) + { + OnPropertyChanged(nameof(HasValidationErrors)); + } +} + +/// +/// Helper per item presenza paziente +/// +public class PresenceStatusItem +{ + public string Value { get; set; } = string.Empty; + public string Display { get; set; } = string.Empty; +} From a5f070ddd425a60a41467f393a3ab034b4a900d6 Mon Sep 17 00:00:00 2001 From: Marco Cavallo Date: Sat, 14 Feb 2026 18:28:14 +0100 Subject: [PATCH 06/31] feat(calendar): add CalendarView XAML with month grid and appointments list --- src/PTRP.App/Views/Calendar/CalendarView.xaml | 291 ++++++++++++++++++ .../Views/Calendar/CalendarView.xaml.cs | 11 + 2 files changed, 302 insertions(+) create mode 100644 src/PTRP.App/Views/Calendar/CalendarView.xaml create mode 100644 src/PTRP.App/Views/Calendar/CalendarView.xaml.cs diff --git a/src/PTRP.App/Views/Calendar/CalendarView.xaml b/src/PTRP.App/Views/Calendar/CalendarView.xaml new file mode 100644 index 0000000..9ec58d0 --- /dev/null +++ b/src/PTRP.App/Views/Calendar/CalendarView.xaml @@ -0,0 +1,291 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MinWidth="150" TextAlignment="Center" + Margin="5,0" /> @@ -138,16 +141,16 @@ - - + + - + - + From 7767eae926dbf12d224f14451cdf94f83efffd74 Mon Sep 17 00:00:00 2001 From: Marco Cavallo Date: Sat, 14 Feb 2026 19:00:25 +0100 Subject: [PATCH 22/31] fix: replace Spacing with Margin in VisitFormView (WPF compatibility) - issue #75 --- src/PTRP.App/Views/Visits/VisitFormView.xaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/PTRP.App/Views/Visits/VisitFormView.xaml b/src/PTRP.App/Views/Visits/VisitFormView.xaml index 622d9a9..a7d2c1f 100644 --- a/src/PTRP.App/Views/Visits/VisitFormView.xaml +++ b/src/PTRP.App/Views/Visits/VisitFormView.xaml @@ -152,11 +152,12 @@ Style="{StaticResource MaterialDesignFloatingHintComboBox}" /> - +