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 _attendanceRepository; private readonly ILogger _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 attendanceRepository, ILogger 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 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); } } } }