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

483 lines
17 KiB
C#

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LSFE.MobApp.Contracts;
using LSFE.MobApp.Entities.Account;
using LSFE.MobApp.Helpers;
using LSFE.MobApp.Services;
using LSFE.MobApp.Services.Authentication;
using LSFE.MobApp.ViewModels.Common;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Graphics.Text;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LSFE.MobApp.ViewModels
{
public partial class AttendanceVM : ObservableObject
{
private readonly IRepository<Attendance> _attendanceRepository;
private readonly ILogger<AttendanceVM> _logger;
private readonly ISyncService _syncService;
private readonly AddressService _addressService;
private bool _forAddressTranslation { get; set; }
[ObservableProperty]
private string currentTime;
[ObservableProperty]
private string currentDate;
[ObservableProperty]
private string locationAddress = "Fetching location...";
[ObservableProperty]
private string latitude;
[ObservableProperty]
private string longitude;
[ObservableProperty]
private ImageSource capturedImageIn;
[ObservableProperty]
private ImageSource capturedImageOut;
[ObservableProperty]
private bool hasTimeIn;
[ObservableProperty]
private bool hasTimeOut;
[ObservableProperty]
private string timeInText = "Not clocked in";
[ObservableProperty]
private string timeOutText = "Not clocked out";
[ObservableProperty]
private bool isLoading;
[ObservableProperty]
private bool hasImage;
private Attendance _todayAttendance;
public AttendanceVM(IRepository<Attendance> attendanceRepository, ILogger<AttendanceVM> logger,
ISyncService syncService, AddressService addressService)
{
_addressService = addressService;
_syncService = syncService;
_attendanceRepository = attendanceRepository;
_logger = logger;
UpdateDateTime();
// Update time every second
Dispatcher.GetForCurrentThread()?.StartTimer(TimeSpan.FromSeconds(1), () =>
{
UpdateDateTime();
return true;
});
}
public async Task InitializeAsync()
{
await LoadTodayAttendanceAsync();
await GetLocation();
}
private void UpdateDateTime()
{
var now = DateTime.Now;
CurrentTime = now.ToString("hh:mm:ss tt");
CurrentDate = now.ToString("MMMM dd, yyyy");
}
private async Task LoadTodayAttendanceAsync()
{
try
{
var today = DateTime.Today;
// Get all attendance records for this user
var query = @"SELECT * FROM Attendance
WHERE UserId = ?
AND TimeIn IS NOT NULL
ORDER BY TimeIn DESC";
var cred = await UserCred.GetUserCred();
var allResults = await _attendanceRepository.QueryAsync(query, cred.UserId);
// Filter for today's record in memory
_todayAttendance = allResults
.Where(a => a.TimeIn.HasValue && a.TimeIn.Value.Date == today)
.OrderByDescending(a => a.TimeIn)
.FirstOrDefault();
if (_todayAttendance != null)
{
HasTimeIn = _todayAttendance.TimeIn.HasValue;
HasTimeOut = _todayAttendance.TimeOut.HasValue;
if (HasTimeIn)
TimeInText = _todayAttendance.TimeIn.Value.ToString("hh:mm tt");
if (HasTimeOut)
TimeOutText = _todayAttendance.TimeOut.Value.ToString("hh:mm tt");
// Load saved image if exists
await LoadSavedImageAsync();
_logger.LogInformation("Loaded attendance: TimeIn={TimeIn}, TimeOut={TimeOut}, Image={Image}",
_todayAttendance.TimeIn, _todayAttendance.TimeOut, _todayAttendance.SignatureFileNameIn,
_todayAttendance.SignatureFileNameOut);
}
else
{
// Reset values if no attendance found
HasTimeIn = false;
HasTimeOut = false;
TimeInText = "Not clocked in";
TimeOutText = "Not clocked out";
CapturedImageIn = null;
CapturedImageOut = null;
HasImage = false;
_todayAttendance = null;
_logger.LogInformation("No attendance record found for today");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading today's attendance");
// Reset to safe state on error
HasTimeIn = false;
HasTimeOut = false;
TimeInText = "Error loading data";
TimeOutText = "Error loading data";
CapturedImageIn = null;
CapturedImageOut = null;
HasImage = false;
}
}
private async Task<string?> CaptureAndSavePhotoAsync(bool isTimeOut = false)
{
try
{
var photo = await MediaPicker.Default.CapturePhotoAsync(new MediaPickerOptions
{
Title = isTimeOut ? "Take Time Out Photo" : "Take Time In Photo"
});
if (photo == null)
return null;
// Generate a unique filename
var fileName = $"{Guid.NewGuid()}{Path.GetExtension(photo.FileName)}";
var localFilePath = Path.Combine(FileSystem.AppDataDirectory, fileName);
// Save photo to local app directory
using (var sourceStream = await photo.OpenReadAsync())
using (var fileStream = File.Create(localFilePath))
{
await sourceStream.CopyToAsync(fileStream);
}
// Convert to Base64 (for syncing or storage later)
byte[] bytes = await File.ReadAllBytesAsync(localFilePath);
string base64 = Convert.ToBase64String(bytes);
// Save temporarily to Preferences
if (isTimeOut)
{
Preferences.Set("SignatureBase64Out", base64);
CapturedImageOut = ImageSource.FromFile(localFilePath);
}
else
{
Preferences.Set("SignatureBase64In", base64);
CapturedImageIn = ImageSource.FromFile(localFilePath);
}
_logger.LogInformation("Saved {Type} photo locally: {FilePath}",
isTimeOut ? "Time-Out" : "Time-In", localFilePath);
return fileName;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error capturing {Type} photo", isTimeOut ? "Time-Out" : "Time-In");
await Shell.Current.DisplayAlert("Error", $"Failed to capture photo: {ex.Message}", "OK");
return null;
}
}
private async Task LoadSavedImageAsync()
{
try
{
if (_todayAttendance == null)
{
CapturedImageIn = null;
CapturedImageOut = null;
return;
}
// --- Time In Photo ---
if (!string.IsNullOrEmpty(_todayAttendance.SignatureFileNameIn))
{
var pathIn = Path.Combine(FileSystem.AppDataDirectory, _todayAttendance.SignatureFileNameIn);
if (File.Exists(pathIn))
{
CapturedImageIn = ImageSource.FromFile(pathIn);
}
else
{
CapturedImageIn = null;
_logger.LogWarning("Time In image not found: {Path}", pathIn);
}
}
// --- Time Out Photo ---
if (!string.IsNullOrEmpty(_todayAttendance.SignatureFileNameOut))
{
var pathOut = Path.Combine(FileSystem.AppDataDirectory, _todayAttendance.SignatureFileNameOut);
if (File.Exists(pathOut))
{
CapturedImageOut = ImageSource.FromFile(pathOut);
}
else
{
CapturedImageOut = null;
_logger.LogWarning("Time Out image not found: {Path}", pathOut);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading saved images");
CapturedImageIn = null;
CapturedImageOut = null;
}
}
[RelayCommand]
private async Task TakePhotoAsync()
{
try
{
bool isTimeOut = !string.IsNullOrEmpty(_todayAttendance?.SignatureFileNameIn) &&
string.IsNullOrEmpty(_todayAttendance?.SignatureFileNameOut);
var fileName = await CaptureAndSavePhotoAsync(isTimeOut);
if (string.IsNullOrEmpty(fileName))
{
_logger.LogWarning("No photo captured.");
return;
}
if (_todayAttendance == null)
{
_logger.LogWarning("No attendance record found to update.");
return;
}
if (isTimeOut)
{
_todayAttendance.SignatureFileNameOut = fileName;
CapturedImageOut = ImageSource.FromFile(Path.Combine(FileSystem.AppDataDirectory, fileName));
}
else
{
_todayAttendance.SignatureFileNameIn = fileName;
CapturedImageIn = ImageSource.FromFile(Path.Combine(FileSystem.AppDataDirectory, fileName));
}
await _attendanceRepository.UpdateAsync(_todayAttendance);
_logger.LogInformation("Updated attendance with {Type} photo: {FileName}", isTimeOut ? "Time-Out" : "Time-In", fileName);
// Optional: immediately reload to show image in UI
await LoadSavedImageAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in TakePhotoAsync");
}
}
[RelayCommand]
private async Task TimeInAsync()
{
if (HasTimeIn)
{
await Shell.Current.DisplayAlert("Already Clocked In", "You have already clocked in today.", "OK");
return;
}
try
{
IsLoading = true;
// Step 1: Capture photo first
var fileName = await CaptureAndSavePhotoAsync();
if (string.IsNullOrEmpty(fileName))
{
await UtilityVM.ShowAlert("Time-in", "A photo is required to continue the Time In process.");
return;
}
else
{
// Step 2: Get location
await GetLocation();
var cred = await UserCred.GetUserCred();
// Step 3: Save attendance record
var attendance = new Attendance
{
AppAttendanceId = Guid.NewGuid(),
UserId = cred.UserId,
TimeIn = DateTime.Now,
TimeInLocation = LocationAddress,
Longitude = double.Parse(Longitude),
Latitude = double.Parse(Latitude),
SignatureFileNameIn = fileName,
ForAddressTranslation = _forAddressTranslation,
};
var result = await _attendanceRepository.SaveAsync(attendance);
if (result > 0)
{
// Reload to get the saved record with its ID
await LoadTodayAttendanceAsync();
await Shell.Current.DisplayAlert("Success",
"Time In recorded successfully!" +
(string.IsNullOrEmpty(fileName) ? "\n(No photo captured)" : ""),
"OK");
_logger.LogInformation("Time In recorded: {TimeIn} at {Location}, Photo: {Photo}",
attendance.TimeIn, attendance.TimeInLocation, fileName ?? "None");
}
else
{
throw new Exception("Failed to save attendance record");
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error recording time in");
await Shell.Current.DisplayAlert("Error", $"Failed to record Time In: {ex.Message}", "OK");
}
finally
{
IsLoading = false;
}
}
[RelayCommand]
private async Task TimeOutAsync()
{
if (hasTimeOut)
{
await Shell.Current.DisplayAlert("Already Clocked Out", "You have already clocked out today.", "OK");
return;
}
try
{
IsLoading = true;
// Step 1: Capture photo first
var fileName = await CaptureAndSavePhotoAsync(true);
if (string.IsNullOrEmpty(fileName))
{
await UtilityVM.ShowAlert("Time-out", "A photo is required to continue the Time Out process.");
return;
}
else
{
await GetLocation();
var cred = await UserCred.GetUserCred();
_todayAttendance.TimeOut = DateTime.Now;
_todayAttendance.TimeOutLocation = LocationAddress;
_todayAttendance.UserId = cred.UserId;
_todayAttendance.SignatureFileNameOut = fileName;
_todayAttendance.ForAddressTranslation = _forAddressTranslation;
var result = await _attendanceRepository.UpdateAsync(_todayAttendance);
if (result > 0)
{
HasTimeOut = true;
TimeOutText = _todayAttendance.TimeOut.Value.ToString("hh:mm tt");
// Reload to get the saved record with its ID
await LoadTodayAttendanceAsync();
await Shell.Current.DisplayAlert("Success", "Time Out recorded successfully!", "OK");
//_logger.LogInformation("Time Out recorded: {TimeOut} at {Location}",
//_todayAttendance.TimeOut, _todayAttendance.TimeOutLocation);
}
else
{
throw new Exception("Failed to update attendance record");
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error recording time out");
await Shell.Current.DisplayAlert("Error", $"Failed to record Time Out: {ex.Message}", "OK");
}
finally
{
IsLoading = false;
}
}
[RelayCommand]
private async Task SyncAsync()
{
IsLoading = true;
await GetLocation();
bool isSuccess = await _syncService.SyncPostAttendanceAsync();
if (isSuccess)
await UtilityVM.ShowAlert("Synchronization", "Synchronization successful.");
else
await UtilityVM.ShowAlert("Synchronization Failed", "Synchronization failed. Please check your signal and try again.");
IsLoading = false;
}
private async Task GetLocation()
{
try
{
var (result,isReadableAddress) = await _addressService.GetCurrentLocationAsync();
Latitude = result.Latitude.ToString("F6");
Longitude = result.Longitude.ToString("F6");
LocationAddress = result.Address;
if (isReadableAddress)
_forAddressTranslation=false;
else
_forAddressTranslation = true;
}
catch (Exception ex)
{
string message = ex.Message.Contains("permission", StringComparison.OrdinalIgnoreCase)
? "Please allow location access in your device settings."
: ex.Message;
await UtilityVM.ShowAlert("Location Error", message);
}
}
}
}