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

544 lines
19 KiB
C#

using LSFE.MobApp.Contracts;
using LSFE.MobApp.Entities.Doctor;
using LSFE.MobApp.Helpers;
using LSFE.MobApp.Models;
using LSFE.MobApp.Services;
using LSFE.MobApp.ViewModels.Common;
using Microsoft.Extensions.Configuration;
using System.Windows.Input;
namespace LSFE.MobApp.ViewModels.Coverage
{
public class DoctorSignatureVM : BaseDoctorVM
{
private readonly List<ProductMaterial> _selectedMaterials;
private readonly List<SamplingItem> _selectedSamplings;
private Action<byte[], string> _onSignatureCompleted;
private readonly AddressService _addressService;
#region Repository
private readonly Repository<Entities.Planning.MRRawPlan> _mrRawPlanRepository;
private readonly Repository<Entities.Doctor.Doctor> _doctorRepository;
#endregion
private readonly Entities.Doctor.Doctor _doctor;
public DoctorSignatureVM(Entities.Doctor.Doctor doctor,
AddressService addressService,
ISyncService syncService,
Repository<Entities.Planning.MRRawPlan> mrRawPlanRepository,
Repository<Entities.Doctor.Doctor> doctorRepository, IConfiguration configuration,
List<ProductMaterial> selectedMaterials,
List<SamplingItem> selectedSamplings,
Action<byte[], string> onSignatureCompleted = null)
: base(doctorRepository, syncService, addressService, configuration)
{
_doctor=doctor;
_mrRawPlanRepository = mrRawPlanRepository;
_doctorRepository=doctorRepository;
_selectedMaterials = selectedMaterials;
_selectedSamplings = selectedSamplings;
_onSignatureCompleted = onSignatureCompleted;
_addressService = addressService;
// Initialize commands
SelectDrawTabCommand = new Command(SelectDrawTab);
SelectPhotoTabCommand = new Command(SelectPhotoTab);
ClearSignatureCommand = new Command(ClearSignature);
PreviewSignatureCommand = new Command(async () => await PreviewSignature());
TakePhotoCommand = new Command(async () => await TakePhoto());
RemovePhotoCommand = new Command(RemovePhoto);
ConfirmSignatureCommand = new Command(async () => await ConfirmSignature());
CancelCommand = new Command(async () => await Cancel());
// Default to draw tab
IsDrawTabSelected = true;
}
#region Status Properties - Using Helper (DRY!)
public string StatusText => DoctorStatusHelper.GetStatusText(_doctor.Status);
public Color StatusBackgroundColor => DoctorStatusHelper.GetStatusBackgroundColor(_doctor.Status);
public Color StatusTextColor => DoctorStatusHelper.GetStatusTextColor(_doctor.Status);
#endregion
/// <summary>
/// This method is automatically called when status updates are broadcast
protected override async void OnDoctorStatusUpdated(int doctorId)
{
// Only update if it's this doctor
if (_doctor.DoctorId == doctorId)
{
var updated = await RefreshDoctorFromRepositoryAsync(doctorId);
if (updated != null)
{
_doctor.Status = updated.Status;
// Notify UI to refresh
OnPropertyChanged(nameof(StatusText));
OnPropertyChanged(nameof(StatusBackgroundColor));
OnPropertyChanged(nameof(StatusTextColor));
}
}
}
#region Properties
private bool _isLoading;
public bool IsLoading
{
get => _isLoading;
set
{
_isLoading = value;
OnPropertyChanged();
}
}
private bool _isDrawTabSelected;
public bool IsDrawTabSelected
{
get => _isDrawTabSelected;
set
{
_isDrawTabSelected = value;
OnPropertyChanged();
OnPropertyChanged(nameof(IsPhotoTabSelected));
OnPropertyChanged(nameof(DrawTabBackgroundColor));
OnPropertyChanged(nameof(DrawTabTextColor));
OnPropertyChanged(nameof(PhotoTabBackgroundColor));
OnPropertyChanged(nameof(PhotoTabTextColor));
OnPropertyChanged(nameof(ShowClearButton));
}
}
public bool IsPhotoTabSelected => !_isDrawTabSelected;
public bool ShowClearButton => _isDrawTabSelected && !IsSignaturePadEmpty;
public Color DrawTabBackgroundColor => _isDrawTabSelected ? Color.FromArgb("#0EA5E9") : Color.FromArgb("#F3F4F6");
public Color DrawTabTextColor => _isDrawTabSelected ? Colors.White : Color.FromArgb("#6B7280");
public Color PhotoTabBackgroundColor => !_isDrawTabSelected ? Color.FromArgb("#0EA5E9") : Color.FromArgb("#F3F4F6");
public Color PhotoTabTextColor => !_isDrawTabSelected ? Colors.White : Color.FromArgb("#6B7280");
public string DoctorName => $"{_doctor.FirstName} {_doctor.MiddleInitial} {_doctor.LastName}";
public string DoctorSpecialization => _doctor.SpecializationName;
public string SelectedItemsSummary
{
get
{
var summary = "";
if (_selectedMaterials?.Any() == true)
{
summary += $"📄 {_selectedMaterials.Count} Marketing Material(s)\n";
}
if (_selectedSamplings?.Any() == true)
{
summary += $"💊 {_selectedSamplings.Count} Sampling Item(s)";
}
return summary.Trim();
}
}
private ImageSource _signaturePhotoSource;
public ImageSource SignaturePhotoSource
{
get => _signaturePhotoSource;
set
{
_signaturePhotoSource = value;
OnPropertyChanged();
OnPropertyChanged(nameof(HasSignaturePhoto));
OnPropertyChanged(nameof(HasNoSignaturePhoto));
OnPropertyChanged(nameof(CanConfirm));
}
}
public bool HasSignaturePhoto => _signaturePhotoSource != null;
public bool HasNoSignaturePhoto => _signaturePhotoSource == null;
private byte[] _signaturePhotoBytes;
private ImageSource _finalSignatureSource;
public ImageSource FinalSignatureSource
{
get => _finalSignatureSource;
set
{
_finalSignatureSource = value;
OnPropertyChanged();
OnPropertyChanged(nameof(HasFinalSignature));
}
}
public bool HasFinalSignature => _finalSignatureSource != null;
private byte[] _finalSignatureBytes;
private string _signatureTimestamp;
public string SignatureTimestamp
{
get => _signatureTimestamp;
set
{
_signatureTimestamp = value;
OnPropertyChanged();
}
}
public bool CanConfirm => HasFinalSignature || HasSignaturePhoto || (_isDrawTabSelected && !IsSignaturePadEmpty);
private bool _isSignaturePadEmpty = true;
public bool IsSignaturePadEmpty
{
get => _isSignaturePadEmpty;
set
{
_isSignaturePadEmpty = value;
OnPropertyChanged();
OnPropertyChanged(nameof(CanConfirm));
OnPropertyChanged(nameof(ShowClearButton));
}
}
#endregion
#region Commands
public ICommand SelectDrawTabCommand { get; }
public ICommand SelectPhotoTabCommand { get; }
public ICommand ClearSignatureCommand { get; set; }
public ICommand PreviewSignatureCommand { get; set; }
public ICommand TakePhotoCommand { get; }
public ICommand ChoosePhotoCommand { get; }
public ICommand RemovePhotoCommand { get; }
public ICommand ConfirmSignatureCommand { get; }
public ICommand CancelCommand { get; }
#endregion
#region Methods
private void SelectDrawTab()
{
IsDrawTabSelected = true;
}
private void SelectPhotoTab()
{
IsDrawTabSelected = false;
}
public async Task PreviewSignature()
{
try
{
IsLoading = true;
await Application.Current.MainPage.DisplayAlert(
"Preview",
"Signature preview will be shown here",
"OK");
}
catch (Exception ex)
{
await Application.Current.MainPage.DisplayAlert("Error", $"Failed to preview: {ex.Message}", "OK");
}
finally
{
IsLoading = false;
}
}
private bool _isPhotoSignature;
public void SetSignatureFromPad(byte[] signatureBytes)
{
if (signatureBytes != null && signatureBytes.Length > 0)
{
_finalSignatureBytes = signatureBytes;
FinalSignatureSource = ImageSource.FromStream(() => new MemoryStream(signatureBytes));
SignatureTimestamp = $"Signed on {DateTime.Now:MMMM dd, yyyy hh:mm tt}";
_isPhotoSignature = false; // This is a drawn signature
}
}
private async Task ProcessPhoto(FileResult photo)
{
try
{
IsLoading = true;
using var stream = await photo.OpenReadAsync();
using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream);
_signaturePhotoBytes = memoryStream.ToArray();
SignaturePhotoSource = ImageSource.FromStream(() => new MemoryStream(_signaturePhotoBytes));
// Set as final signature
_finalSignatureBytes = _signaturePhotoBytes;
FinalSignatureSource = SignaturePhotoSource;
SignatureTimestamp = $"Captured on {DateTime.Now:MMMM dd, yyyy hh:mm tt}";
_isPhotoSignature = true;
}
catch (Exception ex)
{
await Application.Current.MainPage.DisplayAlert("Error", $"Failed to process photo: {ex.Message}", "OK");
}
finally
{
IsLoading = false;
}
}
private async Task TakePhoto()
{
try
{
if (!MediaPicker.Default.IsCaptureSupported)
{
await Application.Current.MainPage.DisplayAlert(
"Not Supported",
"Camera is not supported on this device",
"OK");
return;
}
var photo = await MediaPicker.Default.CapturePhotoAsync();
if (photo != null)
{
await ProcessPhoto(photo);
}
}
catch (PermissionException)
{
await Application.Current.MainPage.DisplayAlert(
"Permission Denied",
"Camera permission is required to take photos. Please enable it in settings.",
"OK");
}
catch (Exception ex)
{
await Application.Current.MainPage.DisplayAlert("Error", $"Failed to take photo: {ex.Message}", "OK");
}
}
private void RemovePhoto()
{
SignaturePhotoSource = null;
_signaturePhotoBytes = null;
FinalSignatureSource = null;
_finalSignatureBytes = null;
SignatureTimestamp = null;
}
#region Facade Actual Coverage Confirmation
private async Task ConfirmSignature()
{
try
{
// STEP 1: Ensure signature exists
if (!await EnsureSignatureIsValid())
return;
// STEP 2: Ask user to confirm
if (!await ConfirmSignatureSave())
return;
IsLoading = true;
// STEP 3: Save signature file
var fileName = await SaveSignatureFile();
if (fileName == null)
return;
_onSignatureCompleted?.Invoke(_finalSignatureBytes, fileName);
// STEP 4: Update plan
var plan = await GetTodayPlan();
if (plan == null)
return;
var doctor = await _doctorRepository.FirstOrDefaultAsync(d => d.MRRawPlanId == _doctor.MRRawPlanId);
if (doctor == null)
{
await UtilityVM.ShowAlert("Error: ", $"Doctor not found.");
return;
}
await UpdatePlanWithSignature(plan, doctor, fileName);
// STEP 5: Sync
await PerformSyncAndNotify();
// STEP 6: Navigate back
await Application.Current.MainPage.Navigation.PopAsync();
}
catch (Exception ex)
{
await UtilityVM.ShowAlert("Error: ",$"Failed to save signature: {ex.Message}");
}
finally
{
IsLoading = false;
}
}
private async Task PerformSyncAndNotify()
{
bool isSynced = await _syncService.SyncPostCoverageTodayAsync(_doctor.MRRawPlanId);
// Broadcast to ALL ViewModels - ONE LINE!
DoctorStatusHelper.BroadcastStatusUpdate(_doctor.DoctorId);
string message = isSynced
? "Actual coverage was saved and synced successfully."
: "Saved locally. Please sync again later.";
await Application.Current.MainPage.DisplayAlert("Success", message, "OK");
}
private async Task<bool> ConfirmSignatureSave()
{
return await Application.Current.MainPage.DisplayAlert(
"Confirm Signature",
"Are you sure you want to save this signature?",
"Yes, Save",
"No"
);
}
private async Task<bool> EnsureSignatureIsValid()
{
if (_isDrawTabSelected && _finalSignatureBytes == null)
{
var bytes = await GetSignatureBytesFromPad();
if (bytes == null || bytes.Length == 0)
{
await UtilityVM.ShowAlert("Signature: ","Please draw a signature before confirming.");
return false;
}
SetSignatureFromPad(bytes);
}
if (_finalSignatureBytes == null || _finalSignatureBytes.Length == 0)
{
await UtilityVM.ShowAlert("Signature: ", "Please provide a signature before confirming.");
return false;
}
return true;
}
private async Task<string> SaveSignatureFile()
{
var fileName = $"signature_{DateTime.Now:yyyyMMdd_HHmmss}.png";
var saved = await SaveSignatureToFile(_finalSignatureBytes, fileName);
if (string.IsNullOrWhiteSpace(saved))
{
await UtilityVM.ShowAlert("Saving signature: ", "Failed to save signature file.");
return null;
}
return fileName;
}
private async Task<Entities.Planning.MRRawPlan?> GetTodayPlan()
{
var today = DateTime.Today;
var tomorrow = today.AddDays(1);
var plan = await _mrRawPlanRepository.FirstOrDefaultAsync(
r => r.CallDate >= today &&
r.CallDate < tomorrow &&
r.DoctorId == _doctor.DoctorId);
if (plan == null)
await UtilityVM.ShowAlert("Actual coverage: ", "No plan found for today. Cannot save signature.");
return plan;
}
private async Task UpdatePlanWithSignature(
Entities.Planning.MRRawPlan plan,Doctor doctor, string fileName)
{
var (locationResult,isReadableAddress) = await _addressService.GetCurrentLocationAsync();
if (plan.EndTime != null)
return;
plan.SignatureFileName = fileName;
plan.Latitude = locationResult.Latitude.ToString();
plan.Longitude = locationResult.Longitude.ToString();
plan.EndTime = DateTime.Now;
plan.Location = locationResult.Address;
plan.IsSignature = _isPhotoSignature;
plan.IsMissed = false;
plan.IsReschedule = false;
plan.Status = 2; // completed
plan.ForAddressTranslation = isReadableAddress ? false : true;
doctor.Status = 2; // completed
await _mrRawPlanRepository.UpdateAsync(plan);
await _doctorRepository.UpdateAsync(doctor);
}
#endregion
private Func<Task<byte[]>> _getSignatureBytesFromPad;
public void SetGetSignatureBytesAction(Func<Task<byte[]>> getSignatureBytesAction)
{
_getSignatureBytesFromPad = getSignatureBytesAction;
}
private async Task<byte[]> GetSignatureBytesFromPad()
{
if (_getSignatureBytesFromPad != null)
{
return await _getSignatureBytesFromPad();
}
return null;
}
public static string GetFullSignaturePath(string fileName)
{
return Path.Combine(FileSystem.AppDataDirectory, fileName);
}
private async Task<string> SaveSignatureToFile(byte[] imageBytes, string fileName)
{
try
{
// Save to app data directory (just like your photo handling)
var localFilePath = Path.Combine(FileSystem.AppDataDirectory, fileName);
await File.WriteAllBytesAsync(localFilePath, imageBytes);
// Return only the filename (not the full path)
return fileName;
}
catch (Exception ex)
{
throw new Exception($"Failed to save signature file: {ex.Message}");
}
}
private async Task Cancel()
{
var confirm = await Application.Current.MainPage.DisplayAlert(
"Cancel",
"Are you sure you want to cancel? Any unsaved signature will be lost.",
"Yes, Cancel",
"No");
if (confirm)
{
await Application.Current.MainPage.Navigation.PopAsync();
}
}
private Action _clearSignaturePad;
public void SetClearSignaturePadAction(Action clearAction)
{
_clearSignaturePad = clearAction;
}
public void ClearSignature()
{
// Clear the final signature
FinalSignatureSource = null;
_finalSignatureBytes = null;
SignatureTimestamp = null;
// Clear the actual signature pad if the action is set
_clearSignaturePad?.Invoke();
OnPropertyChanged(nameof(CanConfirm));
}
#endregion
}
}