LSFE/LSFE.MobApp/ViewModels/Coverage/DoctorsVM.cs
2026-07-30 10:31:04 +08:00

705 lines
23 KiB
C#

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<Doctor> _doctorApiClient;
private readonly IApiClient<Institution> _institutionApiClient;
#endregion
#region Local repository
private readonly Repository<Institution> _institutionRepository;
private readonly Repository<MRRawPlan> _mrRawPlanRepository;
#endregion
#region ObservableCollection & List
private ObservableCollection<DoctorVM> _doctors;
private ObservableCollection<DoctorVM> _filteredDoctors;
public ObservableCollection<DoctorVM> Doctors
{
get => _doctors;
set
{
_doctors = value;
OnPropertyChanged();
}
}
public ObservableCollection<DoctorVM> FilteredDoctors
{
get => _filteredDoctors;
set
{
_filteredDoctors = value;
OnPropertyChanged();
OnPropertyChanged(nameof(IsEmpty));
OnPropertyChanged(nameof(ResultsCountText));
}
}
private CancellationTokenSource _searchCancellationTokenSource;
private List<Institution> _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<Doctor> doctorRepository,
Repository<Institution> institutionRepository,
Repository<MRRawPlan> mrRawPlanRepository,
IConfiguration configuration,
AddressService addressService)
: base(doctorRepository, syncService, addressService, configuration)
{
_doctorApiClient = apiClientFactory.CreateClient<Doctor>("DoctorMgmt/GetMyDoctor");
_institutionApiClient = apiClientFactory.CreateClient<Institution>("Maintenance/institutions");
_syncService = syncService;
_configuration = configuration;
_addressService = addressService;
_navigationService = navigationService;
_institutionRepository = institutionRepository;
_mrRawPlanRepository= mrRawPlanRepository;
Doctors = new ObservableCollection<DoctorVM>();
FilteredDoctors = new ObservableCollection<DoctorVM>();
_institutions = new List<Institution>();
StatusFilterOptions = new List<string> { "All Statuses", "Pending", "Started", "Completed", "Reschedule", "Missed" };
InstitutionFilterOptions = new List<string> { "All Institutions" };
_searchText = string.Empty;
_selectedStatusFilter = "All Statuses";
_selectedInstitutionFilter = "All Institutions";
// Commands
AddDoctorCommand = new Command(async () => await AddDoctor(), () => CanInteract);
ViewDoctorCommand = new Command<DoctorVM>(async (doctor) => await ViewDoctor(doctor), (doctor) => CanInteract);
EditDoctorCommand = new Command<DoctorVM>(async (doctor) => await EditDoctor(doctor), (doctor) => CanInteract);
DeleteDoctorCommand = new Command<DoctorVM>(async (doctor) => await DeleteDoctor(doctor), (doctor) => CanInteract);
CoverageCommand = new Command<DoctorVM>(async (doctor) => await Coverage(doctor), (doctor) => CanInteract);
SignOutCommand = new Command(async (doctor) => await LogOut());
RescheduleCommand = new Command<DoctorVM>(async doctor =>
{
await ShowReason.ShowReasonModal("Reschedule Reason", async reason =>
{
await Reschedule(doctor.Doctor.DoctorId, reason);
});
}, doctor => CanInteract);
MissedCommand = new Command<DoctorVM>(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<string> _institutionFilterOptions;
public List<string> 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<string> 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)
/// <summary>
/// Automatically called when any doctor status is updated anywhere in the app
/// </summary>
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<bool> 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<string> { "All Institutions" }
.Concat(_institutions.Select(i => i.InstitutionName)
.Where(n => !string.IsNullOrEmpty(n))
.OrderBy(n => n))
.ToList();
}
else
{
InstitutionFilterOptions = new List<string> { "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<DoctorVM>(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<DoctorVM>)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<DoctorVM>)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<AddEditDoctorVM>(this, "DoctorSaved", async (sender) =>
{
await LoadDataAsync();
MessagingCenter.Unsubscribe<AddEditDoctorVM>(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<DoctorVM>)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<DoctorVM>)ViewDoctorCommand).ChangeCanExecute();
}
}
private async Task EditDoctor(DoctorVM doctor)
{
if (doctor == null || IsProcessing) return;
try
{
IsProcessing = true;
((Command<DoctorVM>)EditDoctorCommand).ChangeCanExecute();
var editVm = new AddEditDoctorVM(_doctorApiClient, _institutionApiClient, _institutions, doctor.Doctor);
var editPage = new AddEditDoctorPage(editVm);
MessagingCenter.Subscribe<AddEditDoctorVM>(this, "DoctorSaved", async (sender) =>
{
await LoadDataAsync();
MessagingCenter.Unsubscribe<AddEditDoctorVM>(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<DoctorVM>)EditDoctorCommand).ChangeCanExecute();
}
}
private async Task DeleteDoctor(DoctorVM doctor)
{
if (doctor == null || IsProcessing) return;
try
{
IsProcessing = true;
((Command<DoctorVM>)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<DoctorVM>)DeleteDoctorCommand).ChangeCanExecute();
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_searchCancellationTokenSource?.Cancel();
_searchCancellationTokenSource?.Dispose();
}
base.Dispose(disposing);
}
#endregion
}
/// <summary>
/// Doctor View Model for UI binding - Now using centralized status helper
/// </summary>
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
}