483 lines
17 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
} |