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

769 lines
31 KiB
C#

using LSFE.Infrastructure.Dto.Maintenance;
using LSFE.Infrastructure.Dto.Planning;
using LSFE.Infrastructure.Entities.Maintenance;
using LSFE.MobApp.Contracts;
using LSFE.MobApp.Entities.Account;
using LSFE.MobApp.Entities.Location;
using LSFE.MobApp.Mapper;
using LSFE.MobApp.Models;
using LSFE.MobApp.Services.Authentication;
using LSFE.MobApp.Services.Coverage;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LSFE.MobApp.Services
{
public class SyncService : ISyncService
{
#region Api Client Factory
private readonly IApiClientFactory _apiClientFactory;
private readonly IApiClient<Entities.Doctor.Doctor> _doctorApiClient;
private readonly IApiClient<ProductTransactionDto> _productTransApiClient;
private readonly IApiClient<InventoryTransaction> _inventoryTransApiClient;
private readonly IApiClient<MRRawPlansDto> _mrRawPlanApiClient;
private readonly IApiClient<LoginResponse> _userApiClient;
#endregion
#region Local Repository
private readonly IRepository<Institution> _institutionRepository;
private readonly IRepository<Entities.Account.Attendance> _attendanceRepository;
private readonly IRepository<Entities.Doctor.Doctor> _doctorRepository;
private readonly IRepository<Entities.Planning.ProductTransaction> _productTransRepository;
private readonly IRepository<Entities.Planning.InventoryTransaction> _inventoryTransRepository;
private readonly IRepository<Entities.Planning.MRRawPlan> _mrRawPlanRepository;
private readonly IRepository<Entities.Account.User> _userRepository;
#endregion
#region Helper
private readonly IFileDownloadService _fileDownloadService;
#endregion
private readonly IConfiguration _configuration;
private readonly AddressService _addressService;
public SyncService(
IApiClientFactory apiClientFactory,
IRepository<Institution> institutionRepository,
IRepository<Entities.Doctor.Doctor> doctor,
IRepository<Entities.Account.Attendance> attendanceRepository,
IRepository<Entities.Planning.ProductTransaction> productTransRepository,
IRepository<Entities.Planning.InventoryTransaction> inventoryTransRepository,
IRepository<Entities.Planning.MRRawPlan> mrRawPlanRepository,
IRepository<Entities.Account.User> userRepository,
IConfiguration configuration,
IFileDownloadService fileDownloadService,
AddressService addressService)
{
#region Api Constructor
_configuration = configuration;
_apiClientFactory = apiClientFactory;
_doctorApiClient = apiClientFactory.CreateClient<Entities.Doctor.Doctor>("DoctorMgmt/GetDoctorCoverageToday");
_productTransApiClient = apiClientFactory.CreateClient<ProductTransactionDto>("Plan/GetDoctorProductToday");
_inventoryTransApiClient = apiClientFactory.CreateClient<InventoryTransaction>("Plan/GetDoctorSampleToday");
_mrRawPlanApiClient = apiClientFactory.CreateClient<MRRawPlansDto>("Plan/GetMRRawPlans");
_userApiClient = apiClientFactory.CreateClient<LoginResponse>(_configuration["ApiSettings:Login"] ?? "AnonAccount/Login/");
#endregion
#region Local Constructor
_institutionRepository = institutionRepository;
_doctorRepository = doctor;
_productTransRepository = productTransRepository;
_inventoryTransRepository = inventoryTransRepository;
_attendanceRepository = attendanceRepository;
_mrRawPlanRepository = mrRawPlanRepository;
_userRepository = userRepository;
#endregion
#region Helper
_fileDownloadService = fileDownloadService;
_addressService = addressService;
#endregion
}
#region Sync Pull
public async Task<bool> SyncPullUsersAsync(User users)
{
try
{
var result = await _userRepository.SaveAsync(users);
if (result > 0)
{
return true;
}
return false;
}
catch (Exception ex)
{
Debug.WriteLine($"Sync error: {ex.Message}");
return false;
}
}
public async Task<bool> SyncPullMRRawPlansAsync()
{
try
{
var cred =await UserCred.GetUserCred();
if (cred.Token == null || cred.Token.Length == 0 || cred.Token == "N/A")
{
var userResponse = await _userApiClient.CreateAsync(cred);
cred.Token = userResponse.Token;
}
var serverPlans = await _mrRawPlanApiClient.GetWithQueryAsync(new Dictionary<string, string>
{
{ "UserId", cred.UserId },
{ "UserName", cred.UserName },
{ "Token", cred.Token }
});
if (serverPlans == null || !serverPlans.Any())
{
Debug.WriteLine("No data received from server");
return false;
}
// Get existing local data
var localPlans = await _mrRawPlanRepository.GetAllAsync();
int newCount = 0;
int updatedCount = 0;
foreach (var serverPlan in serverPlans)
{
try
{
// Find existing local record by InstitutionId
var existingLocal = await _mrRawPlanRepository.
FirstOrDefaultAsync(x => x.MRRawPlanId == serverPlan.MRRawPlanId);
if (existingLocal == null)
{
// Create new local record
var newPlan = ApiEntityMapper.MapMRRawPlanFromApi(serverPlan);
var result = await _mrRawPlanRepository.SaveAsync(newPlan);
if (result > 0)
{
newCount++;
}
else
{
Debug.WriteLine($"❌ Failed to save: {newPlan.MRRawPlanId}");
}
}
else
{
// Update existing record
ApiEntityMapper.UpdateMRRawPlanFromApi(existingLocal, serverPlan);
var result = await _mrRawPlanRepository.UpdateAsync(existingLocal);
if (result > 0)
{
updatedCount++;
Debug.WriteLine($"✅ Updated: {existingLocal.MRRawPlanId}");
}
else
{
Debug.WriteLine($"❌ Failed to update: {existingLocal.MRRawPlanId}");
}
}
}
catch (Exception itemEx)
{
Debug.WriteLine($"Error processing mrRawPlan {serverPlan.MRRawPlanId}: {itemEx.Message}");
}
}
Debug.WriteLine($"Sync completed: {newCount} new, {updatedCount} updated");
return true;
}
catch (Exception ex)
{
Debug.WriteLine($"Sync error: {ex.Message}");
return false;
}
}
public async Task<bool> SyncPullProductsAsync()
{
try
{
var cred = await UserCred.GetUserCred();
if (cred.Token == null || cred.Token.Length == 0 || cred.Token == "N/A")
{
var userResponse = await _userApiClient.CreateAsync(cred);
cred.Token = userResponse.Token;
}
var serverProducts = await _productTransApiClient.GetWithQueryAsync(new Dictionary<string, string>
{
{ "UserId", cred.UserId },
{ "UserName", cred.UserName },
{ "Token", cred.Token }
});
if (serverProducts == null || !serverProducts.Any())
{
Debug.WriteLine("No data received from server");
return false;
}
// Get existing local data
var localProducts = await _productTransRepository.GetAllAsync();
int newCount = 0;
int updatedCount = 0;
foreach (var serverProduct in serverProducts)
{
try
{
// Find existing local record by InstitutionId
var existingLocal = await _productTransRepository.
FirstOrDefaultAsync(x => x.ProductTransId == serverProduct.ProductTransId);
if (existingLocal == null)
{
// Create new local record
var newProduct = ApiEntityMapper.MapProductTransFromApi(serverProduct);
var result = await _productTransRepository.SaveAsync(newProduct);
if (result > 0)
{
newCount++;
Debug.WriteLine($"✅ Successfully created: {newProduct.ProductName}");
}
else
{
Debug.WriteLine($"❌ Failed to save: {newProduct.ProductName}");
}
}
else
{
// Update existing record
ApiEntityMapper.UpdateProductTransFromApi(existingLocal, serverProduct);
var result = await _productTransRepository.UpdateAsync(existingLocal);
if (result > 0)
{
updatedCount++;
Debug.WriteLine($"✅ Updated: {existingLocal.ProductName}");
}
else
{
Debug.WriteLine($"❌ Failed to update: {existingLocal.ProductName}");
}
}
}
catch (Exception itemEx)
{
Debug.WriteLine($"Error processing institution {serverProduct.ProductId}: {itemEx.Message}");
}
}
Debug.WriteLine($"Sync completed: {newCount} new, {updatedCount} updated");
return true;
}
catch (Exception ex)
{
Debug.WriteLine($"Sync error: {ex.Message}");
return false;
}
}
public async Task<bool> SyncPullSamplesAsync()
{
try
{
var cred = await UserCred.GetUserCred();
if (cred.Token == null || cred.Token.Length == 0 || cred.Token == "N/A")
{
var userResponse = await _userApiClient.CreateAsync(cred);
cred.Token = userResponse.Token;
}
var serverInventoryTrans = await _inventoryTransApiClient.GetWithQueryAsync(new Dictionary<string, string>
{
{ "UserId", cred.UserId },
{ "UserName", cred.UserName },
{ "Token", cred.Token }
});
if (serverInventoryTrans == null || !serverInventoryTrans.Any())
{
Debug.WriteLine("No data received from server");
return false;
}
// Get existing local data
var localInventoryTrans = await _inventoryTransRepository.GetAllAsync();
int newCount = 0;
int updatedCount = 0;
foreach (var serverInventoryTran in serverInventoryTrans)
{
try
{
// Find existing local record by InventoryTransId
var existingLocal = await _inventoryTransRepository.
FirstOrDefaultAsync(x => x.InventoryTransId == serverInventoryTran.InventoryTransId);
if (existingLocal == null)
{
// Create new local record
var newProduct = ApiEntityMapper.MapInventoryTransFromApi(serverInventoryTran);
var result = await _inventoryTransRepository.SaveAsync(newProduct);
if (result > 0)
{
newCount++;
}
else
{
Debug.WriteLine($"❌ Failed to save: {newProduct.Description}");
}
}
else
{
// Update existing record
ApiEntityMapper.UpdateInventoryTransFromApi(existingLocal, serverInventoryTran);
var result = await _inventoryTransRepository.UpdateAsync(existingLocal);
if (result > 0)
{
updatedCount++;
}
else
{
Debug.WriteLine($"❌ Failed to update: {existingLocal.Description}");
}
}
}
catch (Exception itemEx)
{
Debug.WriteLine($"Error processing inventory {serverInventoryTran.InventoryTransId}: {itemEx.Message}");
}
}
//Debug.WriteLine($"Sync completed: {newCount} new, {updatedCount} updated");
return true;
}
catch (Exception ex)
{
Debug.WriteLine($"Sync error: {ex.Message}");
return false;
}
}
public async Task<bool> SyncPullInstitutionsAsync()
{
try
{
// Use the exact API entity name
var apiClient = _apiClientFactory.CreateClient<Institutions>("Maintenance/institutions");
var serverInstitutions = await apiClient.GetAllAsync();
if (serverInstitutions == null || !serverInstitutions.Any())
{
Debug.WriteLine("No data received from server");
return false;
}
Debug.WriteLine($"Received {serverInstitutions.Count} institutions from API");
// Get existing local data
var localInstitutions = await _institutionRepository.GetAllAsync();
Debug.WriteLine($"Found {localInstitutions.Count} existing local institutions");
int newCount = 0;
int updatedCount = 0;
foreach (var serverInstitution in serverInstitutions)
{
try
{
// Find existing local record by InstitutionId
var existingLocal = await _institutionRepository.
FirstOrDefaultAsync(x => x.InstitutionId == serverInstitution.InstitutionId);
if (existingLocal == null)
{
// Create new local record
var newInstitution = ApiEntityMapper.MapInstutionFromApi(serverInstitution);
Debug.WriteLine($"Creating new institution: {newInstitution.InstitutionName} (ServerID: {newInstitution.InstitutionId})");
var result = await _institutionRepository.SaveAsync(newInstitution);
if (result > 0)
{
newCount++;
Debug.WriteLine($"✅ Successfully created: {newInstitution.InstitutionName}");
}
else
{
Debug.WriteLine($"❌ Failed to save: {newInstitution.InstitutionName}");
}
}
else
{
// Update existing record
ApiEntityMapper.UpdateInstutionFromApi(existingLocal, serverInstitution);
var result = await _institutionRepository.UpdateAsync(existingLocal);
if (result > 0)
{
updatedCount++;
Debug.WriteLine($"✅ Updated: {existingLocal.InstitutionName}");
}
else
{
Debug.WriteLine($"❌ Failed to update: {existingLocal.InstitutionName}");
}
}
}
catch (Exception itemEx)
{
Debug.WriteLine($"Error processing institution {serverInstitution.InstitutionId}: {itemEx.Message}");
}
}
Debug.WriteLine($"Sync completed: {newCount} new, {updatedCount} updated");
return true;
}
catch (Exception ex)
{
Debug.WriteLine($"Sync error: {ex.Message}");
return false;
}
}
public async Task<bool> SyncPullDoctorsAsync()
{
try
{
var cred= await UserCred.GetUserCred();
if (cred.Token == null || cred.Token.Length == 0 || cred.Token == "N/A")
{
var userResponse = await _userApiClient.CreateAsync(cred);
cred.Token = userResponse.Token;
await SecureStorage.SetAsync("auth_token", userResponse.Token ?? "N/A");
}
var serverDoctors = await _doctorApiClient.GetWithQueryAsync(new Dictionary<string, string>
{
{ "UserId", cred.UserId },
{ "UserName", cred.UserName },
{ "Token", cred.Token }
});
if (serverDoctors == null || !serverDoctors.Any())
{
Debug.WriteLine("No data received from server");
return false;
}
// Get existing local data and update new
await _doctorRepository.TruncateAsync();
var localDoctor = await _doctorRepository.GetAllAsync();
int newCount = 0;
int updatedCount = 0;
foreach (var serverDoc in serverDoctors)
{
try
{
// Find existing local record by InstitutionId
var existingLocal = await _doctorRepository.
FirstOrDefaultAsync(x => x.DoctorId == serverDoc.DoctorId);
if (existingLocal == null)
{
// Create new local record
var newDoctor = ApiEntityMapper.MapDoctorFromApi(serverDoc);
Debug.WriteLine($"Creating new institution: {newDoctor.FirstName} (ServerID: {newDoctor.DoctorId})");
var result = await _doctorRepository.SaveAsync(newDoctor);
if (result > 0)
{
newCount++;
Debug.WriteLine($"✅ Successfully created: {newDoctor.FirstName}");
}
else
{
Debug.WriteLine($"❌ Failed to save: {newDoctor.FirstName}");
}
}
else
{
// Update existing record
ApiEntityMapper.UpdateDoctorFromApi(existingLocal, serverDoc);
var result = await _doctorRepository.UpdateAsync(existingLocal);
if (result > 0)
{
updatedCount++;
Debug.WriteLine($"✅ Updated: {existingLocal.FirstName}");
}
else
{
Debug.WriteLine($"❌ Failed to update: {existingLocal.FirstName}");
}
}
}
catch (Exception itemEx)
{
Debug.WriteLine($"Error processing institution {serverDoc.InstitutionId}: {itemEx.Message}");
}
}
Debug.WriteLine($"Sync completed: {newCount} new, {updatedCount} updated");
return true;
}
catch (Exception ex)
{
Debug.WriteLine($"Sync error: {ex.Message}");
return false;
}
}
public async Task<bool> SyncPullCoveragePlanTodayAsync()
{
await SyncPullDoctorsAsync();
await SyncPullProductsAsync();
await SyncPullSamplesAsync();
await SyncPullMRRawPlansAsync();
await DownloadAllMaterials();
return await SyncPullInstitutionsAsync();
}
#endregion
#region Sync Post
public async Task<bool> SyncPostAttendanceAsync()
{
try
{
var today = DateTime.Today;
var localAttendance = await _attendanceRepository.GetAllAsync();
var _todayAttendance = localAttendance
.Where(a => a.TimeIn.HasValue && a.TimeIn.Value.Date == today)
.OrderByDescending(a => a.TimeIn)
.FirstOrDefault();
// Add null check here
if (_todayAttendance == null)
{
Debug.WriteLine("No attendance record found for today");
return false;
}
var cred = await UserCred.GetUserCred();
if (cred.Token == null || cred.Token.Length == 0 || cred.Token == "N/A")
{
var userResponse = await _userApiClient.CreateAsync(cred);
cred.Token = userResponse.Token;
}
var apiClient = _apiClientFactory.
CreateClient<Entities.Account.Attendance>(_configuration["ApiSettings:Endpoints:PostAttendance"]);
var serverAttendance = await apiClient.CreateAttendanceAsync(_todayAttendance);
if (!serverAttendance.Success)
{
Debug.WriteLine("No data received from server");
return false;
}
Debug.WriteLine($"Sync completed");
return true;
}
catch (Exception ex)
{
Debug.WriteLine($"Sync error: {ex.Message}");
return false;
}
}
public async Task<bool> SyncPostCoverageTodayAsync(Guid mrRawPlanId)
{
try
{
// 1. Fetch the local record
var localActualCoveragePlan = await _mrRawPlanRepository
.FirstOrDefaultAsync(a => a.MRRawPlanId == mrRawPlanId);
// Validate the record exists
if (localActualCoveragePlan == null)
{
Debug.WriteLine($"[SYNC] No coverage plan found for ID: {mrRawPlanId}");
return false;
}
// Check if already synced (avoid redundant API calls)
if (localActualCoveragePlan.IsSync)
{
Debug.WriteLine($"[SYNC] Coverage plan {mrRawPlanId} already synced. Skipping.");
return true; // Already synced is a success
}
// 4. Validate required data before sending to API
if (!ValidateCoveragePlan(localActualCoveragePlan))
{
Debug.WriteLine($"[SYNC] Coverage plan {mrRawPlanId} validation failed");
return false;
}
if (localActualCoveragePlan.ForAddressTranslation)
{
var (address, isHumanReadable) = await _addressService.
GetAddressAsync(double.Parse(localActualCoveragePlan.Latitude),
double.Parse(localActualCoveragePlan.Longitude));
localActualCoveragePlan.Location = address;
}
// 5. Create API client
var apiClient = _apiClientFactory
.CreateClient<Entities.Planning.MRRawPlan>(
_configuration["ApiSettings:Endpoints:PostActualCoveragePlan"]);
Debug.WriteLine($"[SYNC] Sending coverage plan {mrRawPlanId} to server...");
// Send to server
var serverResponse = await apiClient.CreateCoveragePlanAsync(localActualCoveragePlan);
// Validate server response
if (serverResponse == null)
{
Debug.WriteLine($"[SYNC] Null response from server for {mrRawPlanId}");
return false;
}
if (!serverResponse.Success)
{
Debug.WriteLine($"[SYNC] Server returned failure for {mrRawPlanId}. " +
$"Message: {serverResponse.Message ?? "No message"}");
return false;
}
localActualCoveragePlan.IsSync = true;
localActualCoveragePlan.LastSyncDate = DateTime.Now;
int updateResult = await _mrRawPlanRepository.UpdateAsync(localActualCoveragePlan);
if (updateResult <= 0)
{
Debug.WriteLine($"[SYNC] Failed to update local IsSync flag for {mrRawPlanId}");
// Note: Data is on server but local flag update failed
// Consider how to handle this edge case
return false;
}
Debug.WriteLine($"[SYNC] ✓ Coverage plan {mrRawPlanId} synced successfully");
return true;
}
catch (HttpRequestException httpEx)
{
// Network-specific errors
Debug.WriteLine($"[SYNC] Network error for {mrRawPlanId}: {httpEx.Message}");
return false;
}
catch (TimeoutException timeoutEx)
{
// Timeout errors
Debug.WriteLine($"[SYNC] Timeout error for {mrRawPlanId}: {timeoutEx.Message}");
return false;
}
catch (Exception ex)
{
// General errors
Debug.WriteLine($"[SYNC] Unexpected error for {mrRawPlanId}: {ex.Message}");
Debug.WriteLine($"[SYNC] Stack trace: {ex.StackTrace}");
return false;
}
}
private bool ValidateCoveragePlan(Entities.Planning.MRRawPlan plan)
{
if (plan == null)
return false;
// Check required fields
if (plan.MRRawPlanId == Guid.Empty)
{
Debug.WriteLine("[SYNC] Invalid MRRawPlanId");
return false;
}
return true;
}
#endregion
#region Download Materials & Samplings
private async Task DownloadAllMaterials()
{
try
{
var allTransactions = await _productTransRepository.GetAllAsync();
foreach (var product in allTransactions)
{
var endpoint = _configuration["ApiSettings:Endpoints:GetProductFiles"];
var fileUrl = $"{endpoint}{Uri.EscapeDataString(product.ProductLink)}";
var material = new ProductMaterial
{
ProductTransId = product.ProductTransId,
ProductId = product.ProductId,
ProductName = product.ProductName,
ProductLink = product.ProductLink,
FileUrl = fileUrl,
FileExtension = Path.GetExtension(product.ProductLink)
};
if (!await _fileDownloadService.IsFileDownloadedAsync(product.ProductLink))
{
await DownloadMaterials(material);
}
}
// await Application.Current.MainPage.DisplayAlert("Success", "All materials downloaded!", "OK");
}
catch (Exception ex)
{
await Application.Current.MainPage.DisplayAlert("Error", $"Failed to download all: {ex.Message}", "OK");
}
}
private async Task DownloadMaterials(ProductMaterial material)
{
if (material == null || material.IsDownloading) return;
try
{
material.IsDownloading = true;
await _fileDownloadService.DownloadFileAsync(material.FileUrl, material.ProductLink, CancellationToken.None);
material.IsDownloaded = true;
material.IsDownloading = false;
}
catch (Exception ex)
{
material.IsDownloading = false;
await Application.Current.MainPage.DisplayAlert("Error", $"Failed to download: {ex.Message}", "OK");
}
}
#endregion
}
}