using System.Collections.ObjectModel; using LSFE.MobApp.Contracts; using LSFE.MobApp.Entities.Doctor; using LSFE.MobApp.Entities.Location; using LSFE.MobApp.Entities.Planning; using LSFE.MobApp.Helpers; using LSFE.MobApp.Services; using LSFE.MobApp.ViewModels.Common; using Microsoft.Extensions.Configuration; using System.Windows.Input; using LSFE.MobApp.Views.CoveragePlan; using System.ComponentModel; using LSFE.MobApp.Pages.CoveragePlan; using System.Runtime.CompilerServices; namespace LSFE.MobApp.ViewModels.Coverage; public class DoctorsVM : BaseDoctorVM { #region Api client private readonly IApiClient _doctorApiClient; private readonly IApiClient _institutionApiClient; #endregion #region Local repository private readonly Repository _institutionRepository; private readonly Repository _mrRawPlanRepository; #endregion #region ObservableCollection & List private ObservableCollection _doctors; private ObservableCollection _filteredDoctors; public ObservableCollection Doctors { get => _doctors; set { _doctors = value; OnPropertyChanged(); } } public ObservableCollection FilteredDoctors { get => _filteredDoctors; set { _filteredDoctors = value; OnPropertyChanged(); OnPropertyChanged(nameof(IsEmpty)); OnPropertyChanged(nameof(ResultsCountText)); } } private CancellationTokenSource _searchCancellationTokenSource; private List _institutions; #endregion #region Service & configuration private readonly ISyncService _syncService; private readonly IConfiguration _configuration; private readonly INavigationService _navigationService; private readonly AddressService _addressService; #endregion public DoctorsVM( IApiClientFactory apiClientFactory, ISyncService syncService, INavigationService navigationService, Repository doctorRepository, Repository institutionRepository, Repository mrRawPlanRepository, IConfiguration configuration, AddressService addressService) : base(doctorRepository, syncService, addressService, configuration) { _doctorApiClient = apiClientFactory.CreateClient("DoctorMgmt/GetMyDoctor"); _institutionApiClient = apiClientFactory.CreateClient("Maintenance/institutions"); _syncService = syncService; _configuration = configuration; _addressService = addressService; _navigationService = navigationService; _institutionRepository = institutionRepository; _mrRawPlanRepository= mrRawPlanRepository; Doctors = new ObservableCollection(); FilteredDoctors = new ObservableCollection(); _institutions = new List(); StatusFilterOptions = new List { "All Statuses", "Pending", "Started", "Completed", "Reschedule", "Missed" }; InstitutionFilterOptions = new List { "All Institutions" }; _searchText = string.Empty; _selectedStatusFilter = "All Statuses"; _selectedInstitutionFilter = "All Institutions"; // Commands AddDoctorCommand = new Command(async () => await AddDoctor(), () => CanInteract); ViewDoctorCommand = new Command(async (doctor) => await ViewDoctor(doctor), (doctor) => CanInteract); EditDoctorCommand = new Command(async (doctor) => await EditDoctor(doctor), (doctor) => CanInteract); DeleteDoctorCommand = new Command(async (doctor) => await DeleteDoctor(doctor), (doctor) => CanInteract); CoverageCommand = new Command(async (doctor) => await Coverage(doctor), (doctor) => CanInteract); SignOutCommand = new Command(async (doctor) => await LogOut()); RescheduleCommand = new Command(async doctor => { await ShowReason.ShowReasonModal("Reschedule Reason", async reason => { await Reschedule(doctor.Doctor.DoctorId, reason); }); }, doctor => CanInteract); MissedCommand = new Command(async doctor => { await ShowReason.ShowReasonModal("Missed Reason", async reason => { await Missed(doctor.Doctor.DoctorId, reason); }); }, doctor => CanInteract); ClearSearchCommand = new Command(() => SearchText = string.Empty); } private async Task LogOut() { await _navigationService.NavigateToLoginPageAsync(); } #region Properties private bool _forAddressTranslation { get; set; } private string _searchText; private bool _isLoading; private bool _isRefreshing; private string _selectedStatusFilter = "All Statuses"; private string _selectedInstitutionFilter = "All Institutions"; private List _institutionFilterOptions; public List InstitutionFilterOptions { get => _institutionFilterOptions; set { _institutionFilterOptions = value; OnPropertyChanged(); } } public string SelectedInstitutionFilter { get => _selectedInstitutionFilter; set { _selectedInstitutionFilter = value; OnPropertyChanged(); ApplyFilters(); } } private bool _isProcessing; public bool CanInteract => !IsLoading && !IsProcessing; public bool IsProcessing { get => _isProcessing; set { _isProcessing = value; OnPropertyChanged(); OnPropertyChanged(nameof(CanInteract)); } } private bool _isFiltering; public bool IsFiltering { get => _isFiltering; set { _isFiltering = value; OnPropertyChanged(); } } public string SearchText { get => _searchText; set { _searchText = value; OnPropertyChanged(); OnPropertyChanged(nameof(HasSearchText)); _searchCancellationTokenSource?.Cancel(); _searchCancellationTokenSource = new CancellationTokenSource(); var token = _searchCancellationTokenSource.Token; Task.Run(async () => { await Task.Delay(300, token); if (!token.IsCancellationRequested) { MainThread.BeginInvokeOnMainThread(() => ApplyFilters()); } }, token); } } public bool HasSearchText => !string.IsNullOrWhiteSpace(SearchText); public string SelectedStatusFilter { get => _selectedStatusFilter; set { _selectedStatusFilter = value; OnPropertyChanged(); ApplyFilters(); } } public List StatusFilterOptions { get; } public bool IsLoading { get => _isLoading; set { _isLoading = value; OnPropertyChanged(); } } public bool IsEmpty => !IsLoading && (FilteredDoctors == null || FilteredDoctors.Count == 0); public string ResultsCountText => $"Showing {FilteredDoctors?.Count ?? 0} of {Doctors?.Count ?? 0} doctors"; #endregion #region Status Update Handler (from Base Class) /// /// Automatically called when any doctor status is updated anywhere in the app /// protected override async void OnDoctorStatusUpdated(int doctorId) { var doctorVM = Doctors.FirstOrDefault(d => d.DoctorId == doctorId); if (doctorVM != null) { var updated = await RefreshDoctorFromRepositoryAsync(doctorId); if (updated != null) { doctorVM.Doctor.Status = updated.Status; doctorVM.RefreshStatus(); } } } #endregion #region Commands public ICommand RescheduleCommand { get; } public ICommand MissedCommand { get; } public ICommand AddDoctorCommand { get; } public ICommand ViewDoctorCommand { get; } public ICommand EditDoctorCommand { get; } public ICommand DeleteDoctorCommand { get; } public ICommand ClearSearchCommand { get; } public ICommand CoverageCommand { get; } public ICommand SignOutCommand { get; } #endregion #region Status Update Methods private async Task UpdateDoctorAndPlanAsync(int doctorId, byte status, string reason) { var today = DateTime.Today; var tomorrow = today.AddDays(1); try { var (locationResult, isReadableAddress) = await _addressService.GetCurrentLocationAsync(); var doctor = await _doctorRepository.FirstOrDefaultAsync(d => d.DoctorId == doctorId); if (doctor == null) { await Application.Current.MainPage.DisplayAlert("Error", "Doctor not found.", "OK"); return; } doctor.Status = status; await _doctorRepository.UpdateAsync(doctor); var plan = await _mrRawPlanRepository.FirstOrDefaultAsync( p => p.DoctorId == doctorId && p.CallDate >= today && p.CallDate < tomorrow); if (plan == null) { await Application.Current.MainPage.DisplayAlert("Error", "No plan found for today.", "OK"); return; } string successMessage; if (status == 3) // Reschedule { plan.IsReschedule = true; plan.IsMissed = false; plan.RescheduleReason = reason; plan.MissedReason = null; successMessage = "Schedule has been successfully rescheduled."; } else // Missed { plan.IsMissed = true; plan.IsReschedule = false; plan.MissedReason = reason; plan.RescheduleReason = null; successMessage = "Schedule has been marked as missed."; } if (isReadableAddress) _forAddressTranslation = false; else _forAddressTranslation = true; plan.Latitude = locationResult.Latitude.ToString() ?? "0"; plan.Longitude = locationResult.Longitude.ToString() ?? "0"; plan.Location = locationResult.Address ?? "N/A"; await _mrRawPlanRepository.UpdateAsync(plan); // BROADCAST the status update to ALL ViewModels DoctorStatusHelper.BroadcastStatusUpdate(doctorId); await Application.Current.MainPage.DisplayAlert("Success", successMessage, "OK"); } catch (Exception ex) { await Application.Current.MainPage.DisplayAlert("Error", ex.Message, "OK"); } } private async Task Reschedule(int doctorId, string reason) { if (await IsCompleted()) return; await UpdateDoctorAndPlanAsync(doctorId, status: 3, reason); } private async Task Missed(int doctorId, string reason) { if (await IsCompleted()) return; await UpdateDoctorAndPlanAsync(doctorId, status: 4, reason); } private async Task IsCompleted() { if (FilteredDoctors[0].StatusText == "Completed") { await UtilityVM.ShowAlert("Status Update", $"Action failed: Dr. {Doctors[0].FullName}'s status is already marked as Completed."); return true; } return false; } public void RefreshData() { // Guard: skip if already loading or refreshing if (IsLoading || _isRefreshing) return; _isRefreshing = true; _ = LoadDataAsync(); } #endregion #region Data Loading & Filtering private async Task LoadDataAsync() { try { IsLoading = true; _institutions = await _institutionRepository.GetAllAsync(); if (_institutions != null && _institutions.Any()) { InstitutionFilterOptions = new List { "All Institutions" } .Concat(_institutions.Select(i => i.InstitutionName) .Where(n => !string.IsNullOrEmpty(n)) .OrderBy(n => n)) .ToList(); } else { InstitutionFilterOptions = new List { "All Institutions" }; } var today = DateTime.Today; var tomorrow = today.AddDays(1); var doctors = await _doctorRepository.GetAllAsync(); // FILTER → Only today's doctors var todayDoctors = doctors .Where(a => a.CreatedDate >= today && a.CreatedDate < tomorrow) .OrderByDescending(a => a.CreatedDate) .ToList(); Doctors.Clear(); // Load only today's doctors foreach (var doctor in todayDoctors) { var institution = _institutions?.FirstOrDefault(i => i.InstitutionId == doctor.InstitutionId); if (institution != null) { doctor.InstitutionName = institution.InstitutionName; } Doctors.Add(new DoctorVM(doctor)); } ApplyFilters(); } catch (Exception ex) { await UtilityVM.ShowAlert("Error", $"Failed to load doctors: {ex.Message}"); } finally { IsLoading = false; _isRefreshing = false; } } private async void ApplyFilters() { try { IsFiltering = true; var filtered = await Task.Run(() => { var result = Doctors.AsEnumerable(); if (!string.IsNullOrWhiteSpace(SearchText)) { var searchLower = SearchText.ToLower(); result = result.Where(d => d.FullName.ToLower().Contains(searchLower) || (d.EmailAddress?.ToLower().Contains(searchLower) ?? false) || (d.PhoneNo?.ToLower().Contains(searchLower) ?? false) || (d.LicenseNo?.ToLower().Contains(searchLower) ?? false) || (d.InstitutionName?.ToLower().Contains(searchLower) ?? false)); } if (SelectedStatusFilter != "All Statuses") { result = result.Where(d => d.StatusText == SelectedStatusFilter); } if (!string.IsNullOrEmpty(SelectedInstitutionFilter) && SelectedInstitutionFilter != "All Institutions") { result = result.Where(d => d.InstitutionName == SelectedInstitutionFilter); } return result.ToList(); }); FilteredDoctors = new ObservableCollection(filtered); } finally { IsFiltering = false; } } #endregion #region CRUD Operations private async Task Coverage(DoctorVM doctor) { if (doctor == null || IsProcessing) return; var doctorEntities = new Entities.Doctor.Doctor() { Status=doctor.Status, FirstName=doctor.FullName, DoctorId = doctor.DoctorId, SpecializationName=doctor.Specialization, InstitutionName = doctor.InstitutionName, LicenseNo=doctor.LicenseNo, MaxVisit=doctor.MaxVisit, BirthDate=doctor.BirthDate, MRRawPlanId=doctor.MRRawPlanId }; try { IsLoading = true; ((Command)CoverageCommand).ChangeCanExecute(); var viewVm = new ActualCoverageVM( doctorEntities, _doctorRepository, _mrRawPlanRepository, _syncService, _addressService, _configuration); var viewPage = new ActualCoveragePage(viewVm); await Application.Current.MainPage.Navigation.PushAsync(viewPage); } catch (Exception ex) { await UtilityVM.ShowAlert("Error", $"Failed to view doctor: {ex.Message}"); } finally { IsLoading = false; ((Command)CoverageCommand).ChangeCanExecute(); } } private async Task AddDoctor() { if (IsProcessing) return; // Prevent double-tap try { IsProcessing = true; ((Command)AddDoctorCommand).ChangeCanExecute(); // Disable button var addVm = new AddEditDoctorVM(_doctorApiClient, _institutionApiClient, _institutions); var addPage = new AddEditDoctorPage(addVm); MessagingCenter.Subscribe(this, "DoctorSaved", async (sender) => { await LoadDataAsync(); MessagingCenter.Unsubscribe(this, "DoctorSaved"); }); //await _navigation.PushModalAsync(new NavigationPage(addPage)); await Application.Current.MainPage.Navigation.PushModalAsync(new NavigationPage(addPage)); } catch (Exception ex) { await UtilityVM.ShowAlert("Error", $"Failed to open add page: {ex.Message}"); } finally { IsProcessing = false; ((Command)AddDoctorCommand).ChangeCanExecute(); // Re-enable button } } private async Task ViewDoctor(DoctorVM doctor) { if (doctor == null || IsProcessing) return; try { IsProcessing = true; ((Command)ViewDoctorCommand).ChangeCanExecute(); var viewVm = new ViewDoctorVM(doctor.Doctor); var viewPage = new ViewDoctorPage(viewVm); await Application.Current.MainPage.Navigation.PushAsync(viewPage); // await _navigation.PushAsync(viewPage); } catch (Exception ex) { await UtilityVM.ShowAlert("Error", $"Failed to view doctor: {ex.Message}"); } finally { IsProcessing = false; ((Command)ViewDoctorCommand).ChangeCanExecute(); } } private async Task EditDoctor(DoctorVM doctor) { if (doctor == null || IsProcessing) return; try { IsProcessing = true; ((Command)EditDoctorCommand).ChangeCanExecute(); var editVm = new AddEditDoctorVM(_doctorApiClient, _institutionApiClient, _institutions, doctor.Doctor); var editPage = new AddEditDoctorPage(editVm); MessagingCenter.Subscribe(this, "DoctorSaved", async (sender) => { await LoadDataAsync(); MessagingCenter.Unsubscribe(this, "DoctorSaved"); }); await Application.Current.MainPage.Navigation.PushAsync(editPage); //await _navigation.PushModalAsync(new NavigationPage(editPage)); } catch (Exception ex) { await UtilityVM.ShowAlert("Error", $"Failed to edit doctor: {ex.Message}"); } finally { IsProcessing = false; ((Command)EditDoctorCommand).ChangeCanExecute(); } } private async Task DeleteDoctor(DoctorVM doctor) { if (doctor == null || IsProcessing) return; try { IsProcessing = true; ((Command)DeleteDoctorCommand).ChangeCanExecute(); var confirm = await UtilityVM.ShowConfirm( "Confirm Delete", $"Are you sure you want to delete Dr. {doctor.FullName}?", "Delete", "Cancel"); if (!confirm) return; var response = await _doctorApiClient.DeleteAsync(doctor.Doctor.Id); if (response.Success) { Doctors.Remove(doctor); ApplyFilters(); await UtilityVM.ShowAlert("Success", "Doctor deleted successfully"); } else { await UtilityVM.ShowAlert("Error", response.Message ?? "Failed to delete doctor"); } } catch (Exception ex) { await UtilityVM.ShowAlert("Error", $"Failed to delete doctor: {ex.Message}"); } finally { IsProcessing = false; ((Command)DeleteDoctorCommand).ChangeCanExecute(); } } protected override void Dispose(bool disposing) { if (disposing) { _searchCancellationTokenSource?.Cancel(); _searchCancellationTokenSource?.Dispose(); } base.Dispose(disposing); } #endregion } /// /// Doctor View Model for UI binding - Now using centralized status helper /// public class DoctorVM : INotifyPropertyChanged { public Entities.Doctor.Doctor Doctor { get; } public DoctorVM(Entities.Doctor.Doctor doctor) { Doctor = doctor ?? throw new ArgumentNullException(nameof(doctor)); } #region Doctor Information public int DoctorId => Doctor.DoctorId; public Guid MRRawPlanId => Doctor.MRRawPlanId; public string FullName => $"Dr. {Doctor.FirstName} {Doctor.MiddleInitial} {Doctor.LastName}".Replace(" ", " ").Trim(); public string EmailAddress => Doctor.EmailAddress ?? "N/A"; public string PhoneNo => Doctor.PhoneNo ?? "N/A"; public string LicenseNo => Doctor.LicenseNo ?? "N/A"; public DateTime BirthDate => Doctor.BirthDate; public byte MaxVisit => Doctor.MaxVisit; public string Specialization => Doctor.SpecializationName ?? "General"; public string InstitutionName => Doctor.InstitutionName ?? "No Institution"; public bool HasInstitution => !string.IsNullOrEmpty(Doctor.InstitutionName); #endregion #region Status Changes public void UpdateDoctorModel(Entities.Doctor.Doctor updated) { if (updated == null) return; Doctor.Status = updated.Status; RefreshStatus(); } public void RefreshStatus() { OnPropertyChanged(nameof(StatusText)); OnPropertyChanged(nameof(StatusBackgroundColor)); OnPropertyChanged(nameof(StatusTextColor)); } #endregion #region Status Properties - Using Centralized Helper public string StatusText => DoctorStatusHelper.GetStatusText(Doctor.Status); public byte Status=> Doctor.Status; public Color StatusBackgroundColor => DoctorStatusHelper.GetStatusBackgroundColor(Doctor.Status); public Color StatusTextColor => DoctorStatusHelper.GetStatusTextColor(Doctor.Status); #endregion #region INotifyPropertyChanged public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } #endregion }