544 lines
19 KiB
C#
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
|
|
}
|
|
} |