commit 405c7b3a6d79207e7b788165623aed66ff73b299 Author: rowell_m_soriano Date: Thu Jul 30 10:31:04 2026 +0800 migration to new Repo diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9491a2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,363 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd \ No newline at end of file diff --git a/LSFE.Domain/Contracts/Account/INavigationService.cs b/LSFE.Domain/Contracts/Account/INavigationService.cs new file mode 100644 index 0000000..c1b8cad --- /dev/null +++ b/LSFE.Domain/Contracts/Account/INavigationService.cs @@ -0,0 +1,11 @@ +using LSFE.Infrastructure.Dto.Account; + +namespace LSFE.Domain.Contracts +{ + public interface INavigationService + { + Task GetUserNavigationAsync(string userId); + Task> GetNavigationByRoleIdsAsync(List roleIds); + Task> GetNavigationByRoleAsync(string roleId); + } +} diff --git a/LSFE.Domain/Contracts/Doctor/IDoctorRepo.cs b/LSFE.Domain/Contracts/Doctor/IDoctorRepo.cs new file mode 100644 index 0000000..ecbad50 --- /dev/null +++ b/LSFE.Domain/Contracts/Doctor/IDoctorRepo.cs @@ -0,0 +1,24 @@ +using LSFE.Infrastructure.Dto.Doctor; +using LSFE.Infrastructure.Entities.Doctor; +using LSFE.Infrastructure.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + + +namespace LSFE.Domain.Contracts.Doctor +{ + public interface IDoctorRepo + { + Task SoftDeleteAsync(int doctorId); + Task> GetDoctor(DoctorDto dto); + Task> GetMyDoctor(DoctorDto dto); + Task> GetAllDoctor(DoctorDto dto); + Task> GetDoctorCoverageToday(DoctorDto dto); + Task PostPutDoctor(DoctorDto dto); + Task ApproveDoctor(int doctorId, byte newStatus,string userName); + Task> GetPendingApprovals(DoctorDto doctor); + } +} \ No newline at end of file diff --git a/LSFE.Domain/Contracts/IAccountService.cs b/LSFE.Domain/Contracts/IAccountService.cs new file mode 100644 index 0000000..5a11f73 --- /dev/null +++ b/LSFE.Domain/Contracts/IAccountService.cs @@ -0,0 +1,28 @@ +using LSFE.Infrastructure.Dto; +using LSFE.Infrastructure.Entities; +using LSFE.Infrastructure.Model; +using Microsoft.AspNetCore.Identity; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Domain.Contracts +{ + public interface IAccountService + { + Task> GetUserRole(); + Task> GetUsers(string company); + Task> GetDepartment(); + Task GetUserById(string userId); + Task CreateToken(AppsUsers user); + Task PostPutDepartmentAsync(Departments dto); + Task RegisterUserAsync(RegisterUserDto dto); + Task UpdateUserAsync(UpdateUserDto dto); + Task AddUserToRoleAsync(string userId, string roleName); + Task AddClaimToUserAsync(string userId, Claim claim); + } + +} diff --git a/LSFE.Domain/Contracts/IGenericRepository.cs b/LSFE.Domain/Contracts/IGenericRepository.cs new file mode 100644 index 0000000..fdcf769 --- /dev/null +++ b/LSFE.Domain/Contracts/IGenericRepository.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Domain.Contracts +{ + public interface IGenericRepository where T : class + { + Task GetByIdAsync(int id); + Task> GetAllAsync(); + Task AddAsync(T entity); + void Update(T entity); + void Delete(T entity); + } +} diff --git a/LSFE.Domain/Contracts/IUnitOfWork.cs b/LSFE.Domain/Contracts/IUnitOfWork.cs new file mode 100644 index 0000000..b9645b0 --- /dev/null +++ b/LSFE.Domain/Contracts/IUnitOfWork.cs @@ -0,0 +1,46 @@ +using LSFE.Domain.Contracts.Doctor; +using LSFE.Domain.Contracts.Maintenance; +using LSFE.Domain.Contracts.Planning; +using LSFE.Domain.Contracts.Reports; +using LSFE.Domain.Contracts.TimeKeeping; +using LSFE.Domain.Services; +using LSFE.Domain.Services.TimeKeeping; +using LSFE.Infrastructure.Entities.Admin; +using LSFE.Infrastructure.Entities.Doctor; +using LSFE.Infrastructure.Entities.Maintenance; +using LSFE.Infrastructure.Entities.Planning; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Domain.Contracts +{ + public interface IUnitOfWork : IDisposable + { + IErrorLogger ErrorLogger { get; } + IDoctorRepo Doctors { get; } + IAttendanceRepo Attendances { get; } + IPlanningRepo Planning { get; } + IReportsRepo Reports { get; } + IMaintenanceRepo Areas { get; } + IMaintenanceRepo Districts { get; } + IMaintenanceRepo Divisions { get; } + IMaintenanceRepo Territories { get; } + IMaintenanceRepo InstitutionCategories { get; } + IMaintenanceRepo Institutions { get; } + IMaintenanceRepo Products { get; } + IMaintenanceRepo Specializations { get; } + IMaintenanceRepo Inventory { get; } + IMaintenanceRepo InventoryTransactions { get; } + IMaintenanceRepo ProductTransactions { get; } + IMaintenanceRepo MRRawPlans { get; } + IMaintenanceRepo CatchUpCalls { get; } + IMaintenanceRepo MRPlanDetails { get; } + IMaintenanceRepo MRPlanHeaders { get; } + IMaintenanceRepo UserDevices { get; } + + Task SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/LSFE.Domain/Contracts/Maintenance/IMaintenanceRepo.cs b/LSFE.Domain/Contracts/Maintenance/IMaintenanceRepo.cs new file mode 100644 index 0000000..932f40d --- /dev/null +++ b/LSFE.Domain/Contracts/Maintenance/IMaintenanceRepo.cs @@ -0,0 +1,25 @@ +using LSFE.Infrastructure.Model; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Domain.Contracts.Maintenance +{ + public interface IMaintenanceRepo where TEntity : class + { + Task> GetAllAsync(params Expression>[] includeProperties); + Task GetByIdAsync(object id); + Task FindAsync(Expression> predicate); + Task AnyAsync(Expression> predicate); + IQueryable Query(); + Task> FindAllAsync(Expression> predicate); + Task AddRangeAsync(IEnumerable entities); + Task AddOrUpdateAsync(TEntity entity); + Task SoftDeleteAsync(int id); + Task DeleteAsync(Guid id); + } +} diff --git a/LSFE.Domain/Contracts/Planning/IPlanningRepo.cs b/LSFE.Domain/Contracts/Planning/IPlanningRepo.cs new file mode 100644 index 0000000..146193e --- /dev/null +++ b/LSFE.Domain/Contracts/Planning/IPlanningRepo.cs @@ -0,0 +1,33 @@ +using LSFE.Infrastructure.Dto; +using LSFE.Infrastructure.Dto.Doctor; +using LSFE.Infrastructure.Dto.Planning; +using LSFE.Infrastructure.Entities.Maintenance; +using LSFE.Infrastructure.Entities.Planning; +using LSFE.Infrastructure.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Domain.Contracts.Planning +{ + public interface IPlanningRepo + { + Task SoftDeleteAsync(int doctorId); + Task> GetPlan(PlanningDto dto); + Task> GetDoctorById(DoctorDto dto); + Task> GetDoctorWithPlan(DoctorDto dto); + Task> GetPendingApprovals(PlanningDto dto); + Task> GetAllMissReschedule(CredentialDto dto); + Task> GetAllAppointmentToday(CredentialDto dto); + Task> GetMRRawPlans(PlanByUserIdRequest dto); + + Task> GetSampleTransaction(Guid mrRawPlanId); + Task> GetProductTransaction(Guid mrRawPlanId); + + Task PostPutPlan(PlanningDto dto); + Task CopyMonthlyMRRawPlans(PlanningDto dto); + Task ApprovePlan(ApprovePlanDto dto); + } +} diff --git a/LSFE.Domain/Contracts/Reports/IReportsRepo.cs b/LSFE.Domain/Contracts/Reports/IReportsRepo.cs new file mode 100644 index 0000000..c7605e8 --- /dev/null +++ b/LSFE.Domain/Contracts/Reports/IReportsRepo.cs @@ -0,0 +1,19 @@ +using LSFE.Infrastructure.Dto.Reports; +using LSFE.Infrastructure.Entities.Reports; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Domain.Contracts.Reports +{ + public interface IReportsRepo + { + Task> GetCallRateReport(ReportsDto dto); + Task> GetCallReachReport(ReportsDto dto); + Task> GetCatchUpCallReport(ReportsDto dto); + Task> GetActualCoverageReport(ReportsDto dto); + Task PutMissedCall(ReportsDto dto); + } +} diff --git a/LSFE.Domain/Contracts/TimeKeeping/IAttendanceRepo.cs b/LSFE.Domain/Contracts/TimeKeeping/IAttendanceRepo.cs new file mode 100644 index 0000000..79403e2 --- /dev/null +++ b/LSFE.Domain/Contracts/TimeKeeping/IAttendanceRepo.cs @@ -0,0 +1,21 @@ +using DocumentFormat.OpenXml.Office2010.ExcelAc; +using LSFE.Infrastructure.Dto; +using LSFE.Infrastructure.Dto.TimeKeeping; +using LSFE.Infrastructure.Entities.TimeKeeping; +using LSFE.Infrastructure.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Domain.Contracts.TimeKeeping +{ + public interface IAttendanceRepo + { + Task> + GetAttendance(Infrastructure.Dto.TimeKeeping.AttendanceDto dto); + Task> GetAttendanceToday(CredentialDto dto); + Task PostPutAttendance(Infrastructure.Dto.TimeKeeping.AttendanceDto dto); + } +} diff --git a/LSFE.Domain/Helpers/ContentTypeHelper.cs b/LSFE.Domain/Helpers/ContentTypeHelper.cs new file mode 100644 index 0000000..f62d23f --- /dev/null +++ b/LSFE.Domain/Helpers/ContentTypeHelper.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Domain.Helpers +{ + public static class ContentTypeHelper + { + public static string GetContentType(string fileName) + { + var extension = Path.GetExtension(fileName).ToLowerInvariant(); + return extension switch + { + ".pdf" => "application/pdf", + ".doc" => "application/msword", + ".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ".pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation", + ".xls" => "application/vnd.ms-excel", + ".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ".png" => "image/png", + ".jpg" or ".jpeg" => "image/jpeg", + ".gif" => "image/gif", + ".txt" => "text/plain", + _ => "application/octet-stream" + }; + } + public static string ValidateFile(string filePath, string uploadsPath) + { + var message = ""; + // Security: Prevent directory traversal attacks + var fullPath = Path.GetFullPath(filePath); + + if (!fullPath.StartsWith(Path.GetFullPath(uploadsPath))) + { + message="Invalid file path"; + } + + if (!System.IO.File.Exists(filePath)) + { + message="File not found"; + } + return message; + } + } +} diff --git a/LSFE.Domain/Helpers/ExcelTemplateValidator.cs b/LSFE.Domain/Helpers/ExcelTemplateValidator.cs new file mode 100644 index 0000000..ed802f4 --- /dev/null +++ b/LSFE.Domain/Helpers/ExcelTemplateValidator.cs @@ -0,0 +1,40 @@ +using ClosedXML.Excel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Domain.Helpers +{ + public class ExcelTemplateValidator + { + private static readonly string[] REQUIRED_HEADERS = new[] + { + "BRAND", "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December", + "Year", "Medrep_Name" + }; + + public static (bool isValid, List errors) ValidateHeaders(IXLWorksheet worksheet) + { + var errors = new List(); + var isValid = true; + + for (int col = 1; col <= REQUIRED_HEADERS.Length; col++) + { + var headerValue = worksheet.Cell(1, col).Value.ToString()?.Trim(); + var expectedHeader = REQUIRED_HEADERS[col - 1]; + + if (string.IsNullOrEmpty(headerValue) || + !headerValue.Equals(expectedHeader, StringComparison.OrdinalIgnoreCase)) + { + errors.Add($"Column {col}: Expected '{expectedHeader}' but found '{headerValue}'"); + isValid = false; + } + } + + return (isValid, errors); + } + } +} diff --git a/LSFE.Domain/LSFE.Domain.csproj b/LSFE.Domain/LSFE.Domain.csproj new file mode 100644 index 0000000..71db202 --- /dev/null +++ b/LSFE.Domain/LSFE.Domain.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/LSFE.Domain/Services/Account/NavigationService.cs b/LSFE.Domain/Services/Account/NavigationService.cs new file mode 100644 index 0000000..51d7825 --- /dev/null +++ b/LSFE.Domain/Services/Account/NavigationService.cs @@ -0,0 +1,170 @@ +using LSFE.Domain.Contracts; +using LSFE.Infrastructure.Database; +using LSFE.Infrastructure.Dto.Account; +using LSFE.Infrastructure.Entities; +using Microsoft.AspNetCore.Identity; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using System.Data; + +namespace LSFE.Domain.Services.Account +{ + public class NavigationService : INavigationService + { + private readonly LSFEDbContext _context; + private readonly UserManager _userManager; + + public NavigationService(LSFEDbContext context, UserManager userManager) + { + _context = context; + _userManager = userManager; + } + + public async Task GetUserNavigationAsync(string userId) + { + try + { + var user = await _userManager.FindByIdAsync(userId); + + if (user == null) + { + return new NavigationResponseDto + { + Success = false, + Message = "User not found or inactive", + Data = new List() + }; + } + + // Get user roles using UserManager + var roles = await _userManager.GetRolesAsync(user); + + if (!roles.Any()) + { + return new NavigationResponseDto + { + Success = false, + Message = "User has no assigned roles", + Data = new List() + }; + } + + // Get role IDs from role names + var roleIds = await _context.Roles + .Where(r => roles.Contains(r.Name)) + .Select(r => r.Id) + .ToListAsync(); + + // Get navigation for all user roles + var navigationItems = await GetNavigationByRoleIdsAsync(roleIds); + + return new NavigationResponseDto + { + Success = true, + Data = navigationItems, + Message = "Navigation retrieved successfully" + }; + } + catch (Exception ex) + { + return new NavigationResponseDto + { + Success = false, + Message = $"Error retrieving navigation: {ex.Message}", + Data = new List() + }; + } + } + + public async Task> GetNavigationByRoleIdsAsync(List roleIds) + { + try + { + var roleIdsParam = string.Join(",", roleIds); + + using (var command = _context.Database.GetDbConnection().CreateCommand()) + { + command.CommandText = "GetUserNavigation"; + command.CommandType = CommandType.StoredProcedure; + command.Parameters.Add(new SqlParameter("@RoleIds", roleIdsParam)); + + await _context.Database.OpenConnectionAsync(); + + using (var result = await command.ExecuteReaderAsync()) + { + var rootModules = new List(); + var childModules = new List(); + + // Read root modules + while (await result.ReadAsync()) + { + rootModules.Add(new NavigationItemDto + { + Id = result["Id"].ToString(), + Label = result["Label"].ToString(), + Icon = result["Icon"].ToString(), + Route = result["Route"].ToString(), + Badge = result["Badge"]?.ToString(), + Permissions = new PermissionsDto + { + CanView = Convert.ToBoolean(result["CanView"]), + CanCreate = Convert.ToBoolean(result["CanCreate"]), + CanEdit = Convert.ToBoolean(result["CanEdit"]), + CanDelete = Convert.ToBoolean(result["CanDelete"]) + }, + Children = new List() + }); + } + + // Read child modules + if (await result.NextResultAsync()) + { + while (await result.ReadAsync()) + { + childModules.Add(new NavigationItemDto + { + Id = result["Id"].ToString(), + Label = result["Label"].ToString(), + Icon = result["Icon"].ToString(), + Route = result["Route"].ToString(), + ParentModuleId = result["ParentModuleId"] != DBNull.Value + ? Convert.ToInt32(result["ParentModuleId"]) + : (int?)null, + Permissions = new PermissionsDto + { + CanView = Convert.ToBoolean(result["CanView"]), + CanCreate = Convert.ToBoolean(result["CanCreate"]), + CanEdit = Convert.ToBoolean(result["CanEdit"]), + CanDelete = Convert.ToBoolean(result["CanDelete"]) + } + }); + } + } + + // Attach children to parents + foreach (var parent in rootModules) + { + var children = childModules + .Where(c => c.ParentModuleId.ToString() == parent.Id) + .ToList(); + + parent.Children = children.Any() ? children : null; + } + + return rootModules; + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Error in GetNavigationByRoleIdsAsync: {ex.Message}"); + throw; + } + } + // Optional: Add method to get single role navigation (for backwards compatibility) + public async Task> GetNavigationByRoleAsync(string roleId) + { + return await GetNavigationByRoleIdsAsync(new List { roleId }); + } + } +} \ No newline at end of file diff --git a/LSFE.Domain/Services/AccountService.cs b/LSFE.Domain/Services/AccountService.cs new file mode 100644 index 0000000..48eed49 --- /dev/null +++ b/LSFE.Domain/Services/AccountService.cs @@ -0,0 +1,459 @@ +using LSFE.Domain.Contracts; +using LSFE.Infrastructure.Database; +using LSFE.Infrastructure.Dto; +using LSFE.Infrastructure.Entities; +using LSFE.Infrastructure.Entities.Auth; +using LSFE.Infrastructure.Model; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Domain.Services +{ + public class AccountService : IAccountService + { + private readonly UserManager _userManager; + private readonly IConfiguration _configuration; + private readonly RoleManager _roleManager; + private readonly LSFEDbContext _context; + public AccountService(UserManager userManager, IConfiguration configuration, + RoleManager roleManager , LSFEDbContext context ) + { + _userManager = userManager; + _configuration = configuration; + _roleManager = roleManager; + _context = context; + } + public async Task AddUserToRoleAsync(string userId, string roleName) + { + var user = await _userManager.FindByIdAsync(userId); + if (user == null) + return new Response + { + Success = false, + Message = "User not found", + MessCode = 0 + }; + + // Ensure role exists + if (!await _roleManager.RoleExistsAsync(roleName)) + { + var roleResult = await _roleManager.CreateAsync(new Role()); + if (!roleResult.Succeeded) + return new Response + { + Success = false, + Message = roleResult.ToString(), + MessCode = 0 + }; + } + + await _userManager.AddToRoleAsync(user, roleName); + return new Response + { + Success = true, + Message = "Add user role successfully!", + MessCode = 1 + }; + } + + public async Task AddClaimToUserAsync(string userId, Claim claim) + { + var user = await _userManager.FindByIdAsync(userId); + if (user == null) + return new Response + { + Success = false, + Message = "User not found", + MessCode = 0 + }; + + await _userManager.AddClaimAsync(user, claim); + return new Response + { + Success = true, + Message = "Success", + MessCode = 1 + }; + } + public async Task RegisterUserAsync(RegisterUserDto dto) + { + try + { + // 🔍 1. Validate required fields + if (string.IsNullOrWhiteSpace(dto.UserName) || + string.IsNullOrWhiteSpace(dto.Email) || + string.IsNullOrWhiteSpace(dto.Password)) + { + return new Response + { + Success = false, + Message = "Username, Email, and Password are required.", + MessCode = 0 + }; + } + + // 🧩 2. Check if username or email already exists + var existingUserByName = await _userManager.FindByNameAsync(dto.UserName); + if (existingUserByName != null) + { + return new Response + { + Success = false, + Message = $"Username '{dto.UserName}' is already taken.", + MessCode = 0 + }; + } + + var existingUserByEmail = await _userManager.FindByEmailAsync(dto.Email); + if (existingUserByEmail != null) + { + return new Response + { + Success = false, + Message = $"Email '{dto.Email}' is already registered.", + MessCode = 0 + }; + } + + // 🏢 3. Optional: Validate Department + if (dto.DepartmentId.HasValue) + { + var departmentExists = await _context.Departments + .AnyAsync(d => d.DepartmentId == dto.DepartmentId && d.IsActive); + + if (!departmentExists) + { + return new Response + { + Success = false, + Message = "Invalid Department selected.", + MessCode = 0 + }; + } + } + + // 🧍‍♂️ 4. Create new user + var user = new AppsUsers + { + UserName = dto.UserName, + Email = dto.Email, + FullName = dto.FullName, + EmployeeId = dto.EmployeeId, + Company = dto.Company, + DepartmentId = dto.DepartmentId, + Address = dto.Address, + PhoneNumber = dto.PhoneNumber, + CreatedDate = DateTime.UtcNow, + UpdatedDate = DateTime.UtcNow, + CreatedBy = dto.UpdatedBy, + UpdatedBy = dto.UpdatedBy, + EmailConfirmed = true // optional: mark as verified by admin + }; + + var createResult = await _userManager.CreateAsync(user, dto.Password); + if (!createResult.Succeeded) + { + var errors = string.Join("; ", createResult.Errors.Select(e => e.Description)); + return new Response + { + Success = false, + Message = $"Failed to create user: {errors}", + MessCode = 0 + }; + } + + // 🛡️ 5. Assign Role if provided + if (!string.IsNullOrWhiteSpace(dto.RoleId)) + { + var role = await _roleManager.FindByIdAsync(dto.RoleId); + if (role == null) + { + return new Response + { + Success = false, + Message = $"Role with ID '{dto.RoleId}' does not exist.", + MessCode = 0 + }; + } + + await _userManager.AddToRoleAsync(user, role.Name); + + // optional: add user claim for role + await _userManager.AddClaimAsync(user, new Claim("RoleName", role.Name)); + } + + return new Response + { + Success = true, + Message = "User registered successfully!", + MessCode = 1 + }; + } + catch (Exception ex) + { + return new Response + { + Success = false, + Message = $"Unexpected error: {ex.Message}", + MessCode = 0 + }; + } + } + public async Task CreateToken(AppsUsers user) + { + var authClaims = await BuildClaims(user); + + var authSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(_configuration["JWT:Secret"])); + + var token = new JwtSecurityToken( + issuer: _configuration["JWT:ValidIssuer"], + audience: _configuration["JWT:ValidAudience"], + expires: DateTime.UtcNow.AddMinutes(30), + claims: authClaims, + signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256) + ); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + private async Task> BuildClaims(AppsUsers user) + { + var roles = await _userManager.GetRolesAsync(user); + + var claims = new List + { + new Claim(ClaimTypes.Name, user.UserName), + new Claim(ClaimTypes.NameIdentifier, user.Id), + new Claim("FullName", user.FullName ?? ""), + new Claim("Company", user.Company ?? ""), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + }; + + claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r))); + return claims; + } + + public async Task GetUserById(string userId) + { + return await _userManager.Users.FirstOrDefaultAsync(u => u.Id == userId); + } + + public async Task UpdateUserAsync(UpdateUserDto dto) + { + using var transaction = await _context.Database.BeginTransactionAsync(); + try + { + var user = await _userManager.FindByIdAsync(dto.Id.ToString()); + if (user == null) + return new Response { Success = false, Message = "User not found.", MessCode = 0 }; + + // --- Update basic info --- + user.Email = dto.Email; + user.FullName = dto.FullName; + user.EmployeeId = dto.EmployeeId; + user.Company = dto.Company; + user.DepartmentId = dto.DepartmentId; + user.Address = dto.Address; + user.PhoneNumber = dto.PhoneNumber; + user.UpdatedBy = dto.UpdatedBy; + user.UpdatedDate = DateTime.UtcNow; + + // --- Update password (if provided) --- + if (!string.IsNullOrWhiteSpace(dto.Password)) + { + var passwordHasher = new PasswordHasher(); + user.PasswordHash = passwordHasher.HashPassword(user, dto.Password); + } + + var updateResult = await _userManager.UpdateAsync(user); + if (!updateResult.Succeeded) + return new Response + { + Success = false, + Message = string.Join(", ", updateResult.Errors.Select(e => e.Description)), + MessCode = 0 + }; + + // --- Update roles --- + if (!string.IsNullOrEmpty(dto.RoleName)) + { + var currentRoles = await _userManager.GetRolesAsync(user); + if (currentRoles.Any()) + await _userManager.RemoveFromRolesAsync(user, currentRoles); + + if (await _roleManager.RoleExistsAsync(dto.RoleName)) + await _userManager.AddToRoleAsync(user, dto.RoleName); + else + return new Response + { + Success = false, + Message = $"Role '{dto.RoleName}' not found.", + MessCode = 0 + }; + } + + // --- Update claims --- + if (!string.IsNullOrEmpty(dto.RoleName)) + { + var existingClaims = await _userManager.GetClaimsAsync(user); + var existingRoleClaim = existingClaims.FirstOrDefault(c => c.Type == "Role"); + if (existingRoleClaim != null) + await _userManager.RemoveClaimAsync(user, existingRoleClaim); + + await _userManager.AddClaimAsync(user, new Claim("Role", dto.RoleName)); + } + + await transaction.CommitAsync(); + return new Response + { + Success = true, + Message = "User updated successfully!", + MessCode = 1 + }; + } + catch (Exception ex) + { + await transaction.RollbackAsync(); + return new Response + { + Success = false, + Message = $"Updating user failed: {ex.Message}", + MessCode = 0 + }; + } + } + + public async Task> GetUsers(string company) + { + try + { + var users = await _userManager.Users + .Where(u=>u.Company== company) + .Include(u => u.Department) + .ToListAsync(); + + var roles = await _roleManager.Roles.ToListAsync(); // cache all roles once + + var result = new List(); + + foreach (var user in users) + { + var roleNames = await _userManager.GetRolesAsync(user); + + // Map role names to RoleId + RoleName + var userRoles = roles + .Where(r => roleNames.Contains(r.Name)) + .Select(r => new RoleDto + { + RoleId = r.Id, + RoleName = r.Name! + }) + .ToList(); + + result.Add(new UserWithRoleDto + { + Id = user.Id, + EmployeeId = user.EmployeeId, + Company = user.Company, + Address = user.Address, + UpdatedDate = user.UpdatedDate, + UpdatedBy = user.UpdatedBy, + Email = user.Email, + FullName = user.FullName, + RoleName = userRoles[0].RoleName, + CreatedBy = user.CreatedBy, + CreatedDate = user.CreatedDate, + UserName = user.UserName, + DepartmentId = user.DepartmentId, + RoleId = userRoles[0].RoleId, + Department = user.Department, + AccessFailedCount = user.AccessFailedCount, + PhoneNumber = user.PhoneNumber, + LockoutEnd = user.LockoutEnd, + LockoutEnabled = user.LockoutEnabled, + UserRoles = userRoles + }); + } + + return result; + } + catch (Exception ex) + { + ex.ToString(); + throw; + } + + } + + public async Task PostPutDepartmentAsync(Departments dto) + { + var existing = await _context.Departments + .FirstOrDefaultAsync(d => d.DepartmentId == dto.DepartmentId); + + if (existing == null) + { + var duplicate = await _context.Departments + .AnyAsync(d => d.DepartmentName == dto.DepartmentName + || d.DepartmentCode == dto.DepartmentCode); + + if (duplicate) + { + return new Response + { + Success = false, + Message = "Department name or code already exists.", + MessCode = 0 + }; + } + + _context.Departments.Add(dto); + await _context.SaveChangesAsync(); + return new Response + { + Success = true, + Message = "Department created successfully.", + MessCode = 1 + }; + } + else + { + existing.DepartmentName = dto.DepartmentName; + existing.DepartmentCode = dto.DepartmentCode; + existing.IsActive = dto.IsActive; + + _context.Departments.Update(existing); + await _context.SaveChangesAsync(); + return new Response + { + Success = true, + Message = "Department updated successfully.", + MessCode = 1 + }; + } + } + public async Task> GetDepartment() + { + return await _context.Departments.ToListAsync(); + } + public async Task> GetUserRole() + { + return await _roleManager.Roles + .Select(r => new RoleDto + { + RoleId = r.Id, + RoleName = r.Name + }) + .ToListAsync(); + } + } +} diff --git a/LSFE.Domain/Services/Doctor/DoctorRepo.cs b/LSFE.Domain/Services/Doctor/DoctorRepo.cs new file mode 100644 index 0000000..ecf9250 --- /dev/null +++ b/LSFE.Domain/Services/Doctor/DoctorRepo.cs @@ -0,0 +1,489 @@ +using LSFE.Domain.Contracts.Doctor; +using LSFE.Infrastructure.Database; +using LSFE.Infrastructure.Dto.Doctor; +using LSFE.Infrastructure.Entities.Doctor; +using LSFE.Infrastructure.Model; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + + +namespace LSFE.Domain.Services.Doctor +{ + public class DoctorRepo : GenericRepository, IDoctorRepo + { + private readonly LSFEDbContext _context; + public DoctorRepo(LSFEDbContext context) : base(context) + { + _context = context; + } + + public async Task SoftDeleteAsync(int doctorId) + { + var response = new Response(); + + try + { + var existingDoctor = await GetByIdAsync(doctorId); + + if (existingDoctor == null) + { + response.Success = false; + response.Message = $"Doctor with ID {doctorId} not found."; + response.MessCode = 0; + return response; + } + + existingDoctor.IsActive = false; + + Update(existingDoctor); + await _context.SaveChangesAsync(); + + response.Success = true; + response.Data = existingDoctor; + response.Message = "Doctor deactivated successfully."; + response.MessCode = 3; // code for delete/soft delete + } + catch (Exception ex) + { + response.Success = false; + response.Message = ex.Message; + response.MessCode = 0; + } + + return response; + } + public async Task> GetAllDoctor(DoctorDto dto) + { + var allItems = await _context.Doctors + .FromSqlRaw("EXEC GetAllDoctor @UserId", + new SqlParameter("@UserId", dto.UserId)) + .ToListAsync(); + + return allItems ?? new List(); + } + public async Task> GetDoctor(DoctorDto dto) + { + var query = + from d in _context.Doctors + join i in _context.Institutions + on d.InstitutionId equals i.InstitutionId into di + from i in di.DefaultIfEmpty() + join s in _context.Specializations + on d.SpecializationId equals s.SpecializationId into si + from s in si.DefaultIfEmpty() + join dis in _context.Districts + on d.DistrictId equals dis.DistrictId into dist + from dis in dist.DefaultIfEmpty() + join fda in _context.ForDoctorApprovals + on d.DoctorId equals fda.DoctorId + join u in _context.Users + on d.UserId equals u.Id + where d.UserId == dto.UserId + && (dto.DoctorId == 0 || d.DoctorId == dto.DoctorId) + select new DoctorDto + { + DoctorId = d.DoctorId, + FirstName = d.FirstName, + MiddleInitial = d.MiddleInitial, + LastName = d.LastName, + EmailAddress = d.EmailAddress, + PhoneNo = d.PhoneNo, + SpecializationId = d.SpecializationId, + LicenseNo = d.LicenseNo, + Address = d.Address, + BirthDate = d.BirthDate, + MaxVisit = d.MaxVisit, + MedRepName=u.UserName, + Status = fda.Status, + IsTerritorial= d.IsTerritorial, + InstitutionId = fda.InstitutionId, + InstitutionName = i != null ? i.InstitutionName : null, + SpecializationName = s != null ? s.SpecializationName : null, + DistrictId = fda != null ? dis.DistrictId : 0, + DistrictName = dis != null ? dis.DistrictName : null + }; + + return await query.ToListAsync(); + } + + public async Task> GetMyDoctor(DoctorDto dto) + { + var query = + from d in _context.Doctors + join i in _context.Institutions + on d.InstitutionId equals i.InstitutionId into di + from i in di.DefaultIfEmpty() + join s in _context.Specializations + on d.SpecializationId equals s.SpecializationId into si + from s in si.DefaultIfEmpty() + join dis in _context.Districts + on d.DistrictId equals dis.DistrictId into dist + from dis in dist.DefaultIfEmpty() + join u in _context.Users + on d.UserId equals u.Id + where d.UserId == dto.UserId + select new DoctorDto + { + DoctorId = d.DoctorId, + FirstName = d.FirstName, + MiddleInitial = d.MiddleInitial, + LastName = d.LastName, + EmailAddress = d.EmailAddress, + PhoneNo = d.PhoneNo, + SpecializationId = d.SpecializationId, + LicenseNo = d.LicenseNo, + Address = d.Address, + BirthDate = d.BirthDate, + MaxVisit = d.MaxVisit, + Status = d.Status, + MedRepName = u.UserName, + IsTerritorial = d.IsTerritorial, + InstitutionId = d.InstitutionId, + InstitutionName = i != null ? i.InstitutionName : null, + SpecializationName = s != null ? s.SpecializationName : null, + DistrictId = dis != null ? dis.DistrictId : 0, + DistrictName = dis != null ? dis.DistrictName : null + }; + + return await query.ToListAsync(); + } + public async Task PostPutDoctor(DoctorDto dto) + { + var response = new Response(); + + try + { + Infrastructure.Entities.Doctor.Doctor? existingDoctor = null; + ForDoctorApproval? existingApproval = null; + + // Check if DoctorId exists + if (dto.DoctorId > 0) + { + existingDoctor = await GetByIdAsync(dto.DoctorId); + existingApproval = await _context.ForDoctorApprovals + .FirstOrDefaultAsync(x => x.DoctorId == dto.DoctorId); + } + + int doctorId = dto.DoctorId; + + // Step 1: Insert/Update in main Doctors table first to get DoctorId + if (existingDoctor == null) + { + // CREATE new doctor in main table with Status = 1 (For DSM Approval) + var newDoctor = MapToNewDoctor(dto); + newDoctor.Status = 1; // Always start with "For DSM Approval" + newDoctor.IsActive = false; // Not active until approved + + await AddAsync(newDoctor); + await _context.SaveChangesAsync(); // Save to get DoctorId + + doctorId = newDoctor.DoctorId; // Get the auto-generated DoctorId + } + else + { + // UPDATE existing doctor - keep current status, don't change it here + // The doctor table will only be updated when status = 3 (Approved) + // For now, we just ensure it exists + doctorId = existingDoctor.DoctorId; + } + + // Step 2: Insert/Update in ForDoctorApproval staging table + if (existingApproval == null) + { + // INSERT into ForDoctorApproval + var newApproval = new ForDoctorApproval + { + ForDoctorApprovalId = Guid.NewGuid(), + DoctorId = doctorId, // Use the DoctorId from main table + FirstName = dto.FirstName, + MiddleInitial = dto.MiddleInitial, + LastName = dto.LastName, + MaxVisit = dto.MaxVisit, + SpecializationId = dto.SpecializationId, + UserId = dto.UserId, + StageLevelId = dto.StageLevelId, + DistrictId = dto.DistrictId, + BirthDate = dto.BirthDate, + EmailAddress = dto.EmailAddress, + Address = dto.Address, + PhoneNo = dto.PhoneNo, + LicenseNo = dto.LicenseNo, + InstitutionId = dto.InstitutionId, + IsTerritorial = dto.IsTerritorial, + Status = 1, // Always "For DSM Approval" when submitted + CreatedDate = DateTime.UtcNow, + UpdatedDate = DateTime.UtcNow, + CreatedBy = dto.UserName + }; + + await _context.ForDoctorApprovals.AddAsync(newApproval); + await _context.SaveChangesAsync(); + + return new Response + { + Success = true, + Data = new { DoctorId = doctorId, Approval = newApproval }, + Message = "Doctor submitted for approval successfully.", + MessCode = 1 + }; + } + else + { + // UPDATE existing ForDoctorApproval + existingApproval.FirstName = dto.FirstName; + existingApproval.MiddleInitial = dto.MiddleInitial; + existingApproval.LastName = dto.LastName; + existingApproval.MaxVisit = dto.MaxVisit; + existingApproval.DistrictId = dto.DistrictId; + existingApproval.SpecializationId = dto.SpecializationId; + existingApproval.BirthDate = dto.BirthDate; + existingApproval.EmailAddress = dto.EmailAddress; + existingApproval.Address = dto.Address; + existingApproval.PhoneNo = dto.PhoneNo; + existingApproval.LicenseNo = dto.LicenseNo; + existingApproval.InstitutionId = dto.InstitutionId; + existingApproval.IsTerritorial = dto.IsTerritorial; + existingApproval.UserId = dto.UserId; + existingApproval.StageLevelId = dto.StageLevelId; + existingApproval.Status = 1; // Reset to "For DSM Approval" + existingApproval.UpdatedDate = DateTime.UtcNow; + existingApproval.UpdatedBy = dto.UpdatedBy; + + _context.ForDoctorApprovals.Update(existingApproval); + await _context.SaveChangesAsync(); + + return new Response + { + Success = true, + Data = new { DoctorId = doctorId, Approval = existingApproval }, + Message = "Doctor information updated and resubmitted for approval.", + MessCode = 2 + }; + } + } + catch (Exception ex) + { + return new Response + { + Success = false, + Message = ex.Message, + MessCode = 0 + }; + } + } + + public async Task ApproveDoctor(int doctorId, byte newStatus, string updatedBy) + { + var response = new Response(); + + try + { + // Get the approval record from staging table + var approval = await _context.ForDoctorApprovals + .FirstOrDefaultAsync(x => x.DoctorId == doctorId); + + if (approval == null) + { + return new Response + { + Success = false, + Message = "Approval record not found for this doctor.", + MessCode = 0 + }; + } + + // Get the doctor from main table + var doctor = await GetByIdAsync(doctorId); + + if (doctor == null) + { + return new Response + { + Success = false, + Message = "Doctor not found in main table.", + MessCode = 0 + }; + } + + // Validate status progression + // Status flow: 1 (For DSM) -> 2 (For NSM) -> 3 (Approved) or 0 (Denied at any step) + if (newStatus != 0 && newStatus != 2 && newStatus != 3) + { + return new Response + { + Success = false, + Message = "Invalid status. Use 0 (Denied), 2 (For NSM Approval), or 3 (Approved).", + MessCode = 0 + }; + } + + // Update status in ForDoctorApproval staging table + approval.Status = newStatus; + approval.UpdatedDate = DateTime.UtcNow; + _context.ForDoctorApprovals.Update(approval); + + // Handle different status scenarios + if (newStatus==3) // APPROVED - Copy from staging to main table + { + // Apply all changes from ForDoctorApproval to Doctors table + doctor.FirstName = approval.FirstName; + doctor.MiddleInitial = approval.MiddleInitial; + doctor.LastName = approval.LastName; + doctor.MaxVisit = approval.MaxVisit; + doctor.DistrictId = approval.DistrictId; + doctor.SpecializationId = approval.SpecializationId; + doctor.StageLevelId = approval.StageLevelId; + doctor.BirthDate = approval.BirthDate; + doctor.EmailAddress = approval.EmailAddress; + doctor.Address = approval.Address; + doctor.PhoneNo = approval.PhoneNo; + doctor.LicenseNo = approval.LicenseNo; + doctor.InstitutionId = approval.InstitutionId; + doctor.IsTerritorial = approval.IsTerritorial; + doctor.Status = 3; + doctor.IsActive = true; // Activate the doctor + doctor.UpdatedDate = DateTime.UtcNow; + doctor.UpdatedBy = updatedBy; + + Update(doctor); + + // Optionally, you can delete or archive the approval record + // _context.ForDoctorApprovals.Remove(approval); + } + else if (newStatus == 0) // DENIED + { + // Update doctor status to denied + doctor.Status = 0; + doctor.IsActive = false; + doctor.UpdatedDate= DateTime.UtcNow; + doctor.UpdatedBy = updatedBy; + Update(doctor); + } + else if (newStatus == 2) // FOR NSM APPROVAL + { + // Just update the staging table status, keep doctor status as is + doctor.Status = 2; // Update main table to show it's in NSM review + doctor.UpdatedDate = DateTime.UtcNow; + doctor.UpdatedBy = updatedBy; + Update(doctor); + } + + await _context.SaveChangesAsync(); + + string statusMessage = newStatus switch + { + 0 => "denied", + 2 => "forwarded to NSM for approval", + 3 => "approved and activated in the system", + _ => "updated" + }; + + return new Response + { + Success = true, + Data = new { Doctor = doctor, Approval = approval }, + Message = $"Doctor has been {statusMessage}.", + MessCode = 2 + }; + } + catch (Exception ex) + { + return new Response + { + Success = false, + Message = ex.Message, + MessCode = 0 + }; + } + } + + public async Task> GetPendingApprovals(DoctorDto dto) + { + var query = + from fda in _context.ForDoctorApprovals + join d in _context.Doctors + on fda.DoctorId equals d.DoctorId into fdaDoc + from d in fdaDoc.DefaultIfEmpty() + join i in _context.Institutions + on fda.InstitutionId equals i.InstitutionId into fdai + from i in fdai.DefaultIfEmpty() + join dis in _context.Districts + on fda.DistrictId equals dis.DistrictId into fdaDist + from dis in fdaDist.DefaultIfEmpty() + join u in _context.Users + on d.UserId equals u.Id into user + from u in user.DefaultIfEmpty() + where fda.Status == 1 || u.Company == dto.Company + select new ForDoctorApprovalDto + { + ForDoctorApprovalId = fda.ForDoctorApprovalId, + DoctorId = fda.DoctorId, + FirstName = fda.FirstName, + MiddleInitial = fda.MiddleInitial, + LastName = fda.LastName, + EmailAddress = fda.EmailAddress, + PhoneNo = fda.PhoneNo, + SpecializationId = fda.SpecializationId, + LicenseNo = fda.LicenseNo, + Address = fda.Address, + BirthDate = fda.BirthDate, + MaxVisit = fda.MaxVisit, + MedRepName=u.UserName, + Status = fda.Status, + InstitutionId = fda.InstitutionId, + InstitutionName = i != null ? i.InstitutionName : null, + DistrictId = fda.DistrictId, + DistrictName = dis != null ? dis.DistrictName : null, + UserId = fda.UserId, + StageLevelId = fda.StageLevelId, + IsTerritorial = fda.IsTerritorial + }; + + return await query.ToListAsync(); + } + + private Infrastructure.Entities.Doctor.Doctor MapToNewDoctor(DoctorDto dto) + { + return new Infrastructure.Entities.Doctor.Doctor + { + FirstName = dto.FirstName, + MiddleInitial = dto.MiddleInitial, + LastName = dto.LastName, + MaxVisit = dto.MaxVisit, + SpecializationId = dto.SpecializationId, + UserId = dto.UserId, + StageLevelId = dto.StageLevelId, + DistrictId = dto.DistrictId, + BirthDate = dto.BirthDate, + EmailAddress = dto.EmailAddress, + Address = dto.Address, + PhoneNo = dto.PhoneNo, + LicenseNo = dto.LicenseNo, + InstitutionId = dto.InstitutionId, + IsTerritorial = dto.IsTerritorial, + Status = dto.Status, + IsActive = true, + CreatedDate = DateTime.UtcNow, + UpdatedDate = DateTime.UtcNow, + CreatedBy = dto.UserName, + UpdatedBy = dto.UserName, + }; + } + + public async Task> GetDoctorCoverageToday(DoctorDto dto) + { + var allItems = await _context.DoctorCoverageTodays + .FromSqlRaw("EXEC GetDoctorCoverageToday @UserId,@MRPlanDate", + new SqlParameter("@UserId", dto.UserId), + new SqlParameter("@MRPlanDate", dto.MRPlanDate)) + .ToListAsync(); + + return allItems ?? new List(); + } + } +} \ No newline at end of file diff --git a/LSFE.Domain/Services/ErrorLogger.cs b/LSFE.Domain/Services/ErrorLogger.cs new file mode 100644 index 0000000..3aba2f4 --- /dev/null +++ b/LSFE.Domain/Services/ErrorLogger.cs @@ -0,0 +1,31 @@ +using LSFE.Domain.Contracts.Doctor; +using LSFE.Infrastructure.Database; +using LSFE.Infrastructure.Entities; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Domain.Services +{ + public interface IErrorLogger + { + Task LogErrorAsync(ErrorLogs error); + } + + public class ErrorLogger : GenericRepository, IErrorLogger + { + private readonly LSFEDbContext _context; + public ErrorLogger(LSFEDbContext context) : base(context) + { + _context = context; + } + public async Task LogErrorAsync(ErrorLogs error) + { + await _dbContext.AddAsync(error); + await _dbContext.SaveChangesAsync(); + } + } +} diff --git a/LSFE.Domain/Services/ExcelInventoryService.cs b/LSFE.Domain/Services/ExcelInventoryService.cs new file mode 100644 index 0000000..932b79a --- /dev/null +++ b/LSFE.Domain/Services/ExcelInventoryService.cs @@ -0,0 +1,632 @@ +using Azure.Core; +using ClosedXML.Excel; +using DocumentFormat.OpenXml.Office2016.Excel; +using LSFE.Domain.Contracts; +using LSFE.Domain.Helpers; +using LSFE.Infrastructure.Entities; +using LSFE.Infrastructure.Entities.Maintenance; +using LSFE.Infrastructure.Model; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Domain.Services +{ + public class ExcelInventoryService + { + private readonly IUnitOfWork _unitOfWork; + private readonly ILogger _logger; + private const int BATCH_SIZE = 100; + public readonly UserManager _userManager; + + public ExcelInventoryService(IUnitOfWork unitOfWork, ILogger logger, + UserManager userManager) + { + _unitOfWork = unitOfWork; + _logger = logger; + _userManager = userManager; + } + public async Task ProcessExcelFileAsync(IFormFile file, string userId, string userName) + { + var response = new ExcelUploadResponse(); + + try + { + // Use ArrayPool for better memory management + var buffer = System.Buffers.ArrayPool.Shared.Rent((int)file.Length); + try + { + using var fileStream = file.OpenReadStream(); + await fileStream.ReadAsync(buffer.AsMemory(0, (int)file.Length)); + + using var stream = new MemoryStream(buffer, 0, (int)file.Length, writable: false); + using var workbook = new XLWorkbook(stream); + + var worksheet = workbook.Worksheets.FirstOrDefault(); + if (worksheet == null) + { + response.Success = false; + response.Message = "No worksheet found in the Excel file"; + response.MessCode = 0; + return response; + } + + // Validate headers + var (isValid, errors) = ExcelTemplateValidator.ValidateHeaders(worksheet); + if (!isValid) + { + response.Success = false; + response.Message = "Invalid Excel template. Please use the correct template."; + response.Errors = errors; + response.MessCode = 0; + return response; + } + + var rowCount = worksheet.LastRowUsed()?.RowNumber() ?? 0; + response.TotalRows = rowCount - 1; + + // Calculate current year and month once for performance + var currentDate = DateTime.Now; + var currentYear = currentDate.Year; + var currentMonth = currentDate.Month; + + // Pre-load existing inventory data to avoid N+1 query problem + var existingInventory = await LoadExistingInventoryAsync(worksheet, rowCount); + + // Pre-load and validate all MedRep names from the Excel file + var validMedReps = await ValidateMedRepNamesAsync(worksheet, rowCount, response); + if (!validMedReps.Any()) + { + response.Success = false; + response.Message = "No valid MedRep users found in the Excel file."; + response.MessCode = 0; + return response; + } + + // Process in batches + var inventoriesToInsert = new List(); + var summaryDict = new Dictionary(); + + for (int row = 2; row <= rowCount; row++) + { + try + { + var brandName = worksheet.Cell(row, 1).Value.ToString()?.Trim(); + var year = worksheet.Cell(row, 14).Value.ToString()?.Trim(); + var medRepName = worksheet.Cell(row, 15).Value.ToString()?.Trim(); + + if (string.IsNullOrWhiteSpace(brandName)) + { + response.SkippedRows++; + continue; + } + + // Validate MedRep Name exists in the system + if (string.IsNullOrWhiteSpace(medRepName)) + { + response.Errors.Add($"Row {row}: MedRep Name is required."); + response.SkippedRows++; + continue; + } + + if (!validMedReps.Contains(medRepName.ToLower())) + { + response.Errors.Add($"Row {row}: Invalid MedRep User Name '{medRepName}'. User does not exist in the system."); + response.SkippedRows++; + continue; + } + + if (!int.TryParse(year, out int cycleYear)) + { + response.Errors.Add($"Row {row}: Invalid year '{year}'"); + response.SkippedRows++; + continue; + } + + // Validate year is not in the past + if (cycleYear < currentYear) + { + response.Errors.Add($"Row {row}: Year {cycleYear} is in the past. Only current month onwards is allowed."); + response.SkippedRows++; + continue; + } + + // Process each month + for (byte month = 1; month <= 12; month++) + { + var cellValue = worksheet.Cell(row, month + 1).Value.ToString()?.Trim(); + + if (!int.TryParse(cellValue, out int qty) || qty <= 0) + { + continue; + } + + // Validate date is not in the past (current month or future only) + if (cycleYear == currentYear && month < currentMonth) + { + _logger.LogDebug($"Row {row}, Month {month}: Skipping past date - {month}/{cycleYear}"); + response.Errors.Add($"Row {row}, Month {month}: Date {month}/{cycleYear} is in the past. Only {currentMonth}/{currentYear} onwards is allowed."); + response.SkippedRows++; + continue; + } + + // Check if record exists using in-memory lookup (much faster) + var key = $"{brandName}|{month}|{cycleYear}|{medRepName}"; + if (existingInventory.Contains(key)) + { + _logger.LogDebug($"Row {row}, Month {month}: Skipping duplicate - {key}"); + continue; // Skip duplicates + } + + // Also check if we already added this in current batch + if (inventoriesToInsert.Any(x => + x.Description == brandName && + x.CycleMonth == month && + x.CycleYear == cycleYear && + x.MedRepName == medRepName)) + { + _logger.LogDebug($"Row {row}, Month {month}: Skipping - already in current batch"); + continue; + } + + var inventory = new Inventory + { + InventoryId = Guid.NewGuid(), + UserId = userId, + Description = brandName, + QtyIn = qty, + QtyOut = 0, + QtyBalance = qty, + CycleMonth = month, + CycleYear = cycleYear, + MedRepName = medRepName, + CreatedBy = userName, + CreatedDate = DateTime.Now, + IsActive = true + }; + + inventoriesToInsert.Add(inventory); + response.ProcessedRows++; + + // Update summary + var summaryKey = $"{brandName}|{medRepName}"; + if (!summaryDict.TryGetValue(summaryKey, out var summary)) + { + summary = new InventorySummary + { + Brand = brandName, + TotalQty = 0, + MedRepName = medRepName + }; + summaryDict[summaryKey] = summary; + } + summary.TotalQty += qty; + } + + // Batch insert to reduce database round trips + if (inventoriesToInsert.Count >= BATCH_SIZE) + { + await _unitOfWork.Inventory.AddRangeAsync(inventoriesToInsert); + await _unitOfWork.SaveChangesAsync(); + inventoriesToInsert.Clear(); + } + } + catch (Exception ex) + { + response.Errors.Add($"Row {row}: {ex.Message}"); + response.SkippedRows++; + _logger.LogError(ex, $"Error processing row {row}"); + } + } + + // Insert remaining items + if (inventoriesToInsert.Any()) + { + await _unitOfWork.Inventory.AddRangeAsync(inventoriesToInsert); + await _unitOfWork.SaveChangesAsync(); + } + + response.InsertedItems = summaryDict.Values.ToList(); + response.Success = true; + response.MessCode = 1; + response.Message = $"Successfully processed {response.ProcessedRows} inventory records. Skipped {response.SkippedRows} duplicates/invalid rows."; + + _logger.LogInformation($"Excel processing completed: {response.ProcessedRows} processed, {response.SkippedRows} skipped, {response.TotalRows} total rows"); + } + finally + { + // Return buffer to pool + System.Buffers.ArrayPool.Shared.Return(buffer); + } + } + catch (Exception ex) + { + response.Success = false; + response.Message = $"Error processing Excel file: {ex.Message}"; + response.MessCode = 0; + response.Errors.Add(ex.ToString()); + _logger.LogError(ex, "Error processing Excel file"); + } + + return response; + } + + /// + /// Validates all MedRep names from the Excel file against the UserManager + /// Returns a HashSet of valid (lowercase) usernames for O(1) lookup during processing + /// + private async Task> ValidateMedRepNamesAsync(IXLWorksheet worksheet, int rowCount, ExcelUploadResponse response) + { + var validMedReps = new HashSet(StringComparer.OrdinalIgnoreCase); + var uniqueMedRepNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + // Collect unique MedRep names from Excel + for (int row = 2; row <= rowCount; row++) + { + var medRepName = worksheet.Cell(row, 15).Value.ToString()?.Trim(); + if (!string.IsNullOrWhiteSpace(medRepName)) + { + uniqueMedRepNames.Add(medRepName); + } + } + + // Validate each unique MedRep name against UserManager + foreach (var medRepName in uniqueMedRepNames) + { + var user = await _userManager.FindByNameAsync(medRepName.ToLower()); + if (user != null) + { + validMedReps.Add(medRepName.ToLower()); + _logger.LogDebug($"MedRep '{medRepName}' validated successfully"); + } + else + { + _logger.LogWarning($"MedRep '{medRepName}' not found in the system"); + } + } + + _logger.LogInformation($"Validated {validMedReps.Count} out of {uniqueMedRepNames.Count} unique MedRep names"); + + return validMedReps; + } + + /// + /// Pre-loads existing inventory records from database to avoid N+1 queries + /// This validates against ALL records in the inventory table (from all users) + /// to prevent duplicate uploads + /// + private async Task> LoadExistingInventoryAsync(IXLWorksheet worksheet, int rowCount) + { + var brandNames = new HashSet(); + var years = new HashSet(); + var medRepNames = new HashSet(); + var months = new HashSet { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; + + // Collect unique brands, years, and med rep names from Excel file being uploaded + for (int row = 2; row <= rowCount; row++) + { + var brandName = worksheet.Cell(row, 1).Value.ToString()?.Trim(); + var yearStr = worksheet.Cell(row, 14).Value.ToString()?.Trim(); + var medRepName = worksheet.Cell(row, 15).Value.ToString()?.Trim(); + + if (!string.IsNullOrWhiteSpace(brandName)) + { + brandNames.Add(brandName); + } + + if (int.TryParse(yearStr, out int year)) + { + years.Add(year); + } + + if (!string.IsNullOrWhiteSpace(medRepName)) + { + medRepNames.Add(medRepName); + } + } + + if (!brandNames.Any() || !years.Any()) + { + return new HashSet(); + } + + // Single query to load ALL matching records from inventory table + var existingRecords = await _unitOfWork.Inventory + .FindAllAsync(inv => + brandNames.Contains(inv.Description) && + months.Contains(inv.CycleMonth) && + years.Contains(inv.CycleYear) && + medRepNames.Contains(inv.MedRepName) && + inv.IsActive == true); + + // Create lookup set with composite key: Brand|Month|Year|MedRepName + var existingKeys = new HashSet( + existingRecords.Select(inv => + $"{inv.Description}|{inv.CycleMonth}|{inv.CycleYear}|{inv.MedRepName}")); + + _logger.LogInformation($"Loaded {existingKeys.Count} existing inventory records for validation"); + + return existingKeys; + } + } +} +/*#region +public async Task ProcessExcelFileAsync(IFormFile file, string userId, string userName) +{ + var response = new ExcelUploadResponse(); + + try + { + // Use ArrayPool for better memory management + var buffer = System.Buffers.ArrayPool.Shared.Rent((int)file.Length); + try + { + using var fileStream = file.OpenReadStream(); + await fileStream.ReadAsync(buffer.AsMemory(0, (int)file.Length)); + + using var stream = new MemoryStream(buffer, 0, (int)file.Length, writable: false); + using var workbook = new XLWorkbook(stream); + + var worksheet = workbook.Worksheets.FirstOrDefault(); + if (worksheet == null) + { + response.Success = false; + response.Message = "No worksheet found in the Excel file"; + response.MessCode = 0; + return response; + } + + // Validate headers + var (isValid, errors) = ExcelTemplateValidator.ValidateHeaders(worksheet); + if (!isValid) + { + response.Success = false; + response.Message = "Invalid Excel template. Please use the correct template."; + response.Errors = errors; + response.MessCode = 0; + return response; + } + + var user = await _userManager.FindByNameAsync(request.UserName.ToLower()); + if (user == null) + { + response.Success = false; + response.Message = "Invalid MedRep User Name."; + response.Errors = errors; + response.MessCode = 0; + return response; + } + + var rowCount = worksheet.LastRowUsed()?.RowNumber() ?? 0; + response.TotalRows = rowCount - 1; + + // Calculate current year and month once for performance + var currentDate = DateTime.Now; + var currentYear = currentDate.Year; + var currentMonth = currentDate.Month; + + // Pre-load existing inventory data to avoid N+1 query problem + var existingInventory = await LoadExistingInventoryAsync(worksheet, rowCount); + + // Process in batches + var inventoriesToInsert = new List(); + var summaryDict = new Dictionary(); + + for (int row = 2; row <= rowCount; row++) + { + try + { + var brandName = worksheet.Cell(row, 1).Value.ToString()?.Trim(); + var year = worksheet.Cell(row, 14).Value.ToString()?.Trim(); + var medRepName = worksheet.Cell(row, 15).Value.ToString()?.Trim(); + + if (string.IsNullOrWhiteSpace(brandName)) + { + response.SkippedRows++; + continue; + } + + if (!int.TryParse(year, out int cycleYear)) + { + response.Errors.Add($"Row {row}: Invalid year '{year}'"); + response.SkippedRows++; + continue; + } + + // Validate year is not in the past + if (cycleYear < currentYear) + { + response.Errors.Add($"Row {row}: Year {cycleYear} is in the past. Only current month onwards is allowed."); + response.SkippedRows++; + continue; + } + + // Process each month + for (byte month = 1; month <= 12; month++) + { + var cellValue = worksheet.Cell(row, month + 1).Value.ToString()?.Trim(); + + if (!int.TryParse(cellValue, out int qty) || qty <= 0) + { + continue; + } + + // Validate date is not in the past (current month or future only) + if (cycleYear == currentYear && month < currentMonth) + { + _logger.LogDebug($"Row {row}, Month {month}: Skipping past date - {month}/{cycleYear}"); + response.Errors.Add($"Row {row}, Month {month}: Date {month}/{cycleYear} is in the past. Only {currentMonth}/{currentYear} onwards is allowed."); + response.SkippedRows++; + continue; + } + + // Check if record exists using in-memory lookup (much faster) + // Include MedRepName in key because same brand can be distributed by different reps + var key = $"{brandName}|{month}|{cycleYear}|{medRepName}"; + if (existingInventory.Contains(key)) + { + _logger.LogDebug($"Row {row}, Month {month}: Skipping duplicate - {key}"); + continue; // Skip duplicates + } + + // Also check if we already added this in current batch (prevent duplicates within same file) + if (inventoriesToInsert.Any(x => + x.Description == brandName && + x.CycleMonth == month && + x.CycleYear == cycleYear && + x.MedRepName == medRepName)) + { + _logger.LogDebug($"Row {row}, Month {month}: Skipping - already in current batch"); + continue; + } + + var inventory = new Inventory + { + InventoryId = Guid.NewGuid(), + UserId = userId, + Description = brandName, + QtyIn = qty, + QtyOut = 0, + QtyBalance = qty, + CycleMonth = month, + CycleYear = cycleYear, + MedRepName = medRepName, + CreatedBy = userName, + CreatedDate = DateTime.Now, + IsActive = true + }; + + inventoriesToInsert.Add(inventory); + response.ProcessedRows++; + + // Update summary + var summaryKey = $"{brandName}|{medRepName}"; + if (!summaryDict.TryGetValue(summaryKey, out var summary)) + { + summary = new InventorySummary + { + Brand = brandName, + TotalQty = 0, + MedRepName = medRepName + }; + summaryDict[summaryKey] = summary; + } + summary.TotalQty += qty; + } + + // Batch insert to reduce database round trips + if (inventoriesToInsert.Count >= BATCH_SIZE) + { + await _unitOfWork.Inventory.AddRangeAsync(inventoriesToInsert); + await _unitOfWork.SaveChangesAsync(); + inventoriesToInsert.Clear(); + } + } + catch (Exception ex) + { + response.Errors.Add($"Row {row}: {ex.Message}"); + response.SkippedRows++; + _logger.LogError(ex, $"Error processing row {row}"); + } + } + + // Insert remaining items + if (inventoriesToInsert.Any()) + { + await _unitOfWork.Inventory.AddRangeAsync(inventoriesToInsert); + await _unitOfWork.SaveChangesAsync(); + } + + response.InsertedItems = summaryDict.Values.ToList(); + response.Success = true; + response.MessCode = 1; + response.Message = $"Successfully processed {response.ProcessedRows} inventory records. Skipped {response.SkippedRows} duplicates/invalid rows."; + + _logger.LogInformation($"Excel processing completed: {response.ProcessedRows} processed, {response.SkippedRows} skipped, {response.TotalRows} total rows"); + } + finally + { + // Return buffer to pool + System.Buffers.ArrayPool.Shared.Return(buffer); + } + } + catch (Exception ex) + { + response.Success = false; + response.Message = $"Error processing Excel file: {ex.Message}"; + response.MessCode = 0; + response.Errors.Add(ex.ToString()); + _logger.LogError(ex, "Error processing Excel file"); + } + + return response; +} +/// +/// Pre-loads existing inventory records from database to avoid N+1 queries +/// This validates against ALL records in the inventory table (from all users) +/// to prevent duplicate uploads +/// +private async Task> LoadExistingInventoryAsync(IXLWorksheet worksheet, int rowCount) +{ + var brandNames = new HashSet(); + var years = new HashSet(); + var medRepNames = new HashSet(); + var months = new HashSet { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; // All possible months + + // Collect unique brands, years, and med rep names from Excel file being uploaded + for (int row = 2; row <= rowCount; row++) + { + var brandName = worksheet.Cell(row, 1).Value.ToString()?.Trim(); + var yearStr = worksheet.Cell(row, 14).Value.ToString()?.Trim(); + var medRepName = worksheet.Cell(row, 15).Value.ToString()?.Trim(); + + if (!string.IsNullOrWhiteSpace(brandName)) + { + brandNames.Add(brandName); + } + + if (int.TryParse(yearStr, out int year)) + { + years.Add(year); + } + + if (!string.IsNullOrWhiteSpace(medRepName)) + { + medRepNames.Add(medRepName); + } + } + + if (!brandNames.Any() || !years.Any()) + { + return new HashSet(); + } + + // Single query to load ALL matching records from inventory table + // This includes records uploaded by ANY user (current or previous uploads) + // Filters by Brand, Month, Year, and MedRepName to ensure precise duplicate detection + var existingRecords = await _unitOfWork.Inventory + .FindAllAsync(inv => + brandNames.Contains(inv.Description) && + months.Contains(inv.CycleMonth) && + years.Contains(inv.CycleYear) && + medRepNames.Contains(inv.MedRepName) && + inv.IsActive == true); // Only check active records + + // Create lookup set with composite key: Brand|Month|Year|MedRepName + // This allows O(1) existence checks during row processing + // MedRepName is included because the same brand can be handled by different reps + var existingKeys = new HashSet( + existingRecords.Select(inv => + $"{inv.Description}|{inv.CycleMonth}|{inv.CycleYear}|{inv.MedRepName}")); + + _logger.LogInformation($"Loaded {existingKeys.Count} existing inventory records for validation"); + + return existingKeys; +} +#endregion*/ \ No newline at end of file diff --git a/LSFE.Domain/Services/GenericRepository.cs b/LSFE.Domain/Services/GenericRepository.cs new file mode 100644 index 0000000..2e6f423 --- /dev/null +++ b/LSFE.Domain/Services/GenericRepository.cs @@ -0,0 +1,40 @@ +using LSFE.Domain.Contracts; +using LSFE.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Domain.Services +{ + public class GenericRepository where TEntity : class + { + protected readonly LSFEDbContext _dbContext; + + public GenericRepository(LSFEDbContext context) + { + _dbContext = context; + } + + public async Task GetByIdAsync(int id) => await _dbContext.Set().FindAsync(id); + + public async Task> GetAllAsync() => await _dbContext.Set().ToListAsync(); + + public async Task AddAsync(TEntity entity) + { + await _dbContext.Set().AddAsync(entity); + } + + public void Update(TEntity entity) + { + _dbContext.Entry(entity).State = EntityState.Modified; + } + + public void Remove(TEntity entity) + { + _dbContext.Set().Remove(entity); + } + } +} diff --git a/LSFE.Domain/Services/Maintenance/MaintenanceRepo.cs b/LSFE.Domain/Services/Maintenance/MaintenanceRepo.cs new file mode 100644 index 0000000..685cc73 --- /dev/null +++ b/LSFE.Domain/Services/Maintenance/MaintenanceRepo.cs @@ -0,0 +1,135 @@ +using LSFE.Domain.Contracts.Maintenance; +using LSFE.Infrastructure.Database; +using LSFE.Infrastructure.Dto.Maintenance; +using LSFE.Infrastructure.Model; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Domain.Services.Maintenance +{ + public class MaintenanceRepo : IMaintenanceRepo where TEntity : class + { + private readonly LSFEDbContext _context; + private readonly DbSet _dbSet; + public MaintenanceRepo(LSFEDbContext context) + { + _context = context; + _dbSet = _context.Set(); + } + public async Task AnyAsync(Expression> predicate) + { + return await _dbSet.AnyAsync(predicate); + } + public IQueryable Query() + { + return _context.Set().AsQueryable(); + } + public async Task> GetAllAsync(params Expression>[] includeProperties) + { + IQueryable query = _context.Set(); + + if (includeProperties != null) + { + foreach (var includeProperty in includeProperties) + { + query = query.Include(includeProperty); + } + } + + return await query.ToListAsync(); + } + + public async Task AddOrUpdateAsync(TEntity entity) + { + try + { + var keyProperty = _context.Model.FindEntityType(typeof(TEntity))? + .FindPrimaryKey()?.Properties.First(); + + if (keyProperty == null) + throw new InvalidOperationException($"No key defined for {typeof(TEntity).Name}"); + + var keyValue = keyProperty.PropertyInfo?.GetValue(entity); + + var existing = await _context.Set().FindAsync(keyValue); + + if (existing == null) + { + await _context.Set().AddAsync(entity); + } + else + { + _context.Entry(existing).CurrentValues.SetValues(entity); + _context.Entry(existing).State = EntityState.Modified; + } + + return new Response { Success = true, MessCode = 1 }; + } + catch (Exception ex) + { + return new Response + { + Success = false, + Message = ex.ToString(), + MessCode = 0 + }; + } + } + + public async Task SoftDeleteAsync(int id) + { + var entity = await _context.Set().FindAsync(id); + if (entity == null) + return new Response { Success = false, Message = "Not found" }; + + var prop = typeof(TEntity).GetProperty("IsActive"); + if (prop != null) + prop.SetValue(entity, false); + + return new Response { Success = true }; + } + public async Task FindAsync(Expression> predicate) + { + return await _context.Set().FirstOrDefaultAsync(predicate); + } + + public async Task GetByIdAsync(object id) + { + return await _context.Set().FindAsync(id); + } + + public async Task> FindAllAsync(Expression> predicate) + { + return await _context.Set() + .Where(predicate) + .AsNoTracking() + .ToListAsync(); + } + public async Task AddRangeAsync(IEnumerable entities) + { + if (entities == null || !entities.Any()) + { + return; + } + + await _context.Set().AddRangeAsync(entities); + } + public async Task DeleteAsync(Guid id) + { + var entity = await _context.Set().FindAsync(id); + + if (entity == null) + return new Response { Success = false, Message = "Not found" }; + + _context.Set().Remove(entity); + await _context.SaveChangesAsync(); + + return new Response { Success = true }; + } + } +} diff --git a/LSFE.Domain/Services/OutputParamMessage.cs b/LSFE.Domain/Services/OutputParamMessage.cs new file mode 100644 index 0000000..a21d289 --- /dev/null +++ b/LSFE.Domain/Services/OutputParamMessage.cs @@ -0,0 +1,27 @@ +using Microsoft.Data.SqlClient; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Domain.Services +{ + public static class OutputParamMessage + { + public static (SqlParameter MessCode, SqlParameter Message) CreateOutputParams() + { + var messCode = new SqlParameter("@MessCode", SqlDbType.TinyInt) { Direction = ParameterDirection.Output }; + var message = new SqlParameter("@Message", SqlDbType.VarChar, 8000) { Direction = ParameterDirection.Output }; + return (messCode, message); + } + public static (SqlParameter MessCode, SqlParameter Message, SqlParameter planId) CreateOutputParamsPlanId() + { + var messCode = new SqlParameter("@MessCode", SqlDbType.TinyInt) { Direction = ParameterDirection.Output }; + var message = new SqlParameter("@Message", SqlDbType.VarChar, 8000) { Direction = ParameterDirection.Output }; + var planId = new SqlParameter("@MRRawPlanIdOutPut", SqlDbType.VarChar, 8000) { Direction = ParameterDirection.Output }; + return (messCode, message, planId); + } + } +} diff --git a/LSFE.Domain/Services/Planning/PlanningRepo.cs b/LSFE.Domain/Services/Planning/PlanningRepo.cs new file mode 100644 index 0000000..bb45234 --- /dev/null +++ b/LSFE.Domain/Services/Planning/PlanningRepo.cs @@ -0,0 +1,667 @@ +using LSFE.Domain.Contracts.Planning; +using LSFE.Infrastructure.Database; +using LSFE.Infrastructure.Dto; +using LSFE.Infrastructure.Dto.Doctor; +using LSFE.Infrastructure.Dto.Planning; +using LSFE.Infrastructure.Entities.Maintenance; +using LSFE.Infrastructure.Entities.Planning; +using LSFE.Infrastructure.Model; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using Microsoft.VisualBasic; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using static LSFE.Domain.Services.OutputParamMessage; + +namespace LSFE.Domain.Services.Planning +{ + public class PlanningRepo : GenericRepository, IPlanningRepo + { + private readonly LSFEDbContext _context; + public PlanningRepo(LSFEDbContext context) : base(context) + { + _context = context; + } + #region Post Put + public async Task SoftDeleteAsync(int doctorId) + { + var response = new Response(); + + try + { + var existingDoctor = await GetByIdAsync(doctorId); + + if (existingDoctor == null) + { + response.Success = false; + response.Message = $"Doctor with ID {doctorId} not found."; + response.MessCode = 0; + return response; + } + + Update(existingDoctor); + await _context.SaveChangesAsync(); + + response.Success = true; + response.Data = existingDoctor; + response.Message = "Doctor deactivated successfully."; + response.MessCode = 3; // code for delete/soft delete + } + catch (Exception ex) + { + response.Success = false; + response.Message = ex.Message; + response.MessCode = 0; + } + + return response; + } + public async Task PostPutPlan(PlanningDto dto) + { + // Input validation + if (dto == null) + return new Response { Success = false, Message = "Invalid request data", MessCode = 0 }; + + var (messCode, message, planId) = CreateOutputParamsPlanId(); + + using var transaction = await _context.Database.BeginTransactionAsync(); + + try + { + // Execute the stored procedure + await _context.Database.ExecuteSqlRawAsync( + "EXEC PostPutPlan @UserId,@DoctorId,@CallDate,@CallType,@SLP," + + "@IsJoinCall,@IsLiterature,@MRPlanDetailId,@MRRawPlanId,@IsUpdateDelete,@MessCode OUTPUT,@Message OUTPUT,@MRRawPlanIdOutPut OUTPUT", + new SqlParameter("@UserId", dto.UserId), + new SqlParameter("@DoctorId", dto.DoctorId), + new SqlParameter("@CallDate", dto.CallDate), + new SqlParameter("@CallType", dto.SLP ?? "N/A"), + new SqlParameter("@SLP", dto.SLP ?? "N/A"), + new SqlParameter("@IsJoinCall", false), + new SqlParameter("@IsLiterature", dto.IsLiterature), + new SqlParameter("@MRPlanDetailId", + string.IsNullOrEmpty(dto.MRPlanDetailId) || dto.MRPlanDetailId == "N/A" + ? (object)DBNull.Value + : Guid.Parse(dto.MRPlanDetailId)), + new SqlParameter("@MRRawPlanId", + string.IsNullOrEmpty(dto.MRRawPlanId) || dto.MRRawPlanId == "N/A" + ? (object)DBNull.Value + : Guid.Parse(dto.MRRawPlanId)), + new SqlParameter("@IsUpdateDelete", dto.IsUpdateDelete), + messCode, + message, + planId); + + var code = (byte)(messCode.Value ?? 0); + var msg = message.Value?.ToString() ?? "No response message."; + var mrRawPlanId = planId.Value?.ToString() ?? string.Empty; + var success = code != 0; + + // If stored procedure failed, rollback and return + if (!success) + { + await transaction.RollbackAsync(); + return new Response { Success = false, Message = msg, MessCode = code }; + } + + // Validate that we have a valid plan ID for insert/update operations + if (string.IsNullOrEmpty(mrRawPlanId) && dto.IsUpdateDelete != 1) + { + await transaction.RollbackAsync(); + return new Response { Success = false, Message = "You cannot create more than one visit in a week!", MessCode = 0 }; + } + + // Get username for audit trail + var username = await _context.Users + .Where(u => u.Id == dto.UserId) + .Select(u => u.UserName) + .FirstOrDefaultAsync(); + + if (string.IsNullOrEmpty(username)) + { + await transaction.RollbackAsync(); + return new Response { Success = false, Message = "User not found", MessCode = 0 }; + } + + var now = DateTime.Now; + DateOnly today = DateOnly.FromDateTime(DateTime.Now); + + // Handle inventory based on operation type + if (dto.IsUpdateDelete == 1 || dto.IsUpdateDelete == 2) // UPDATE/DELETE + { + + if (!IsSameMonth(dto.CallDate, today)) + { + msg = await HandleInventoryDelete(dto, msg); + msg = await HandlePromosDelete(dto, msg); + + msg = await HandleInventoryInsert(dto, msg, username, now, mrRawPlanId); + msg = await HandlePromosInsert(dto, msg, username, now, mrRawPlanId); + } + } + else if (dto.IsUpdateDelete == 2) // UPDATE + { + if (!IsSameMonth(dto.CallDate, today)) + { + msg = await HandleInventoryUpdate(dto, msg, username, now); + msg = await HandlePromosUpdate(dto, msg, username, now, mrRawPlanId); + } + } + else // INSERT (new plan) + { + msg = await HandleInventoryInsert(dto, msg, username, now, mrRawPlanId); + msg = await HandlePromosInsert(dto, msg, username, now, mrRawPlanId); + } + + await _context.SaveChangesAsync(); + await transaction.CommitAsync(); + + return new Response + { + Success = true, + Message = msg, + MessCode = code + }; + } + catch (Exception ex) + { + await transaction.RollbackAsync(); + return new Response + { + Success = false, + Message = $"Error processing plan: {ex.Message}", + MessCode = 0 + }; + } + } + private bool IsSameMonth(DateOnly date, DateOnly reference) + { + return date.Year == reference.Year && date.Month == reference.Month; + } + + private async Task HandleInventoryDelete(PlanningDto dto, string msg) + { + if (string.IsNullOrEmpty(dto.MRRawPlanId)) + return msg + " Cannot delete inventory: Missing MRRawPlanId."; + + var mrRawPlanGuid = Guid.Parse(dto.MRRawPlanId); + + // Get all inventory transactions (ONE query only) + var transactions = await _context.InventoryTransactions + .Where(t => t.MRRawPlanId == mrRawPlanGuid) + .ToListAsync(); + + if (transactions.Count == 0) + return msg + " No linked inventory transactions found."; + + // Collect inventory IDs for batch query + var inventoryIds = transactions + .Select(t => t.InventoryId) + .Distinct() + .ToList(); + + // Load inventories in ONE query + var inventories = await _context.Inventory + .Where(i => inventoryIds.Contains(i.InventoryId)) + .ToDictionaryAsync(i => i.InventoryId); + + // Process each transaction update + foreach (var tx in transactions) + { + if (!inventories.TryGetValue(tx.InventoryId, out var inventory)) + continue; // if missing, skip safely + + inventory.QtyOut -= tx.QtyIn; // revert qty out + inventory.QtyBalance += tx.QtyIn; // restore qty + + if (inventory.QtyOut < 0) + inventory.QtyOut = 0; // safety check + + _context.Inventory.Update(inventory); + } + + // Remove all transaction records + _context.InventoryTransactions.RemoveRange(transactions); + + return msg + $" Deleted {transactions.Count} record(s)."; + } + + private async Task HandleInventoryUpdate(PlanningDto dto, string msg, + string username, DateTime now) + { + if (dto.SelectedSamples == null || !dto.SelectedSamples.Any()) + return msg; + + if (string.IsNullOrEmpty(dto.MRRawPlanId)) + { + return msg + " Cannot update inventory: Missing MRRawPlanId."; + } + + var mrRawPlanGuid = Guid.Parse(dto.MRRawPlanId); + + foreach (var sample in dto.SelectedSamples) + { + var inventoryTransaction = await _context.InventoryTransactions + .FirstOrDefaultAsync(i => i.InventoryId == sample.InventoryId + && i.MRRawPlanId == mrRawPlanGuid); + + var inventory = await _context.Inventory + .FirstOrDefaultAsync(i => i.InventoryId == sample.InventoryId); + + if (inventory == null) + { + msg += $" {sample.InventoryId}: Inventory not found."; + continue; + } + + if (inventoryTransaction == null) + { + msg += $" {inventory.Description}: No existing transaction found."; + continue; + } + + // Calculate quantity difference + int newQtyBalance = inventory.QtyIn - sample.SelectedQty; + + //Update the new qty balance + inventory.QtyBalance = newQtyBalance; + + // Validate balance + if (inventory.QtyBalance < 0) + { + msg += $" {inventory.Description}: Insufficient stock for update."; + continue; + } + + if (inventory.QtyBalance > inventory.QtyIn) + { + msg += $" {inventory.Description}: Balance exceeds original quantity."; + continue; + } + + // Update transaction + inventoryTransaction.AppointmentDate = dto.CallDate; + inventoryTransaction.QtyOut = sample.SelectedQty; + inventoryTransaction.QtyIn = sample.SelectedQty; + inventoryTransaction.UpdatedBy = username; + inventoryTransaction.UpdatedDate = now; + + _context.InventoryTransactions.Update(inventoryTransaction); + _context.Inventory.Update(inventory); + + msg += $" {inventory.Description}: Updated from {sample.OriginalQty} to {sample.SelectedQty} units."; + } + + return msg; + } + + private async Task HandleInventoryInsert(PlanningDto dto, string msg, + string username, DateTime now, string mrRawPlanId) + { + if (dto.SelectedSamples == null || !dto.SelectedSamples.Any()) + return msg; + + if (!Guid.TryParse(mrRawPlanId, out var planGuid)) + { + return msg + " Invalid plan ID for inventory insert."; + } + + var inventoryWarnings = new List(); + + foreach (var sample in dto.SelectedSamples) + { + var inventory = await _context.Inventory + .FirstOrDefaultAsync(i => i.InventoryId == sample.InventoryId); + + if (inventory == null) + { + inventoryWarnings.Add($"{sample.InventoryId}: Not found"); + continue; + } + + // Calculate quantity difference + int newQtyBalance = inventory.QtyIn - sample.SelectedQty; + + + // Create transaction + var inventoryTransaction = new InventoryTransaction + { + InventoryId = sample.InventoryId, + DoctorId = dto.DoctorId, + Description = sample.Description ?? inventory.Description ?? "Sample", + MRRawPlanId = planGuid, + AppointmentDate = dto.CallDate, + CreatedDate = now, + QtyIn = sample.SelectedQty, + QtyOut = sample.SelectedQty, + UserId = dto.UserId, + CreatedBy = username, + IsActive = true + }; + _context.InventoryTransactions.Add(inventoryTransaction); + + // Deduct from inventory + inventory.QtyBalance = newQtyBalance; + inventory.QtyOut = sample.SelectedQty; + inventory.UpdatedBy = username; + inventory.UpdatedDate = now; + _context.Inventory.Update(inventory); + } + + if (inventoryWarnings.Any()) + { + msg += $" (Inventory warnings: {string.Join("; ", inventoryWarnings)})"; + } + + return msg; + } + + private async Task HandlePromosDelete(PlanningDto dto, string msg) + { + if (dto.SelectedPromos == null || !dto.SelectedPromos.Any()) + return msg; + + if (string.IsNullOrEmpty(dto.MRRawPlanId)) + { + return msg + " Cannot delete promos: Missing MRRawPlanId."; + } + + var mrRawPlanGuid = Guid.Parse(dto.MRRawPlanId); + + var promosToDelete = await _context.ProductTransactions + .Where(pt => pt.MRRawPlanId == mrRawPlanGuid) + .ToListAsync(); + + _context.ProductTransactions.RemoveRange(promosToDelete); + + return msg + $" Deleted {promosToDelete.Count} promo(s)."; + } + + private async Task HandlePromosUpdate(PlanningDto dto, string msg, + string username, DateTime now, string mrRawPlanId) + { + if (dto.SelectedPromos == null || !dto.SelectedPromos.Any()) + return msg; + + if (string.IsNullOrEmpty(dto.MRRawPlanId)) + { + return msg + " Cannot update promos: Missing MRRawPlanId."; + } + + var mrRawPlanGuid = Guid.Parse(dto.MRRawPlanId); + + // Delete existing promos + var existingPromos = await _context.ProductTransactions + .Where(pt => pt.MRRawPlanId == mrRawPlanGuid) + .ToListAsync(); + + _context.ProductTransactions.RemoveRange(existingPromos); + + // Insert new promos + foreach (var product in dto.SelectedPromos) + { + var productTransaction = new ProductTransaction + { + ProductId = product.ProductId, + DoctorId = dto.DoctorId, + ProductName = product.ProductName, + AppointmentDate = dto.CallDate, + MRRawPlanId = mrRawPlanGuid, + CreatedDate = now, + CreatedBy = username, + UserId = dto.UserId, + IsActive = true + }; + + _context.ProductTransactions.Add(productTransaction); + } + + return msg + $" Updated promos: removed {existingPromos.Count}, added {dto.SelectedPromos.Count}."; + } + + private async Task HandlePromosInsert(PlanningDto dto, string msg, + string username, DateTime now, string mrRawPlanId) + { + if (dto.SelectedPromos == null || !dto.SelectedPromos.Any()) + return msg; + + if (!Guid.TryParse(mrRawPlanId, out var planGuid)) + { + return msg + " Invalid plan ID for promo insert."; + } + + foreach (var product in dto.SelectedPromos) + { + var productTransaction = new ProductTransaction + { + ProductId = product.ProductId, + DoctorId = dto.DoctorId, + ProductName= product.ProductName, + AppointmentDate = dto.CallDate, + MRRawPlanId = planGuid, + CreatedDate = now, + CreatedBy = username, + UserId = dto.UserId, + IsActive = true + }; + + _context.ProductTransactions.Add(productTransaction); + } + + return msg; + } + public async Task ApprovePlan(ApprovePlanDto dto) + { + if (dto == null || dto.AppointmentIds == null || !dto.AppointmentIds.Any()) + return new Response { Success = false, Message = "No appointments selected", MessCode = 0 }; + + // Filter out empty GUIDs + var guidList = dto.AppointmentIds.Where(id => id != Guid.Empty).ToList(); + + if (!guidList.Any()) + return new Response { Success = false, Message = "No valid appointment ids", MessCode = 0 }; + + using var tx = await _context.Database.BeginTransactionAsync(); + try + { + var plans = await _context.MRRawPlans + .Where(p => guidList.Contains(p.MRRawPlanId)) + .ToListAsync(); + + if (!plans.Any()) + return new Response { Success = false, Message = "No matching appointments found", MessCode = 0 }; + + int approvedCount = 0; + int alreadyApproved = 0; + var now = DateTime.UtcNow; + var userIdOrSystem = dto.UpdatedBy ?? "System"; + + foreach (var plan in plans) + { + if (plan.IsApproved == true) + { + alreadyApproved++; + continue; + } + plan.IsApproved = true; + plan.ApprovedBy = userIdOrSystem; + plan.ApprovedDate = now; + approvedCount++; + } + + if (approvedCount > 0) + { + _context.MRRawPlans.UpdateRange(plans.Where(p => p.IsApproved == true)); + await _context.SaveChangesAsync(); + } + + await tx.CommitAsync(); + + return new Response + { + Success = true, + Message = $"{approvedCount} approved, {alreadyApproved} already approved.", + MessCode = 1 + }; + } + catch (Exception ex) + { + await tx.RollbackAsync(); + return new Response { Success = false, Message = $"Error: {ex.Message}", MessCode = 0 }; + } + } + + public async Task CopyMonthlyMRRawPlans(PlanningDto dto) + { + try + { + var (messCode, message) = CreateOutputParams(); + + // Execute the stored procedure + await _context.Database.ExecuteSqlRawAsync( + "EXEC CopyMonthlyMRRawPlans @MonthPlanCopy,@NewMonthPlan,@UserId,@MessCode OUTPUT,@Message OUTPUT", + new SqlParameter("@MonthPlanCopy", dto.MonthPlanCopy), + new SqlParameter("@NewMonthPlan", dto.NewMonthPlan), + new SqlParameter("@UserId", dto.UserId), + messCode, + message); + + return new Response + { + Success = true, + Message = message.Value.ToString(), + MessCode = (byte)(messCode.Value ?? 0) + }; + } + catch (Exception ex) + { + + return new Response + { + Success = true, + Message = ex.ToString() ?? ex.InnerException.ToString(), + MessCode = 0 + }; + } + + } + #endregion + + #region Get + public async Task> GetDoctorWithPlan(DoctorDto dto) + { + string spName = dto.IsPlanning + ? "GetDoctorWithPlan" + : "GetDoctorWithPlanStatus"; + + var allItems = await _context.DoctorWithPlan + .FromSqlRaw( + $"EXEC {spName} @UserId, @MRPlanDate", + new SqlParameter("@UserId", dto.UserId), + new SqlParameter("@MRPlanDate", dto.MRPlanDate)) + .ToListAsync(); + + return allItems ?? new List(); + } + + public async Task> GetPlan(PlanningDto dto) + { + var allItems = await _context.MRRawPlans + .FromSqlRaw("EXEC GetPlan @UserId", + new SqlParameter("@UserId", dto.UserId)) + .ToListAsync(); + + return allItems ?? new List(); + } + public async Task> GetDoctorById(DoctorDto dto) + { + var allItems = await _context.DoctorByIds + .FromSqlRaw("EXEC GetDoctorById @UserId,@DoctorId,@MRPlanDate", + new SqlParameter("@UserId", dto.UserId), + new SqlParameter("@DoctorId", dto.DoctorId), + new SqlParameter("@MRPlanDate", dto.MRPlanDate)) + .ToListAsync(); + + return allItems ?? new List(); + } + public async Task> GetPendingApprovals(PlanningDto dto) + { + var mrPlanDate = GetValidDate(dto.MRPlanDate); + + var allItems = await _context.PendingApprovals + .FromSqlRaw( + "EXEC GetPendingApprovals @UserId,@DoctorId,@MRPlanDate", + new SqlParameter("@UserId", dto.UserId), + new SqlParameter("@DoctorId", dto.DoctorId), + new SqlParameter("@MRPlanDate", mrPlanDate) + ) + .ToListAsync(); + + return allItems ?? new List(); + } + private static DateTime GetValidDate(object? input) + { + if (input is DateTime dt) + return dt; + + if (input is string s && DateTime.TryParse(s, out var parsed)) + return parsed; + + return DateTime.UtcNow; + } + + public async Task> GetAllAppointmentToday(CredentialDto dto) + { + var allItems = await _context.MRRawPlans + .FromSqlRaw("EXEC GetAllAppointmentToday @UserId", + new SqlParameter("@UserId", dto.UserId)) + .ToListAsync(); + + return allItems ?? new List(); + } + public async Task> GetAllMissReschedule(CredentialDto dto) + { + var allItems = await _context.MRRawPlans + .FromSqlRaw("EXEC GetAllMissReschedule @UserId,@IsMissed,@IsReschedule", + new SqlParameter("@UserId", dto.UserId), + new SqlParameter("@IsMissed", dto.IsMissed), + new SqlParameter("@IsReschedule", dto.IsReschedule)) + .ToListAsync(); + + return allItems ?? new List(); + } + + public async Task> GetSampleTransaction(Guid mrRawPlanId) + { + var allItems = await _context.InventoryTransactions + .FromSqlRaw("EXEC GetSampleTransaction @MRRawPlanId", + new SqlParameter("@MRRawPlanId", mrRawPlanId)).ToListAsync(); + + return allItems ?? new List(); + } + + public async Task> GetProductTransaction(Guid mrRawPlanId) + { + var allItems = await _context.ProductTransactions + .FromSqlRaw("EXEC GetProductTransaction @MRRawPlanId", + new SqlParameter("@MRRawPlanId", mrRawPlanId)).ToListAsync(); + + return allItems ?? new List(); + } + + public async Task> GetMRRawPlans(PlanByUserIdRequest dto) + { + var plans = await _context.MRRawPlansDto + .FromSqlRaw("EXEC GetMRRawPlans @UserId", + new SqlParameter("@UserId", dto.UserId)).ToListAsync(); + + return plans ?? new List(); + } + #endregion + } +} diff --git a/LSFE.Domain/Services/Reports/ReportsRepo.cs b/LSFE.Domain/Services/Reports/ReportsRepo.cs new file mode 100644 index 0000000..f5a6ff9 --- /dev/null +++ b/LSFE.Domain/Services/Reports/ReportsRepo.cs @@ -0,0 +1,130 @@ +using LSFE.Domain.Contracts.Reports; +using LSFE.Infrastructure.Database; +using LSFE.Infrastructure.Dto.Reports; +using LSFE.Infrastructure.Entities.Reports; +using LSFE.Infrastructure.Model; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Domain.Services.Reports +{ + public class ReportsRepo : IReportsRepo + { + private readonly LSFEDbContext _context; + public ReportsRepo(LSFEDbContext context) + { + _context = context; + } + protected Task<(DateTime StartDate, DateTime EndDate)> + GetCurrentWholeMonth() + { + var now = DateTime.UtcNow; + + var startDate = new DateTime( + now.Year, + now.Month, + 1, + 0, 0, 0, + DateTimeKind.Utc + ); + + var endDate = startDate.AddMonths(1); + + return Task.FromResult((startDate, endDate)); + } + public async Task> GetCallRateReport(ReportsDto dto) + { + var (startDate, endDate) = await GetCurrentWholeMonth(); + + var allItems = await _context.CallRateReports + .FromSqlRaw( + "EXEC GetCallRateReport @UserId,@StartDate,@EndDate,@Period", + new SqlParameter("@UserId", dto.UserId), + new SqlParameter("@StartDate", dto.StartDate ?? startDate), + new SqlParameter("@EndDate", dto.EndDate ?? endDate), + new SqlParameter("@Period", dto.Period ?? "monthly")).ToListAsync(); + + return allItems ?? new List(); + } + + public async Task> GetCallReachReport(ReportsDto dto) + { + var (startDate, endDate) = await GetCurrentWholeMonth(); + + var allItems = await _context.CallReachReports + .FromSqlRaw( + "EXEC GetCallReachReport @UserId,@StartDate,@EndDate,@Period", + new SqlParameter("@UserId", dto.UserId), + new SqlParameter("@StartDate", dto.StartDate ?? startDate), + new SqlParameter("@EndDate", dto.EndDate ?? endDate), + new SqlParameter("@Period", dto.Period ?? "monthly")).ToListAsync(); + + return allItems ?? new List(); + } + + public async Task> GetCatchUpCallReport(ReportsDto dto) + { + var (startDate, endDate) = await GetCurrentWholeMonth(); + + var allItems = await _context.CatchUpCallReports + .FromSqlRaw( + "EXEC GetCatchUpCallReport @UserId,@StartDate,@EndDate,@Period", + new SqlParameter("@UserId", dto.UserId), + new SqlParameter("@StartDate", dto.StartDate ?? startDate), + new SqlParameter("@EndDate", dto.EndDate ?? endDate), + new SqlParameter("@Period", dto.Period ?? "monthly")).ToListAsync(); + + return allItems ?? new List(); + } + public async Task> GetActualCoverageReport(ReportsDto dto) + { + var (startDate, endDate) = await GetCurrentWholeMonth(); + + var allItems = await _context.ActualCoverageReports + .FromSqlRaw( + "EXEC GetActualCoverageReport @UserId,@StartDate,@EndDate,@Period", + new SqlParameter("@UserId", dto.UserId), + new SqlParameter("@StartDate", dto.StartDate ?? startDate), + new SqlParameter("@EndDate", dto.EndDate ?? endDate), + new SqlParameter("@Period", dto.Period ?? "monthly")).ToListAsync(); + + return allItems ?? new List(); + } + public async Task PutMissedCall(ReportsDto dto) + { + try + { + var catchUp = await _context.CatchUpCalls + .SingleOrDefaultAsync(c => c.MRRawPlanId == dto.MRRawPlanId); + + if (catchUp != null) + { + catchUp.MissedReason = dto.MissedReason; + } + + var plan = await _context.MRRawPlans + .SingleOrDefaultAsync(c => c.MRRawPlanId == dto.MRRawPlanId); + + if (plan != null) + { + plan.MissedReason = dto.MissedReason; + } + + await _context.SaveChangesAsync(); + + return plan?.MissedReason != null; + } + catch (Exception ex) + { + ex.ToString(); + throw; + } + + } + } +} diff --git a/LSFE.Domain/Services/Security/RoleAuthorizationCache.cs b/LSFE.Domain/Services/Security/RoleAuthorizationCache.cs new file mode 100644 index 0000000..0fca81b --- /dev/null +++ b/LSFE.Domain/Services/Security/RoleAuthorizationCache.cs @@ -0,0 +1,177 @@ +using LSFE.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; + +namespace LSFE.Domain.Services.Security +{ + public interface IRoleAuthorizationCache + { + Task> GetAllowedRoleIdsAsync(string controller); + Task UserHasAccessAsync(string userId, string controller); + Task> GetUserRoleIdsAsync(string userId); + void InvalidateCache(string controller); + void InvalidateUserCache(string userId); + void InvalidateAllCache(); + } + + public class RoleAuthorizationCache : IRoleAuthorizationCache + { + private readonly IMemoryCache _cache; + private readonly LSFEDbContext _dbContext; + private readonly ILogger _logger; + + private const string CONTROLLER_ROLES_PREFIX = "controller_roles_"; + private const string USER_ROLES_PREFIX = "user_roles_"; + private const int CACHE_DURATION_MINUTES = 30; + + public RoleAuthorizationCache( + IMemoryCache cache, + LSFEDbContext dbContext, + ILogger logger) + { + _cache = cache ?? throw new ArgumentNullException(nameof(cache)); + _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// Get all role IDs that have access to a specific controller (CACHED) + /// + public async Task> GetAllowedRoleIdsAsync(string controller) + { + if (string.IsNullOrWhiteSpace(controller)) + throw new ArgumentException("Controller name cannot be null or empty", nameof(controller)); + + var cacheKey = $"{CONTROLLER_ROLES_PREFIX}{controller}"; + + return await _cache.GetOrCreateAsync(cacheKey, async entry => + { + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(CACHE_DURATION_MINUTES); + entry.SetPriority(CacheItemPriority.High); + + _logger.LogInformation("Cache MISS: Loading role permissions for controller: {Controller}", controller); + + var roleIds = await _dbContext.AuthorizeRoles + .Where(ar => ar.IsActive && ar.Controller == controller) + .Select(ar => ar.RoleId) + .Distinct() + .ToListAsync(); + + _logger.LogInformation("Cached {Count} roles for controller: {Controller}", roleIds.Count, controller); + + return roleIds ?? new List(); + }); + } + + /// + /// Get all role IDs for a specific user (CACHED) + /// + public async Task> GetUserRoleIdsAsync(string userId) + { + if (string.IsNullOrWhiteSpace(userId)) + throw new ArgumentException("User ID cannot be null or empty", nameof(userId)); + + var cacheKey = $"{USER_ROLES_PREFIX}{userId}"; + + return await _cache.GetOrCreateAsync(cacheKey, async entry => + { + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(CACHE_DURATION_MINUTES); + entry.SetPriority(CacheItemPriority.High); + + _logger.LogInformation("Cache MISS: Loading roles for user: {UserId}", userId); + + var userRoleIds = await _dbContext.UserRoles + .Where(ur => ur.UserId == userId) + .Select(ur => ur.RoleId) + .ToListAsync(); + + _logger.LogInformation("Cached {Count} roles for user: {UserId}", userRoleIds.Count, userId); + + return userRoleIds ?? new List(); + }); + } + + /// + /// Check if a specific user has access to a controller (FULLY CACHED) + /// + public async Task UserHasAccessAsync(string userId, string controller) + { + if (string.IsNullOrWhiteSpace(userId)) + throw new ArgumentException("User ID cannot be null or empty", nameof(userId)); + + if (string.IsNullOrWhiteSpace(controller)) + throw new ArgumentException("Controller name cannot be null or empty", nameof(controller)); + + try + { + // Get allowed role IDs from cache (or database if cache miss) + var allowedRoleIds = await GetAllowedRoleIdsAsync(controller); + + if (!allowedRoleIds.Any()) + { + _logger.LogWarning("No roles configured for controller: {Controller}", controller); + return false; + } + + // Get user's role IDs from cache (or database if cache miss) + var userRoleIds = await GetUserRoleIdsAsync(userId); + + if (!userRoleIds.Any()) + { + _logger.LogWarning("User {UserId} has no roles assigned", userId); + return false; + } + + // Check if user has any of the required roles (IN-MEMORY operation, no database!) + var hasAccess = userRoleIds.Any(userRole => allowedRoleIds.Contains(userRole)); + + _logger.LogDebug("User {UserId} access to {Controller}: {HasAccess}", userId, controller, hasAccess); + + return hasAccess; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error checking access for user {UserId} on controller {Controller}", + userId, controller); + throw; + } + } + + /// + /// Invalidate cache for a specific controller + /// + public void InvalidateCache(string controller) + { + if (string.IsNullOrWhiteSpace(controller)) + return; + + var cacheKey = $"{CONTROLLER_ROLES_PREFIX}{controller}"; + _cache.Remove(cacheKey); + + _logger.LogInformation("Cache invalidated for controller: {Controller}", controller); + } + + /// + /// Invalidate cache for a specific user (call this when user roles change) + /// + public void InvalidateUserCache(string userId) + { + if (string.IsNullOrWhiteSpace(userId)) + return; + + var cacheKey = $"{USER_ROLES_PREFIX}{userId}"; + _cache.Remove(cacheKey); + + _logger.LogInformation("Cache invalidated for user: {UserId}", userId); + } + + /// + /// Invalidate all controller role caches + /// + public void InvalidateAllCache() + { + _logger.LogWarning("All cache invalidation requested - consider implementing a cache key tracking system"); + } + } +} \ No newline at end of file diff --git a/LSFE.Domain/Services/TimeKeeping/AttendanceRepo.cs b/LSFE.Domain/Services/TimeKeeping/AttendanceRepo.cs new file mode 100644 index 0000000..d6197c0 --- /dev/null +++ b/LSFE.Domain/Services/TimeKeeping/AttendanceRepo.cs @@ -0,0 +1,108 @@ +using LSFE.Domain.Contracts.Planning; +using LSFE.Domain.Contracts.TimeKeeping; +using LSFE.Infrastructure.Database; +using LSFE.Infrastructure.Dto; +using LSFE.Infrastructure.Dto.TimeKeeping; +using LSFE.Infrastructure.Entities.Planning; +using LSFE.Infrastructure.Entities.TimeKeeping; +using LSFE.Infrastructure.Model; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Domain.Services.TimeKeeping +{ + public class AttendanceRepo : GenericRepository, IAttendanceRepo + { + private readonly LSFEDbContext _context; + public AttendanceRepo(LSFEDbContext context) : base(context) + { + _context = context; + } + public async Task> + GetAttendance(Infrastructure.Dto.TimeKeeping.AttendanceDto dto) + { + var now = DateTime.UtcNow; + var startDate = new DateTime(now.Year,now.Month,1,0, 0, 0,DateTimeKind.Utc); + var endDate = startDate.AddMonths(1); + + + var attendance = await _context.AttendanceDto + .FromSqlRaw( + "EXEC GetAttendance @UserId,@StartDate,@EndDate,@Period", + new SqlParameter("@UserId", dto.UserId), + new SqlParameter("@StartDate", dto.StartDate ?? startDate), + new SqlParameter("@EndDate", dto.EndDate ?? endDate), + new SqlParameter("@Period", dto.Period ?? "monthly")).ToListAsync(); + + return attendance ?? new List(); + } + + public async Task> GetAttendanceToday(CredentialDto dto) + { + var attendance = await _context.Attendances + .FromSqlRaw("EXEC GetAttendanceToday @UserId", + new SqlParameter("@UserId", dto.UserId)) + .ToListAsync(); + + return attendance ?? new List(); + } + + public async Task PostPutAttendance(Infrastructure.Dto.TimeKeeping.AttendanceDto dto) + { + try + { + // Query by AppAttendanceId (the mobile app's ID), not AttendanceId (the DB auto-increment) + var logs = await _context.Attendances + .FirstOrDefaultAsync(a => a.UserId == dto.UserId && + a.AppAttendanceId == dto.AppAttendanceId); + + if (logs == null) + { + // Create new record - don't set AttendanceId, let the DB auto-increment it + var attendance = new Attendance + { + AppAttendanceId = dto.AppAttendanceId, + TimeIn = dto.TimeIn, + UserId = dto.UserId, + TimeOut = dto.TimeOut, + TimeInLocation = dto.TimeInLocation, + TimeOutLocation = dto.TimeOutLocation, + SignatureFileNameIn = dto.SignatureFileNameIn, + }; + await _context.AddAsync(attendance); + await _context.SaveChangesAsync(); // MUST SAVE! + } + else + { + // Update existing record + logs.TimeIn = dto.TimeIn; + logs.TimeOut = dto.TimeOut; + logs.TimeInLocation = dto.TimeInLocation; + logs.TimeOutLocation = dto.TimeOutLocation; + logs.SignatureFileNameOut = dto.SignatureFileNameOut; + await _context.SaveChangesAsync(); + } + + return new Response + { + Message = "Sync Successfully", + MessCode = 1, + Success = true + }; + } + catch (Exception ex) + { + // Log the actual error + Debug.WriteLine($"PostPutAttendance Error: {ex.ToString()}"); + throw; // Re-throw so the controller can log it + } + } + } +} diff --git a/LSFE.Domain/Services/UnitOfWork.cs b/LSFE.Domain/Services/UnitOfWork.cs new file mode 100644 index 0000000..9450e1f --- /dev/null +++ b/LSFE.Domain/Services/UnitOfWork.cs @@ -0,0 +1,86 @@ +using LSFE.Domain.Contracts; +using LSFE.Domain.Contracts.Doctor; +using LSFE.Domain.Contracts.Maintenance; +using LSFE.Domain.Contracts.Planning; +using LSFE.Domain.Contracts.Reports; +using LSFE.Domain.Contracts.TimeKeeping; +using LSFE.Domain.Services.Doctor; +using LSFE.Domain.Services.Maintenance; +using LSFE.Domain.Services.Planning; +using LSFE.Domain.Services.Reports; +using LSFE.Domain.Services.TimeKeeping; +using LSFE.Infrastructure.Database; +using LSFE.Infrastructure.Entities.Admin; +using LSFE.Infrastructure.Entities.Doctor; +using LSFE.Infrastructure.Entities.Maintenance; +using LSFE.Infrastructure.Entities.Planning; +using LSFE.Infrastructure.Entities.TimeKeeping; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Domain.Services +{ + public class UnitOfWork : IUnitOfWork + { + private readonly LSFEDbContext _context; + public IErrorLogger ErrorLogger { get; } + public IPlanningRepo Planning { get; } + public IDoctorRepo Doctors { get; } + public IAttendanceRepo Attendances {get; } + public IMaintenanceRepo Areas { get; } + public IMaintenanceRepo Districts { get; } + public IMaintenanceRepo Divisions { get; } + public IMaintenanceRepo Territories { get; } + public IMaintenanceRepo Institutions { get; } + public IMaintenanceRepo InstitutionCategories { get; } + public IMaintenanceRepo Products { get; } + public IMaintenanceRepo Specializations { get; } + public IMaintenanceRepo Inventory { get; } + public IMaintenanceRepo InventoryTransactions { get; } + public IMaintenanceRepo ProductTransactions { get; } + public IMaintenanceRepo MRRawPlans { get; } + public IMaintenanceRepo CatchUpCalls { get; } + public IMaintenanceRepo MRPlanDetails { get; } + public IMaintenanceRepo MRPlanHeaders { get; } + public IMaintenanceRepo UserDevices { get; } + public IReportsRepo Reports { get; } + public UnitOfWork(LSFEDbContext context) + { + _context = context; + ErrorLogger = new ErrorLogger(_context); + Doctors = new DoctorRepo(_context); + Planning = new PlanningRepo(_context); + Reports = new ReportsRepo(_context); + Attendances = new AttendanceRepo(_context); + Areas = new MaintenanceRepo(_context); + Districts = new MaintenanceRepo(_context); + Divisions = new MaintenanceRepo(_context); + Territories = new MaintenanceRepo(_context); + Institutions = new MaintenanceRepo(_context); + InstitutionCategories = new MaintenanceRepo(_context); + Products = new MaintenanceRepo(_context); + Specializations = new MaintenanceRepo(_context); + Inventory = new MaintenanceRepo(_context); + InventoryTransactions = new MaintenanceRepo(_context); + ProductTransactions = new MaintenanceRepo(_context); + MRRawPlans= new MaintenanceRepo(_context); + CatchUpCalls = new MaintenanceRepo(_context); + MRPlanDetails = new MaintenanceRepo(_context); + MRPlanHeaders = new MaintenanceRepo(_context); + UserDevices = new MaintenanceRepo(_context); + + } + + public async Task SaveChangesAsync() + { + return await _context.SaveChangesAsync(); + } + public void Dispose() + { + _context.Dispose(); + } + } +} \ No newline at end of file diff --git a/LSFE.Infrastructure/Converter/GuidListConverter.cs b/LSFE.Infrastructure/Converter/GuidListConverter.cs new file mode 100644 index 0000000..8602617 --- /dev/null +++ b/LSFE.Infrastructure/Converter/GuidListConverter.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Converter +{ + public class GuidListConverter : JsonConverter> + { + public override List Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var list = new List(); + + if (reader.TokenType != JsonTokenType.StartArray) + throw new JsonException(); + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + return list; + + if (reader.TokenType == JsonTokenType.String) + { + var str = reader.GetString(); + if (Guid.TryParse(str, out var guid)) + list.Add(guid); + } + else if (reader.TokenType == JsonTokenType.PropertyName) + { + // Handle object format if needed + } + } + + return list; + } + + public override void Write(Utf8JsonWriter writer, List value, JsonSerializerOptions options) + { + writer.WriteStartArray(); + foreach (var guid in value) + writer.WriteStringValue(guid.ToString()); + writer.WriteEndArray(); + } + } +} diff --git a/LSFE.Infrastructure/Database/LSFEDbContext.cs b/LSFE.Infrastructure/Database/LSFEDbContext.cs new file mode 100644 index 0000000..e4aa3f8 --- /dev/null +++ b/LSFE.Infrastructure/Database/LSFEDbContext.cs @@ -0,0 +1,154 @@ +using LSFE.Infrastructure.Dto.Planning; +using LSFE.Infrastructure.Entities; +using LSFE.Infrastructure.Entities.Admin; +using LSFE.Infrastructure.Entities.Auth; +using LSFE.Infrastructure.Entities.Doctor; +using LSFE.Infrastructure.Entities.Maintenance; +using LSFE.Infrastructure.Entities.Planning; +using LSFE.Infrastructure.Entities.Reports; +using LSFE.Infrastructure.Entities.TimeKeeping; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Database +{ + public class LSFEDbContext : IdentityDbContext + { + public LSFEDbContext(DbContextOptions options) : base(options) { } + public DbSet Doctors { get; set; } + public DbSet ForDoctorApprovals { get; set; } + public DbSet Departments { get; set; } + public DbSet Attachments { get; set; } + public DbSet Areas { get; set; } + public DbSet Districts { get; set; } + public DbSet Institutions { get; set; } + public DbSet Specializations { get; set; } + public DbSet InstitutionCategories { get; set; } + public DbSet Products { get; set; } + public DbSet ProductTransactions { get; set; } + public DbSet InventoryTransactions { get; set; } + public DbSet Territories { get; set; } + public DbSet Divisions { get; set; } + public DbSet MRRawPlans { get; set; } + public DbSet MRRawPlansDto { get; set; } + public DbSet CatchUpCalls { get; set; } + public DbSet Inventory { get; set; } + public DbSet CatchUpCallReports { get; set; } + public DbSet CallRateReports { get; set; } + public DbSet CallReachReports { get; set; } + public DbSet ActualCoverageReports { get; set; } + public virtual DbSet DoctorWithPlan { get; set; } + public DbSet DoctorCoverageTodays { get; set; } + public virtual DbSet PendingApprovals { get; set; } + public virtual DbSet DoctorByIds { get; set; } + public DbSet MRPlanDetails { get; set; } + public DbSet MRPlanHeaders { get; set; } + public DbSet Attendances { get; set; } + public virtual DbSet AttendanceDto { get; set; } + public DbSet ErrorLogs { get; set; } + public DbSet UserDevices { get; set; } + public DbSet AuthorizeRoles { get; internal set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(entity => + { + entity.ToTable("ActualCoverageReport"); + entity.HasNoKey(); + }); + modelBuilder.Entity() + .Property(d => d.AreaId) + .ValueGeneratedOnAdd(); + + modelBuilder.Entity() + .Property(d => d.DivisionId) + .ValueGeneratedOnAdd(); + + modelBuilder.Entity() + .HasOne(d => d.Institution) + .WithMany(i => i.Doctors) + .HasForeignKey(d => d.InstitutionId); + + modelBuilder.Entity() + .HasOne(d => d.Districts) + .WithMany(i => i.Doctors) + .HasForeignKey(d => d.DistrictId); + + modelBuilder.Entity(b => + { + b.ToTable("Districts"); + b.HasOne(u => u.Areas) + .WithMany() + .HasForeignKey(u => u.AreaId) + .IsRequired(false); + }); + + modelBuilder.Entity(b => + { + b.ToTable("Institutions"); + b.HasOne(u => u.Territories) + .WithMany() + .HasForeignKey(u => u.TerritoryId) + .IsRequired(false); + }); + + modelBuilder.Entity(b => + { + b.ToTable("Territories"); + b.HasOne(u => u.Districts) + .WithMany() + .HasForeignKey(u => u.DistrictId) + .IsRequired(false); + }); + + modelBuilder.Entity(b => + { + b.ToTable("Users"); + b.HasOne(u => u.Attachment) + .WithMany() + .HasForeignKey(u => u.AttachmentId) + .IsRequired(false); + + b.HasOne(u => u.Department) + .WithMany() + .HasForeignKey(u => u.DepartmentId) + .IsRequired(false); + }); + modelBuilder.Entity(b => + { + b.ToTable("Attachment"); + + b.Property(d => d.AttachmentId) + .ValueGeneratedOnAdd(); + }); + modelBuilder.Entity(b => + { + b.ToTable("Departments"); + }); + modelBuilder.Entity>(b => + { + b.ToTable("UserClaims"); + }); + modelBuilder.Entity>(b => + { + b.ToTable("UserTokens"); + }); + modelBuilder.Entity(b => + { + b.ToTable("Roles"); + }); + modelBuilder.Entity>(b => + { + b.ToTable("UserRoles"); + }); + } + } +} \ No newline at end of file diff --git a/LSFE.Infrastructure/Dto/Account/NavigationItemDto.cs b/LSFE.Infrastructure/Dto/Account/NavigationItemDto.cs new file mode 100644 index 0000000..a0ae214 --- /dev/null +++ b/LSFE.Infrastructure/Dto/Account/NavigationItemDto.cs @@ -0,0 +1,29 @@ +using System.Security; + +namespace LSFE.Infrastructure.Dto.Account +{ + public class NavigationItemDto + { + public required string Id { get; set; } + public required string Label { get; set; } + public required string Icon { get; set; } + public string? Badge { get; set; } + public required string Route { get; set; } + public PermissionsDto? Permissions { get; set; } + public List? Children { get; set; } + public int? ParentModuleId { get; set; } + } + public class PermissionsDto + { + public bool CanView { get; set; } + public bool CanCreate { get; set; } + public bool CanEdit { get; set; } + public bool CanDelete { get; set; } + } + public class NavigationResponseDto + { + public bool Success { get; set; } + public List? Data { get; set; } + public string? Message { get; set; } + } +} diff --git a/LSFE.Infrastructure/Dto/Account/UserClaimsDto.cs b/LSFE.Infrastructure/Dto/Account/UserClaimsDto.cs new file mode 100644 index 0000000..c808d81 --- /dev/null +++ b/LSFE.Infrastructure/Dto/Account/UserClaimsDto.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Dto.Account +{ + public class UserClaimsDto + { + public string UserId { get; init; } = default!; + public string UserName { get; init; } = default!; + public string FullName { get; init; } = default!; + public string Company { get; init; } = default!; + public IReadOnlyList Roles { get; init; } = []; + } +} diff --git a/LSFE.Infrastructure/Dto/Admin/DeviceInfo.cs b/LSFE.Infrastructure/Dto/Admin/DeviceInfo.cs new file mode 100644 index 0000000..477a52d --- /dev/null +++ b/LSFE.Infrastructure/Dto/Admin/DeviceInfo.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Dto.Admin +{ + public class DeviceInfoDto + { + public string? DeviceId { get; set; } + public string? DeviceModel { get; set; } + public string? Platform { get; set; } + public string? Version { get; set; } + public string? Manufacturer { get; set; } + public string? Name { get; set; } + public string? DeviceType { get; set; } + public string? VersionString { get; set; } + public string? UserName { get; set; } + } +} diff --git a/LSFE.Infrastructure/Dto/ApprovePlanDto.cs b/LSFE.Infrastructure/Dto/ApprovePlanDto.cs new file mode 100644 index 0000000..f9a9371 --- /dev/null +++ b/LSFE.Infrastructure/Dto/ApprovePlanDto.cs @@ -0,0 +1,19 @@ +using LSFE.Infrastructure.Converter; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Dto +{ + public class ApprovePlanDto + { + [JsonConverter(typeof(GuidListConverter))] + public List AppointmentIds { get; set; } = new(); + public string? UserId { get; set; } + public string? MRPlanDate { get; set; } + public string? UpdatedBy { get; set; } + } +} diff --git a/LSFE.Infrastructure/Dto/CredentialDto.cs b/LSFE.Infrastructure/Dto/CredentialDto.cs new file mode 100644 index 0000000..37a4e48 --- /dev/null +++ b/LSFE.Infrastructure/Dto/CredentialDto.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Dto +{ + public class CredentialDto + { + public string? UserRole { get; set; } + public string? UserId { get; set; } + public bool IsMissed { get; set; } + public bool IsReschedule { get; set; } + } +} diff --git a/LSFE.Infrastructure/Dto/Doctor/CoverageDto.cs b/LSFE.Infrastructure/Dto/Doctor/CoverageDto.cs new file mode 100644 index 0000000..2a10daa --- /dev/null +++ b/LSFE.Infrastructure/Dto/Doctor/CoverageDto.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Dto.Doctor +{ + public class CoverageDto + { + public string? UpdatedBy { get; set; } + public string? UserId { get; set; } + } +} diff --git a/LSFE.Infrastructure/Dto/Doctor/DoctorDto.cs b/LSFE.Infrastructure/Dto/Doctor/DoctorDto.cs new file mode 100644 index 0000000..63eadbd --- /dev/null +++ b/LSFE.Infrastructure/Dto/Doctor/DoctorDto.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Dto.Doctor +{ + public class DoctorDto + { + public int DoctorId { get; set; } = 0; + public string? MiddleInitial { get; set; } + public string? LastName { get; set; } + public string? FirstName { get; set; } + public string? Company { get; set; } + public byte MaxVisit { get; set; } + public string? UserId { get; set; } + public byte StageLevelId { get; set; } + public DateTime? BirthDate { get; set; } + public string? EmailAddress { get; set; } + public int InstitutionId { get; set; } + public byte Status { get; set; } + public bool IsActive { get; set; } + public string? PhoneNo { get; set; } = "N/A"; + public string? MedRepName { get; set; } + public byte SpecializationId { get; set; } + public string? SpecializationName { get; set; } + public string? LicenseNo { get; set; } + public string? Address { get; set; } + public string? InstitutionName { get; set; } + public int? DistrictId { get; set; } + public string? DistrictName { get; set; } + public string? MRPlanDate { get; set; } + public bool IsTerritorial { get; set; } + public bool IsPlanning { get; set; } + public bool IsCatchUpCall { get; set; }=false; + public string? UserName { get; set; } + public string? UpdatedBy { get; set; } + } +} diff --git a/LSFE.Infrastructure/Dto/Doctor/ForDoctorApprovalDto.cs b/LSFE.Infrastructure/Dto/Doctor/ForDoctorApprovalDto.cs new file mode 100644 index 0000000..63d7008 --- /dev/null +++ b/LSFE.Infrastructure/Dto/Doctor/ForDoctorApprovalDto.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Dto.Doctor +{ + public class ForDoctorApprovalDto + { + public Guid ForDoctorApprovalId { get; set; } + public int DoctorId { get; set; } + public string? FirstName { get; set; } + public string? MiddleInitial { get; set; } + public string? LastName { get; set; } + public string? EmailAddress { get; set; } + public string? PhoneNo { get; set; } + public byte SpecializationId { get; set; } + public string? LicenseNo { get; set; } + public string? Address { get; set; } + public DateTime? BirthDate { get; set; } + public byte MaxVisit { get; set; } + public byte Status { get; set; } + public int InstitutionId { get; set; } + public string? InstitutionName { get; set; } + public int? DistrictId { get; set; } + public string? DistrictName { get; set; } + public string? UserId { get; set; } + public byte StageLevelId { get; set; } + public bool IsTerritorial { get; set; } + public string? MedRepName { get; set; } + } +} diff --git a/LSFE.Infrastructure/Dto/Maintenance/InventoryDto.cs b/LSFE.Infrastructure/Dto/Maintenance/InventoryDto.cs new file mode 100644 index 0000000..9da14d7 --- /dev/null +++ b/LSFE.Infrastructure/Dto/Maintenance/InventoryDto.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Dto.Maintenance +{ + public class InventoryDto + { + public Guid InventoryId { get; set; } + public int DoctorId { get; set; } + public string? UserId { get; set; } + public string? UpdatedBy { get; set; } + public string? UserRole { get; set; } + public int QtyOut { get; set; } + public int QtyBalance { get; set; } + public byte CycleMonth { get; set; } + public int CycleYear { get; set; } + public DateTime UpdatedDate { get; set; } + } +} diff --git a/LSFE.Infrastructure/Dto/Maintenance/ProductDto.cs b/LSFE.Infrastructure/Dto/Maintenance/ProductDto.cs new file mode 100644 index 0000000..58c3c04 --- /dev/null +++ b/LSFE.Infrastructure/Dto/Maintenance/ProductDto.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Dto.Maintenance +{ + public class ProductDto + { + public Guid Id { get; set; } + public string? UserId { get; set; } + public string? UpdatedBy { get; set; } + public string? UserRole { get; set; } + } +} diff --git a/LSFE.Infrastructure/Dto/Maintenance/ProductTransactionDto.cs b/LSFE.Infrastructure/Dto/Maintenance/ProductTransactionDto.cs new file mode 100644 index 0000000..2b077cc --- /dev/null +++ b/LSFE.Infrastructure/Dto/Maintenance/ProductTransactionDto.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Dto.Maintenance +{ + public class ProductTransactionDto + { + public Guid ProductTransId { get; set; } + public int ProductId { get; set; } + public string? ProductName { get; set; } + public int DoctorId { get; set; } + public Guid MRRawPlanId { get; set; } + public DateOnly? AppointmentDate { get; set; } + public DateTime CreatedDate { get; set; } + public string? CreatedBy { get; set; } + public string? UserId { get; set; } + public bool IsActive { get; set; } + public string? ProductLink { get; set; } + } +} diff --git a/LSFE.Infrastructure/Dto/Planning/MRRawPlansDto.cs b/LSFE.Infrastructure/Dto/Planning/MRRawPlansDto.cs new file mode 100644 index 0000000..de882b5 --- /dev/null +++ b/LSFE.Infrastructure/Dto/Planning/MRRawPlansDto.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Dto.Planning +{ + [Keyless] + public class MRRawPlansDto + { + public Guid MRRawPlanId { get; set; } + public Guid? MRPlanDetailId { get; set; } + public int DoctorId { get; set; } + public byte? VisitCount { get; set; } + public DateOnly? CallDate { get; set; } + public DateOnly? ActualCallDate { get; set; } + public DateTime? StartTime { get; set; } + public DateTime? EndTime { get; set; } + public string? Longitude { get; set; } + public string? Latitude { get; set; } + public string? Location { get; set; } + public bool IsSignature { get; set; } + public string? SignatureFileName { get; set; } + public bool IsMissed { get; set; } + public string? MissedReason { get; set; } + public bool IsDoctorProxy { get; set; } + public bool IsReschedule { get; set; } + public string? RescheduleReason { get; set; } + public string? CallType { get; set; } + public bool IsJoinCall { get; set; } + public byte? CycleId { get; set; } + public bool IsApproved { get; set; } + public bool IsRecovered { get; set; } + public string? SLP { get; set; } + public string? ApprovedBy { get; set; } + public DateTime? ApprovedDate { get; set; } + public string? UserId { get; set; } + public byte Status { get; set; } + public bool IsCatchUpCall { get; set; } = false; + } +} diff --git a/LSFE.Infrastructure/Dto/Planning/PlanByUserIdRequest.cs b/LSFE.Infrastructure/Dto/Planning/PlanByUserIdRequest.cs new file mode 100644 index 0000000..8c6d4ec --- /dev/null +++ b/LSFE.Infrastructure/Dto/Planning/PlanByUserIdRequest.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Dto.Planning +{ + public class PlanByUserIdRequest + { + public string? UserId { get; set; } + + } +} diff --git a/LSFE.Infrastructure/Dto/Planning/Request/PlanForApprovalRequest.cs b/LSFE.Infrastructure/Dto/Planning/Request/PlanForApprovalRequest.cs new file mode 100644 index 0000000..0c9943b --- /dev/null +++ b/LSFE.Infrastructure/Dto/Planning/Request/PlanForApprovalRequest.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Dto.Planning.Request +{ + public class PlanForApprovalRequest + { + } +} diff --git a/LSFE.Infrastructure/Dto/PlanningDto.cs b/LSFE.Infrastructure/Dto/PlanningDto.cs new file mode 100644 index 0000000..ca626bf --- /dev/null +++ b/LSFE.Infrastructure/Dto/PlanningDto.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Dto +{ + public class PlanningDto + { + public class SelectedSampleDto + { + public Guid InventoryId { get; set; } + public string? Description { get; set; } + public int MaxQty { get; set; } + public int OriginalQty { get; set; } + public int QtyBalance { get; set; } + public int SelectedQty { get; set; } + } + public class SelectedPromoDto + { + public int ProductId { get; set; } + public string? ProductName { get; set; } + } + + public List SelectedSamples { get; set; } = new(); + public List SelectedPromos { get; set; } = new(); + public string? MRPlanDetailId { get; set; }=null; + public int DoctorId { get; set; } + [DataType(DataType.Date)] + public DateOnly PlanDate { get; set; } + public int DivisionId { get; set; } + public bool IsActive { get; set; } + public string? MRRawPlanId { get; set; } = null; + public int ProductId { get; set; } + [DataType(DataType.Date)] + public DateOnly CallDate { get; set; } + public string? RescheduleReason { get; set; } + public string? CallType { get; set; } + public byte CycleId { get; set; } + public bool IsApproved { get; set; } = false; + public bool IsRecovered { get; set; } = false; + public bool IsLiterature { get; set; } + public string? SLP { get; set; } + public byte IsUpdateDelete { get; set; } + public string? MRPlanDate { get; set; } + public string? UpdatedBy { get; set; } + public string? UserId { get; set; } + public byte CycleMonth { get; set; } + public int CycleYear { get; set; } + public DateOnly MonthPlanCopy { get; set; } + public DateOnly NewMonthPlan { get; set; } + } +} diff --git a/LSFE.Infrastructure/Dto/RegisterUserDto.cs b/LSFE.Infrastructure/Dto/RegisterUserDto.cs new file mode 100644 index 0000000..c3d953b --- /dev/null +++ b/LSFE.Infrastructure/Dto/RegisterUserDto.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Dto +{ + public class RegisterUserDto + { + public string? UserName { get; set; } + public string? Email { get; set; } + public string? Password { get; set; } + public string? FullName { get; set; } + public string? Company { get; set; } + public int? DepartmentId { get; set; } + public string? Address { get; set; } + public int? AttachmentId { get; set; } = 1; + public string? EmployeeId { get; set; } + public string? RoleId { get; set; } + public string? PhoneNumber { get; set; } + public string? RoleName { get; set; } + public string? UpdatedBy { get; set; } + } +} diff --git a/LSFE.Infrastructure/Dto/Reports/ReportsDto.cs b/LSFE.Infrastructure/Dto/Reports/ReportsDto.cs new file mode 100644 index 0000000..6ef99d9 --- /dev/null +++ b/LSFE.Infrastructure/Dto/Reports/ReportsDto.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Dto.Reports +{ + public class ReportsDto + { + public required string UserId { get; set; } + public string? MedRepName { get; set; } + public string? Company { get; set; } + public DateOnly CallMonth { get; set; } + public Guid MRRawPlanId { get; set; } + public string? MissedReason { get; set; } + public string? Period { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + } +} diff --git a/LSFE.Infrastructure/Dto/RoleDto.cs b/LSFE.Infrastructure/Dto/RoleDto.cs new file mode 100644 index 0000000..4db64d9 --- /dev/null +++ b/LSFE.Infrastructure/Dto/RoleDto.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Dto +{ + public class RoleDto + { + public string? RoleId { get; set; } + public string? RoleName { get; set; } + } +} diff --git a/LSFE.Infrastructure/Dto/TimeKeeping/AttendanceDto.cs b/LSFE.Infrastructure/Dto/TimeKeeping/AttendanceDto.cs new file mode 100644 index 0000000..ed89f52 --- /dev/null +++ b/LSFE.Infrastructure/Dto/TimeKeeping/AttendanceDto.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Dto.TimeKeeping +{ + public class AttendanceDto + { + public int AttendanceId { get; set; } + public string? AppAttendanceId { get; set; } + public DateTime? TimeIn { get; set; } + public DateTime? TimeOut { get; set; } + public string? UserId { get; set; } + public string? TimeInLocation { get; set; } + public string? TimeOutLocation { get; set; } + public string? SignatureFileNameIn { get; set; } + public string? SignatureFileNameOut { get; set; } + public string? SignatureBase64 { get; set; } + public string? MedRepName { get; set; } + public DateTime? EndDate { get; set; } + public DateTime? StartDate { get; set; } + public string? Period { get; set; } + } +} diff --git a/LSFE.Infrastructure/Dto/UpdateUserDto.cs b/LSFE.Infrastructure/Dto/UpdateUserDto.cs new file mode 100644 index 0000000..d355743 --- /dev/null +++ b/LSFE.Infrastructure/Dto/UpdateUserDto.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Dto +{ + public class UpdateUserDto + { + public string? Id { get; set; } + public string? UserName { get; set; } + public string? Email { get; set; } + public string? Password { get; set; } + public string? FullName { get; set; } + public string? RoleId { get; set; } + public string? RoleName { get; set; } + public string? Company { get; set; } + public int? DepartmentId { get; set; } + public string? Address { get; set; } + public string? EmployeeId { get; set; } + public string? UpdatedBy { get; set; } + public string? PhoneNumber { get; set; } + } +} diff --git a/LSFE.Infrastructure/Dto/UserWithRoleDto.cs b/LSFE.Infrastructure/Dto/UserWithRoleDto.cs new file mode 100644 index 0000000..48e8632 --- /dev/null +++ b/LSFE.Infrastructure/Dto/UserWithRoleDto.cs @@ -0,0 +1,18 @@ +using LSFE.Infrastructure.Entities; +using Microsoft.AspNetCore.Identity; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Dto +{ + public class UserWithRoleDto : AppsUsers + { + public string? RoleName { get; set; } + public Departments? Departments { get; set; } + public List? UserRoles { get; set; } + public string? RoleId { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Admin/UserDevice.cs b/LSFE.Infrastructure/Entities/Admin/UserDevice.cs new file mode 100644 index 0000000..db4a6bf --- /dev/null +++ b/LSFE.Infrastructure/Entities/Admin/UserDevice.cs @@ -0,0 +1,35 @@ +using LSFE.Infrastructure.Model; +using LSFE.Infrastructure.Model.Admin; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Admin +{ + [Table("UserDevices")] + public class UserDevice + { + [Key] + public int UserDeviceId { get; set; } + public string? UserId { get; set; } + public string? DeviceId { get; set; } + public string? DeviceModel { get; set; } + public string? DevicePlatform { get; set; } + public string? DeviceManufacturer { get; set; } + public string? DeviceVersion { get; set; } + public string? AppVersion { get; set; } + public bool IsAuthorized { get; set; } + public bool IsActive { get; set; } + public DateTime? DeactivatedDate { get; set; } + public string? DeactivatedBy { get; set; } + public string? DeactivationReason { get; set; } + public DateTime RegisteredDate { get; set; } + public string? RegisteredBy { get; set; } + //public DateTime LastLoginDate { get; set; } + public string? Notes { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/AppsUsers.cs b/LSFE.Infrastructure/Entities/AppsUsers.cs new file mode 100644 index 0000000..b5e0004 --- /dev/null +++ b/LSFE.Infrastructure/Entities/AppsUsers.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Identity; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Net.Mail; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities +{ + public class AppsUsers : IdentityUser + { + public string? FullName { get; set; } + public string? Company { get; set; } + public int? DepartmentId { get; set; } + + [ForeignKey("DepartmentId")] + public Departments? Department { get; set; } + + public DateTime CreatedDate { get; set; } + public DateTime UpdatedDate { get; set; } + public string? CreatedBy { get; set; } + public string? UpdatedBy { get; set; } + public string? Address { get; set; } + public int? AttachmentId { get; set; } + + [ForeignKey("AttachmentId")] + public Attachment? Attachment { get; set; } + public string? EmployeeId { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Attachment.cs b/LSFE.Infrastructure/Entities/Attachment.cs new file mode 100644 index 0000000..66aa2f8 --- /dev/null +++ b/LSFE.Infrastructure/Entities/Attachment.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities +{ + [Table("Attachments")] + public class Attachment + { + [Key] + public int AttachmentId { get; set; } + public string? URL { get; set; } + public string? FileName { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Auth/AuthorizeRoles.cs b/LSFE.Infrastructure/Entities/Auth/AuthorizeRoles.cs new file mode 100644 index 0000000..7671dd2 --- /dev/null +++ b/LSFE.Infrastructure/Entities/Auth/AuthorizeRoles.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace LSFE.Infrastructure.Entities.Auth +{ + public class AuthorizeRoles + { + [Key] + public int AuthorizeRoleId { get; set; } + public string? RoleId { get; set; } + public string? Name { get; set; } + public string? Controller { get; set; } + public bool IsActive { get; set; } + public DateTime CreatedAt { get; set; } + public string? CreatedBy { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Auth/Module.cs b/LSFE.Infrastructure/Entities/Auth/Module.cs new file mode 100644 index 0000000..9a02ebd --- /dev/null +++ b/LSFE.Infrastructure/Entities/Auth/Module.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace LSFE.Infrastructure.Entities.Auth +{ + [Table("Modules")] + public class Module + { + [Key] + public int ModuleId { get; set; } + + [Required] + [MaxLength(50)] + public required string ModuleName { get; set; } + + [Required] + [MaxLength(50)] + public required string ModuleKey { get; set; } + + [Required] + [MaxLength(50)] + public required string IconName { get; set; } + + public int DisplayOrder { get; set; } + + public bool IsActive { get; set; } = true; + + // CHANGE: Make this nullable for root modules + public int? ParentModuleId { get; set; } + + [MaxLength(200)] + public required string RouteURL { get; set; } + + [MaxLength(200)] + public string? Description { get; set; } + + public DateTime CreatedDate { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + [ForeignKey("ParentModuleId")] + public virtual Module? ParentModule { get; set; } + + public virtual ICollection? ChildModules { get; set; } + + public virtual ICollection? RoleModulePermissions { get; set; } + + } +} \ No newline at end of file diff --git a/LSFE.Infrastructure/Entities/Auth/Role.cs b/LSFE.Infrastructure/Entities/Auth/Role.cs new file mode 100644 index 0000000..52c0e43 --- /dev/null +++ b/LSFE.Infrastructure/Entities/Auth/Role.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Identity; + +namespace LSFE.Infrastructure.Entities.Auth +{ + public class Role : IdentityRole + { + public virtual ICollection RoleModulePermissions { get; set; } + = new List(); + } +} \ No newline at end of file diff --git a/LSFE.Infrastructure/Entities/Auth/RoleModulePermission.cs b/LSFE.Infrastructure/Entities/Auth/RoleModulePermission.cs new file mode 100644 index 0000000..8a4c466 --- /dev/null +++ b/LSFE.Infrastructure/Entities/Auth/RoleModulePermission.cs @@ -0,0 +1,40 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace LSFE.Infrastructure.Entities.Auth +{ + [Table("RoleModulePermissions")] + public class RoleModulePermission + { + [Key] + public int PermissionId { get; set; } + + [Required] + public string RoleId { get; set; } = default!; + + [Required] + public int ModuleId { get; set; } + + public bool CanView { get; set; } = false; + + public bool CanCreate { get; set; } = false; + + public bool CanEdit { get; set; } = false; + + public bool CanDelete { get; set; } = false; + + [MaxLength(500)] + public string? BadgeQuery { get; set; } + + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; + + // Navigation Properties + [ForeignKey("RoleId")] + public virtual Role? Role { get; set; } + + [ForeignKey("ModuleId")] + public virtual Module? Module { get; set; } + } +} \ No newline at end of file diff --git a/LSFE.Infrastructure/Entities/BaseLogEntity.cs b/LSFE.Infrastructure/Entities/BaseLogEntity.cs new file mode 100644 index 0000000..d1a98ba --- /dev/null +++ b/LSFE.Infrastructure/Entities/BaseLogEntity.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities +{ + public class BaseLogEntity + { + public DateTime CreatedDate { get; set; }= DateTime.Now; + public DateTime UpdatedDate { get; set; } + public string? CreatedBy { get; set; } + public string? UpdatedBy { get;set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Departments.cs b/LSFE.Infrastructure/Entities/Departments.cs new file mode 100644 index 0000000..feccc98 --- /dev/null +++ b/LSFE.Infrastructure/Entities/Departments.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities +{ + [Table("Departments")] + public class Departments + { + [Key] + public int DepartmentId { get; set; } + public string? DepartmentCode { get; set; } + public string? DepartmentName { get; set; } + public bool IsActive { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Doctor/Doctor.cs b/LSFE.Infrastructure/Entities/Doctor/Doctor.cs new file mode 100644 index 0000000..4fe4bfe --- /dev/null +++ b/LSFE.Infrastructure/Entities/Doctor/Doctor.cs @@ -0,0 +1,39 @@ +using LSFE.Infrastructure.Entities.Maintenance; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Doctor +{ + [Table("Doctors")] + public class Doctor : BaseLogEntity + { + [Key] + public int DoctorId { get; set; } + public string? MiddleInitial { get; set; } + public string? LastName { get; set; } + public string? FirstName { get; set; } + public byte MaxVisit { get; set; } + public byte SpecializationId { get; set; } + public string? UserId { get; set; } + public byte StageLevelId { get; set; } + public DateTime? BirthDate { get; set; } + public string? EmailAddress { get; set; } + public string? LicenseNo { get; set; } + public string? PhoneNo { get; set; } + public int InstitutionId { get; set; } + [ForeignKey(nameof(InstitutionId))] + public Institutions? Institution { get; set; } + public byte Status { get; set; } + public bool IsActive { get; set; } + public bool IsTerritorial { get; set; } + public string? Address { get; set; } + public int? DistrictId { get; set; } + [ForeignKey(nameof(DistrictId))] + public Districts? Districts { get; set; } + } +} \ No newline at end of file diff --git a/LSFE.Infrastructure/Entities/Doctor/DoctorCoverageToday.cs b/LSFE.Infrastructure/Entities/Doctor/DoctorCoverageToday.cs new file mode 100644 index 0000000..bd240e3 --- /dev/null +++ b/LSFE.Infrastructure/Entities/Doctor/DoctorCoverageToday.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Doctor +{ + public class DoctorCoverageToday + { + [Key] + public int DoctorId { get; set; } + public Guid MRRawPlanId { get; set; } + public string? MiddleInitial { get; set; } + public string? LastName { get; set; } + public string? FirstName { get; set; } + public byte MaxVisit { get; set; } + public string? SpecializationName { get; set; } + public DateTime BirthDate { get; set; } + public string? EmailAddress { get; set; } + public string? LicenseNo { get; set; } + public string? PhoneNo { get; set; } + public int InstitutionId { get; set; } + public string? InstitutionName { get; set; } + public byte Status { get; set; } + public bool IsTerritorial { get; set; } + public string? Address { get; set; } + public string? DistrictName { get; set; } + public string? AreaName { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Doctor/ForDoctorApproval.cs b/LSFE.Infrastructure/Entities/Doctor/ForDoctorApproval.cs new file mode 100644 index 0000000..a038865 --- /dev/null +++ b/LSFE.Infrastructure/Entities/Doctor/ForDoctorApproval.cs @@ -0,0 +1,39 @@ +using LSFE.Infrastructure.Entities.Maintenance; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Doctor +{ + [Table("ForDoctorApproval")] + public class ForDoctorApproval : BaseLogEntity + { + [Key] + public Guid ForDoctorApprovalId { get; set; } + public int DoctorId { get; set; } + public string? MiddleInitial { get; set; } + public string? LastName { get; set; } + public string? FirstName { get; set; } + public byte MaxVisit { get; set; } + public byte SpecializationId { get; set; } + public string? UserId { get; set; } + public byte StageLevelId { get; set; } + public DateTime? BirthDate { get; set; } + public string? EmailAddress { get; set; } + public string? LicenseNo { get; set; } + public string? PhoneNo { get; set; } + public int InstitutionId { get; set; } + [ForeignKey(nameof(InstitutionId))] + public Institutions? Institution { get; set; } + public bool IsTerritorial { get; set; } + public string? Address { get; set; } + public byte Status { get; set; } + public int? DistrictId { get; set; } + [ForeignKey(nameof(DistrictId))] + public Districts? Districts { get; set; } + } +} \ No newline at end of file diff --git a/LSFE.Infrastructure/Entities/Doctor/Specialization.cs b/LSFE.Infrastructure/Entities/Doctor/Specialization.cs new file mode 100644 index 0000000..56cd5d4 --- /dev/null +++ b/LSFE.Infrastructure/Entities/Doctor/Specialization.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Doctor +{ + [Table("Specialization")] + public class Specialization + { + [Key] + public byte SpecializationId { get; set; } + public string? SpecializationName { get; set; } + public int CompanyId { get; set; } + public bool IsActive { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/ErrorLogs.cs b/LSFE.Infrastructure/Entities/ErrorLogs.cs new file mode 100644 index 0000000..0a70151 --- /dev/null +++ b/LSFE.Infrastructure/Entities/ErrorLogs.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities +{ + [Table("ErrorLogs")] + public class ErrorLogs + { + [Key] + public int Id { get; set; } + public string? Message { get; set; } + public string? CreatedBy { get; set; } + public string? Application { get; set; } + public DateTime CreatedDate { get; set; }= DateTime.UtcNow; + } +} diff --git a/LSFE.Infrastructure/Entities/Maintenance/Areas.cs b/LSFE.Infrastructure/Entities/Maintenance/Areas.cs new file mode 100644 index 0000000..85b20ed --- /dev/null +++ b/LSFE.Infrastructure/Entities/Maintenance/Areas.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Maintenance +{ + [Table("Areas")] + public class Areas + { + [Key] + public int AreaId { get; set; } + public string? AreaName { get; set; } + public bool IsActive { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Maintenance/Districts.cs b/LSFE.Infrastructure/Entities/Maintenance/Districts.cs new file mode 100644 index 0000000..186f1fe --- /dev/null +++ b/LSFE.Infrastructure/Entities/Maintenance/Districts.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Maintenance +{ + [Table("Districts")] + public class Districts + { + [Key] + public int DistrictId { get; set; } + public string? DistrictName { get; set; } + public int AreaId { get; set; } + [ForeignKey("AreaId")] + public Areas? Areas { get; set; } + public bool IsActive { get; set; } + public ICollection Doctors { get; set; } = new List(); + } +} diff --git a/LSFE.Infrastructure/Entities/Maintenance/Divisions.cs b/LSFE.Infrastructure/Entities/Maintenance/Divisions.cs new file mode 100644 index 0000000..81b891e --- /dev/null +++ b/LSFE.Infrastructure/Entities/Maintenance/Divisions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Maintenance +{ + [Table("Divisions")] + public class Divisions + { + [Key] + public int DivisionId { get; set; } + public string? DivisionName { get; set; } + public bool IsActive { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Maintenance/InstitutionCategory.cs b/LSFE.Infrastructure/Entities/Maintenance/InstitutionCategory.cs new file mode 100644 index 0000000..78708da --- /dev/null +++ b/LSFE.Infrastructure/Entities/Maintenance/InstitutionCategory.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Maintenance +{ + [Table("InstitutionCategory")] + public class InstitutionCategory + { + [Key] + public int InstitutionCategoryId { get; set; } + public string? InstitutionCategoryCode { get; set; } + public string? InstitutionCategoryName { get; set; } + public bool IsActive { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Maintenance/Institutions.cs b/LSFE.Infrastructure/Entities/Maintenance/Institutions.cs new file mode 100644 index 0000000..b0919f3 --- /dev/null +++ b/LSFE.Infrastructure/Entities/Maintenance/Institutions.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Maintenance +{ + [Table("Institutions")] + public class Institutions + { + [Key] + public int InstitutionId { get; set; } + public string? InstitutionName { get; set; } + public int TerritoryId { get; set; } + [ForeignKey("TerritoryId")] + public Territories? Territories { get; set; } + public bool IsActive { get; set; } + public int InstitutionCategoryId { get; set; } + [ForeignKey("InstitutionCategoryId")] + public InstitutionCategory? InstitutionCategory { get; set; } + public ICollection Doctors { get; set; } = new List(); + } +} diff --git a/LSFE.Infrastructure/Entities/Maintenance/Inventory.cs b/LSFE.Infrastructure/Entities/Maintenance/Inventory.cs new file mode 100644 index 0000000..28d705f --- /dev/null +++ b/LSFE.Infrastructure/Entities/Maintenance/Inventory.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Maintenance +{ + + [Table("Inventory")] + public class Inventory + { + [Key] + public Guid InventoryId { get; set; } + public string? UserId { get; set; } + public string? Description { get; set; } + public int QtyIn { get; set; } + public int QtyOut { get; set; } + public int QtyBalance { get; set; } + public byte CycleMonth { get; set; } + public int CycleYear { get; set; } + public string? MedRepName { get; set; } + public string? CreatedBy { get; set; } + public DateTime CreatedDate { get; set; } + public string? UpdatedBy { get; set; } + public DateTime? UpdatedDate { get; set; } + public bool IsActive { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Maintenance/InventoryTransaction.cs b/LSFE.Infrastructure/Entities/Maintenance/InventoryTransaction.cs new file mode 100644 index 0000000..543cce2 --- /dev/null +++ b/LSFE.Infrastructure/Entities/Maintenance/InventoryTransaction.cs @@ -0,0 +1,32 @@ +using LSFE.Infrastructure.Model; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Maintenance +{ + [Table("InventoryTransaction")] + public class InventoryTransaction + { + [Key] + public Guid InventoryTransId { get; set; } + public Guid InventoryId { get; set; } + public Guid MRRawPlanId { get; set; } + public int DoctorId { get; set; } + public string? Description { get; set; } + public DateOnly? AppointmentDate { get; set; } + public DateTime CreatedDate { get; set; } + public int QtyIn { get; set; } + public int QtyOut { get; set; } + public string? UserId { get; set; } + public string? CreatedBy { get; set; } + public bool IsActive { get; set; } + public string? UpdatedBy { get; set; } + public DateTime? UpdatedDate { get; set; } + public byte IconEmojiId { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Maintenance/ProductTransaction.cs b/LSFE.Infrastructure/Entities/Maintenance/ProductTransaction.cs new file mode 100644 index 0000000..0e567c4 --- /dev/null +++ b/LSFE.Infrastructure/Entities/Maintenance/ProductTransaction.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Maintenance +{ + [Table("ProductTransaction")] + public class ProductTransaction + { + [Key] + public Guid ProductTransId { get; set; } + public int ProductId { get; set; } + public string? ProductName { get; set; } + public int DoctorId { get; set; } + public Guid MRRawPlanId { get; set; } + public DateOnly AppointmentDate { get; set; } + public DateTime CreatedDate { get; set; } + public string? CreatedBy { get; set; } + public string? UserId { get; set; } + public bool IsActive { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Maintenance/Products.cs b/LSFE.Infrastructure/Entities/Maintenance/Products.cs new file mode 100644 index 0000000..3043ff6 --- /dev/null +++ b/LSFE.Infrastructure/Entities/Maintenance/Products.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Maintenance +{ + [Table("Products")] + public class Products + { + [Key] + public int ProductId { get; set; } + public string? ProductName { get; set; } + public string? ProductLink { get; set; } + public string? UserId { get; set; } + public bool IsActive { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Maintenance/Territories.cs b/LSFE.Infrastructure/Entities/Maintenance/Territories.cs new file mode 100644 index 0000000..13101fc --- /dev/null +++ b/LSFE.Infrastructure/Entities/Maintenance/Territories.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Maintenance +{ + [Table("Territories")] + public class Territories + { + [Key] + public int TerritoryId { get; set; } + public string? TerritoryName { get; set; } + public int DistrictId { get; set; } + [ForeignKey("DistrictId")] + public Districts? Districts { get; set; } + public bool IsActive { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Planning/CatchUpCall.cs b/LSFE.Infrastructure/Entities/Planning/CatchUpCall.cs new file mode 100644 index 0000000..6ec3d7e --- /dev/null +++ b/LSFE.Infrastructure/Entities/Planning/CatchUpCall.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Planning +{ + [Table("CatchUpCalls")] + public class CatchUpCall + { + [Key] + public Guid CatchUpCallId { get; set; } + public Guid MRRawPlanId { get; set; } + public Guid? MRPlanDetailId { get; set; } + public byte? VisitCount { get; set; } + public DateOnly? CallDate { get; set; } + public DateOnly? ActualCallDate { get; set; } + public DateTime? StartTime { get; set; } + public DateTime? EndTime { get; set; } + public string? Longitude { get; set; } + public string? Latitude { get; set; } + public string? Location { get; set; } + public bool IsSignature { get; set; } = false; + public string? SignatureFileName { get; set; } + public bool IsMissed { get; set; } = false; + public string? MissedReason { get; set; } + public bool IsDoctorProxy { get; set; } = false; + public bool IsReschedule { get; set; } = false; + public string? RescheduleReason { get; set; } + public string? CallType { get; set; } + public bool IsJoinCall { get; set; } = false; + public byte? CycleId { get; set; } + public bool IsApproved { get; set; } = false; + public bool IsRecovered { get; set; } = false; + public bool IsLiterature { get; set; } + public bool IsCatchUpCall { get; set; } + public string? SLP { get; set; } + public byte Status { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Planning/DoctorById.cs b/LSFE.Infrastructure/Entities/Planning/DoctorById.cs new file mode 100644 index 0000000..bde380c --- /dev/null +++ b/LSFE.Infrastructure/Entities/Planning/DoctorById.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Planning +{ + public class DoctorById + { + [Key] + public string? MRRawPlanId { get; set; } + public Guid MRPlanDetailId { get; set; } + public int DoctorId { get; set; } + public string? MRFullName { get; set; } + public string? InstitutionName { get; set; } + public Guid MRPlanHeaderId { get; set; } + public string? MRPlanDate { get; set; } + public string? DivisionName { get; set; } + public int DivisionId { get; set; } + public string? FirstName { get; set; } + public string? MiddleInitial { get; set; } + public string? LastName { get; set; } + public string? TerritoryName { get; set; } + public string? SpecializationName { get; set; } + public bool IsLiterature { get; set; } + public bool IsApproved { get; set; } + public byte MaxVisit { get; set; } + public string? Status { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Planning/DoctorWithPlan.cs b/LSFE.Infrastructure/Entities/Planning/DoctorWithPlan.cs new file mode 100644 index 0000000..0a60610 --- /dev/null +++ b/LSFE.Infrastructure/Entities/Planning/DoctorWithPlan.cs @@ -0,0 +1,30 @@ +using LSFE.Infrastructure.Entities.Doctor; +using LSFE.Infrastructure.Entities.Maintenance; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Planning +{ + public class DoctorWithPlan + { + [Key] + public int DoctorId { get; set; } + public string? MRFullName { get; set; } + public string? InstitutionName { get; set; } + public Guid MRPlanHeaderId { get; set; } + public Guid MRPlanDetailId { get; set; } + public string? MRPlanDate { get; set; } + public int TerritoryId { get; set; } + public string? TerritoryName { get; set; } + public string? FirstName { get; set; } + public string? MiddleInitial { get; set; } + public string? LastName { get; set; } + public string? SpecializationName { get; set; } + public byte MaxVisit { get; set; } + public string? Status { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Planning/MRPlanDetail.cs b/LSFE.Infrastructure/Entities/Planning/MRPlanDetail.cs new file mode 100644 index 0000000..18803c7 --- /dev/null +++ b/LSFE.Infrastructure/Entities/Planning/MRPlanDetail.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Planning +{ + [Table("MRPlanDetails")] + public class MRPlanDetail + { + [Key] + public Guid MRPlanDetailId { get; set; } + public Guid MRPlanHeaderId { get; set; } + public byte MaxVisit { get; set; } + public int DoctorId { get; set; } + public int TerritoryId { get; set; } + public int DivisionId { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Planning/MRPlanHeader.cs b/LSFE.Infrastructure/Entities/Planning/MRPlanHeader.cs new file mode 100644 index 0000000..1e20cee --- /dev/null +++ b/LSFE.Infrastructure/Entities/Planning/MRPlanHeader.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Planning +{ + [Table("MRPlanHeader")] + public class MRPlanHeader + { + [Key] + public Guid MRPlanHeaderId { get; set; } + public string? MRUserId { get; set; } + public bool IsActive { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Planning/MRRawPlan.cs b/LSFE.Infrastructure/Entities/Planning/MRRawPlan.cs new file mode 100644 index 0000000..5638d55 --- /dev/null +++ b/LSFE.Infrastructure/Entities/Planning/MRRawPlan.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Reflection.Metadata; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Planning +{ + [Table("MRRawPlans")] + public class MRRawPlan + { + [Key] + public Guid MRRawPlanId { get; set; } + public Guid? MRPlanDetailId { get; set; } + public byte? VisitCount { get; set; } + public DateOnly? CallDate { get; set; } + public DateOnly? ActualCallDate { get; set; } + public DateTime? StartTime { get; set; } + public DateTime? EndTime { get; set; } + public string? Longitude { get; set; } + public string? Latitude { get; set; } + public string? Location { get; set; } + public bool IsSignature { get; set; } = false; + public string? SignatureFileName { get; set; } + public bool IsMissed { get; set; } = false; + public string? MissedReason { get; set; } + public bool IsDoctorProxy { get; set; } = false; + public bool IsReschedule { get; set; } = false; + public string? RescheduleReason { get; set; } + public string? CallType { get; set; } + public bool IsJoinCall { get; set; } = false; + public byte? CycleId { get; set; } + public bool IsApproved { get; set; } = false; + public bool IsRecovered { get; set; } = false; + public bool IsLiterature { get; set; } + public string? SLP { get; set; } + public string? ApprovedBy { get; set; } + public DateTime? ApprovedDate { get; set; } + public byte Status { get; set; } + public bool IsCatchUpCall { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Planning/PendingApproval.cs b/LSFE.Infrastructure/Entities/Planning/PendingApproval.cs new file mode 100644 index 0000000..eec5a2b --- /dev/null +++ b/LSFE.Infrastructure/Entities/Planning/PendingApproval.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Planning +{ + public class PendingApproval + { + [Key] + public Guid MRRawPlanId { get; set; } + public Guid MRPlanDetailId { get; set; } + public int DoctorId { get; set; } + public string? MRFullName { get; set; } + public string? InstitutionName { get; set; } + public Guid MRPlanHeaderId { get; set; } + public string? MRPlanDate { get; set; } + public string? DivisionName { get; set; } + public int DivisionId { get; set; } + public string? TerritoryName { get; set; } + public string? FirstName { get; set; } + public string? MiddleInitial { get; set; } + public string? LastName { get; set; } + public string? SpecializationName { get; set; } + public byte MaxVisit { get; set; } + public string? Status { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Reports/ActualCoverageReport.cs b/LSFE.Infrastructure/Entities/Reports/ActualCoverageReport.cs new file mode 100644 index 0000000..daeaaf8 --- /dev/null +++ b/LSFE.Infrastructure/Entities/Reports/ActualCoverageReport.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Reports +{ + [Keyless] + public class ActualCoverageReport + { + public Guid MRRawPlanId { get; set; } + public Guid? MRPlanDetailId { get; set; } + public DateOnly? CallDate { get; set; } + public DateOnly? ActualCallDate { get; set; } + public DateTime? StartTime { get; set; } + public DateTime? EndTime { get; set; } + public string? Longitude { get; set; } + public string? Latitude { get; set; } + public string? Location { get; set; } + public bool IsSignature { get; set; } = false; + public string? SignatureFileName { get; set; } + public bool IsMissed { get; set; } = false; + public string? MissedReason { get; set; } + public bool IsDoctorProxy { get; set; } = false; + public bool IsReschedule { get; set; } = false; + public string? RescheduleReason { get; set; } + public string? CallType { get; set; } + public bool IsApproved { get; set; } = false; + public bool IsRecovered { get; set; } = false; + public bool IsLiterature { get; set; } + public string? SLP { get; set; } + public string? ApprovedBy { get; set; } + public DateTime? ApprovedDate { get; set; } + public bool IsCatchUpCall { get; set; } + public string? MedRepName { get; set; } + public string? Company { get; set; } + public string? DoctorName { get; set; } + public string? TerritoryName { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Reports/CallRateReport.cs b/LSFE.Infrastructure/Entities/Reports/CallRateReport.cs new file mode 100644 index 0000000..c950cbc --- /dev/null +++ b/LSFE.Infrastructure/Entities/Reports/CallRateReport.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Reports +{ + public class CallRateReport + { + [Key] + public required string MRUserId { get; set; } + public string? MedRepName { get; set; } + public string? Company { get; set; } + public int TotalTargetCall { get; set; } + public int TotalActualCallRate { get; set; } + public int TotalMissedCall { get; set; } + public int TotalReschedule { get; set; } + public decimal TotalCallRatePercentage { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Reports/CallReachReport.cs b/LSFE.Infrastructure/Entities/Reports/CallReachReport.cs new file mode 100644 index 0000000..64fc4e7 --- /dev/null +++ b/LSFE.Infrastructure/Entities/Reports/CallReachReport.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Reports +{ + public class CallReachReport + { + [Key] + public required string MRUserId { get; set; } + public string? MedRepName { get; set; } + public string? Company { get; set; } + public int TotalTargetCall { get; set; } + public int TotalActualCallReach { get; set; } + public decimal TotalCallReachPercentage { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/Reports/CatchUpCallReport.cs b/LSFE.Infrastructure/Entities/Reports/CatchUpCallReport.cs new file mode 100644 index 0000000..d967faf --- /dev/null +++ b/LSFE.Infrastructure/Entities/Reports/CatchUpCallReport.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.Reports +{ + public class CatchUpCallReport + { + [Key] + public Guid CatchUpCallId { get; set; } + public Guid MRRawPlanId { get; set; } + public Guid? MRPlanDetailId { get; set; } + public byte? VisitCount { get; set; } + public DateOnly? ActualCallDate { get; set; } + public DateTime? StartTime { get; set; } + public DateTime? EndTime { get; set; } + public string? Longitude { get; set; } + public string? Latitude { get; set; } + public string? Location { get; set; } + public string? MedRepName { get; set; } + public string? DoctorName { get; set; } + public string? Company { get; set; } + public string? MissedReason { get; set; } + public DateOnly OriginalCallDate { get; set; } + public DateOnly CatchUpCallDate { get; set; } + } +} diff --git a/LSFE.Infrastructure/Entities/TimeKeeping/Attendance.cs b/LSFE.Infrastructure/Entities/TimeKeeping/Attendance.cs new file mode 100644 index 0000000..171e83d --- /dev/null +++ b/LSFE.Infrastructure/Entities/TimeKeeping/Attendance.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace LSFE.Infrastructure.Entities.TimeKeeping +{ + [Table("Attendance")] + public class Attendance + { + [Key] + public int AttendanceId { get; set; } + public string? AppAttendanceId { get; set; } + public DateTime? TimeIn { get; set; } + public DateTime? TimeOut { get; set; } + public string? UserId { get; set; } + public string? TimeInLocation { get; set; } + public string? TimeOutLocation { get; set; } + public string? SignatureFileNameIn { get; set; } + public string? SignatureFileNameOut { get; set; } + } +} \ No newline at end of file diff --git a/LSFE.Infrastructure/Entities/TimeKeeping/AttendanceDto.cs b/LSFE.Infrastructure/Entities/TimeKeeping/AttendanceDto.cs new file mode 100644 index 0000000..03505ae --- /dev/null +++ b/LSFE.Infrastructure/Entities/TimeKeeping/AttendanceDto.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Entities.TimeKeeping +{ + public class AttendanceDto + { + [Key] + public int AttendanceId { get; set; } + public string? AppAttendanceId { get; set; } + public DateTime? TimeIn { get; set; } + public DateTime? TimeOut { get; set; } + public string? UserId { get; set; } + public string? TimeInLocation { get; set; } + public string? TimeOutLocation { get; set; } + public string? SignatureFileNameIn { get; set; } + public string? SignatureFileNameOut { get; set; } + public string? MedRepName { get; set; } + } +} diff --git a/LSFE.Infrastructure/LSFE.Infrastructure.csproj b/LSFE.Infrastructure/LSFE.Infrastructure.csproj new file mode 100644 index 0000000..55633ad --- /dev/null +++ b/LSFE.Infrastructure/LSFE.Infrastructure.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/LSFE.Infrastructure/Migrations/20250920072412_iniitialCreate.Designer.cs b/LSFE.Infrastructure/Migrations/20250920072412_iniitialCreate.Designer.cs new file mode 100644 index 0000000..646e709 --- /dev/null +++ b/LSFE.Infrastructure/Migrations/20250920072412_iniitialCreate.Designer.cs @@ -0,0 +1,363 @@ +// +using System; +using LSFE.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace LSFE.Infrastructure.Migrations +{ + [DbContext(typeof(LSFEDbContext))] + [Migration("20250920072412_iniitialCreate")] + partial class iniitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("LSFE.Infrastructure.Entities.AppsUsers", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("AttachmentId") + .HasColumnType("int"); + + b.Property("Company") + .HasColumnType("nvarchar(max)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DepartmentId") + .HasColumnType("int"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FullName") + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedDate") + .HasColumnType("datetime2"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("AttachmentId"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("LSFE.Infrastructure.Entities.Attachment", b => + { + b.Property("AttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("AttachmentId")); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("URL") + .HasColumnType("nvarchar(max)"); + + b.HasKey("AttachmentId"); + + b.ToTable("Attachment", (string)null); + }); + + modelBuilder.Entity("LSFE.Infrastructure.Entities.Departments", b => + { + b.Property("DepartmentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DepartmentId")); + + b.Property("Department") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.HasKey("DepartmentId"); + + b.ToTable("Departments", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("Roles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", (string)null); + }); + + modelBuilder.Entity("LSFE.Infrastructure.Entities.AppsUsers", b => + { + b.HasOne("LSFE.Infrastructure.Entities.Attachment", "Attachment") + .WithMany() + .HasForeignKey("AttachmentId"); + + b.HasOne("LSFE.Infrastructure.Entities.Departments", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Attachment"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("LSFE.Infrastructure.Entities.AppsUsers", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("LSFE.Infrastructure.Entities.AppsUsers", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LSFE.Infrastructure.Entities.AppsUsers", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("LSFE.Infrastructure.Entities.AppsUsers", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LSFE.Infrastructure/Migrations/20250920072412_iniitialCreate.cs b/LSFE.Infrastructure/Migrations/20250920072412_iniitialCreate.cs new file mode 100644 index 0000000..09d1fbe --- /dev/null +++ b/LSFE.Infrastructure/Migrations/20250920072412_iniitialCreate.cs @@ -0,0 +1,287 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace LSFE.Infrastructure.Migrations +{ + /// + public partial class iniitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Attachment", + columns: table => new + { + AttachmentId = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + URL = table.Column(type: "nvarchar(max)", nullable: true), + FileName = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Attachment", x => x.AttachmentId); + }); + + migrationBuilder.CreateTable( + name: "Departments", + columns: table => new + { + DepartmentId = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Department = table.Column(type: "nvarchar(max)", nullable: true), + IsActive = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Departments", x => x.DepartmentId); + }); + + migrationBuilder.CreateTable( + name: "Roles", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Roles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + FullName = table.Column(type: "nvarchar(max)", nullable: true), + Company = table.Column(type: "nvarchar(max)", nullable: true), + DepartmentId = table.Column(type: "int", nullable: true), + CreatedDate = table.Column(type: "datetime2", nullable: false), + UpdatedDate = table.Column(type: "datetime2", nullable: false), + CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), + UpdatedBy = table.Column(type: "nvarchar(max)", nullable: true), + Address = table.Column(type: "nvarchar(max)", nullable: true), + AttachmentId = table.Column(type: "int", nullable: true), + UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "bit", nullable: false), + PasswordHash = table.Column(type: "nvarchar(max)", nullable: true), + SecurityStamp = table.Column(type: "nvarchar(max)", nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumber = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumberConfirmed = table.Column(type: "bit", nullable: false), + TwoFactorEnabled = table.Column(type: "bit", nullable: false), + LockoutEnd = table.Column(type: "datetimeoffset", nullable: true), + LockoutEnabled = table.Column(type: "bit", nullable: false), + AccessFailedCount = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + table.ForeignKey( + name: "FK_Users_Attachment_AttachmentId", + column: x => x.AttachmentId, + principalTable: "Attachment", + principalColumn: "AttachmentId"); + table.ForeignKey( + name: "FK_Users_Departments_DepartmentId", + column: x => x.DepartmentId, + principalTable: "Departments", + principalColumn: "DepartmentId"); + }); + + migrationBuilder.CreateTable( + name: "RoleClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + RoleId = table.Column(type: "nvarchar(450)", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_RoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_RoleClaims_Roles_RoleId", + column: x => x.RoleId, + principalTable: "Roles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "UserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + ProviderKey = table.Column(type: "nvarchar(450)", nullable: false), + ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true), + UserId = table.Column(type: "nvarchar(450)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_UserLogins_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "UserClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(type: "nvarchar(450)", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UserClaims", x => x.Id); + table.ForeignKey( + name: "FK_UserClaims_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "UserRoles", + columns: table => new + { + UserId = table.Column(type: "nvarchar(450)", nullable: false), + RoleId = table.Column(type: "nvarchar(450)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_UserRoles_Roles_RoleId", + column: x => x.RoleId, + principalTable: "Roles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_UserRoles_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "UserTokens", + columns: table => new + { + UserId = table.Column(type: "nvarchar(450)", nullable: false), + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(450)", nullable: false), + Value = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_UserTokens_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_RoleClaims_RoleId", + table: "RoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "IX_UserLogins_UserId", + table: "UserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "Roles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_UserClaims_UserId", + table: "UserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_UserRoles_RoleId", + table: "UserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "Users", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "IX_Users_AttachmentId", + table: "Users", + column: "AttachmentId"); + + migrationBuilder.CreateIndex( + name: "IX_Users_DepartmentId", + table: "Users", + column: "DepartmentId"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "Users", + column: "NormalizedUserName", + unique: true, + filter: "[NormalizedUserName] IS NOT NULL"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "RoleClaims"); + + migrationBuilder.DropTable( + name: "UserLogins"); + + migrationBuilder.DropTable( + name: "UserClaims"); + + migrationBuilder.DropTable( + name: "UserRoles"); + + migrationBuilder.DropTable( + name: "UserTokens"); + + migrationBuilder.DropTable( + name: "Roles"); + + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.DropTable( + name: "Attachment"); + + migrationBuilder.DropTable( + name: "Departments"); + } + } +} diff --git a/LSFE.Infrastructure/Migrations/LSFEDbContextModelSnapshot.cs b/LSFE.Infrastructure/Migrations/LSFEDbContextModelSnapshot.cs new file mode 100644 index 0000000..2a74371 --- /dev/null +++ b/LSFE.Infrastructure/Migrations/LSFEDbContextModelSnapshot.cs @@ -0,0 +1,360 @@ +// +using System; +using LSFE.Infrastructure.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace LSFE.Infrastructure.Migrations +{ + [DbContext(typeof(LSFEDbContext))] + partial class LSFEDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("LSFE.Infrastructure.Entities.AppsUsers", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("Address") + .HasColumnType("nvarchar(max)"); + + b.Property("AttachmentId") + .HasColumnType("int"); + + b.Property("Company") + .HasColumnType("nvarchar(max)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedDate") + .HasColumnType("datetime2"); + + b.Property("DepartmentId") + .HasColumnType("int"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("FullName") + .HasColumnType("nvarchar(max)"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedDate") + .HasColumnType("datetime2"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("AttachmentId"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("LSFE.Infrastructure.Entities.Attachment", b => + { + b.Property("AttachmentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("AttachmentId")); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("URL") + .HasColumnType("nvarchar(max)"); + + b.HasKey("AttachmentId"); + + b.ToTable("Attachment", (string)null); + }); + + modelBuilder.Entity("LSFE.Infrastructure.Entities.Departments", b => + { + b.Property("DepartmentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DepartmentId")); + + b.Property("Department") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.HasKey("DepartmentId"); + + b.ToTable("Departments", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("Roles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", (string)null); + }); + + modelBuilder.Entity("LSFE.Infrastructure.Entities.AppsUsers", b => + { + b.HasOne("LSFE.Infrastructure.Entities.Attachment", "Attachment") + .WithMany() + .HasForeignKey("AttachmentId"); + + b.HasOne("LSFE.Infrastructure.Entities.Departments", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Attachment"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("LSFE.Infrastructure.Entities.AppsUsers", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("LSFE.Infrastructure.Entities.AppsUsers", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LSFE.Infrastructure.Entities.AppsUsers", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("LSFE.Infrastructure.Entities.AppsUsers", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/LSFE.Infrastructure/Model/Admin/AdminVerificationRequest.cs b/LSFE.Infrastructure/Model/Admin/AdminVerificationRequest.cs new file mode 100644 index 0000000..ea52d13 --- /dev/null +++ b/LSFE.Infrastructure/Model/Admin/AdminVerificationRequest.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Model.Admin +{ + public class AdminVerificationRequest + { + public string? AdminUserName { get; set; } + public string? AdminPassword { get; set; } + } + public class RevokeDeviceRequest + { + public string? AdminPassword { get; set; } + public string? UserId { get; set; } + public string? DeviceId { get; set; } + public string? Reason { get; set; } + } + + /*public class UserDevice + { + public int Id { get; set; } + public string? UserId { get; set; } + public string? DeviceId { get; set; } + public string? DeviceModel { get; set; } + public string? DevicePlatform { get; set; } + public string? DeviceVersion { get; set; } + public string? DeviceManufacturer { get; set; } + public string? AppVersion { get; set; } + public bool IsAuthorized { get; set; } + public bool IsActive { get; set; } + public DateTime RegisteredDate { get; set; } + public string? RegisteredBy { get; set; } + public DateTime? LastLoginDate { get; set; } + public DateTime? DeactivatedDate { get; set; } + public string? DeactivatedBy { get; set; } + public string? DeactivationReason { get; set; } + public string? Notes { get; set; } + }*/ +} diff --git a/LSFE.Infrastructure/Model/Admin/AdminVerificationResponse.cs b/LSFE.Infrastructure/Model/Admin/AdminVerificationResponse.cs new file mode 100644 index 0000000..3020da9 --- /dev/null +++ b/LSFE.Infrastructure/Model/Admin/AdminVerificationResponse.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Model.Admin +{ + public class AdminVerificationResponse + { + public bool Success { get; set; } + public string? Data { get; set; } + public string? Message { get; set; } + public int MessCode { get; set; } + } +} diff --git a/LSFE.Infrastructure/Model/Admin/DeviceInfo.cs b/LSFE.Infrastructure/Model/Admin/DeviceInfo.cs new file mode 100644 index 0000000..07e266a --- /dev/null +++ b/LSFE.Infrastructure/Model/Admin/DeviceInfo.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Model.Admin +{ + public class DeviceInfo + { + public string? DeviceId { get; set; } + public string? DeviceModel { get; set; } + public string? Platform { get; set; } + public string? Version { get; set; } + public string? Manufacturer { get; set; } + public string? Name { get; set; } + public string? DeviceType { get; set; } + public string? VersionString { get; set; } + public string? UserName { get; set; } + } +} diff --git a/LSFE.Infrastructure/Model/Admin/DeviceRegistrationRequest.cs b/LSFE.Infrastructure/Model/Admin/DeviceRegistrationRequest.cs new file mode 100644 index 0000000..527ea1b --- /dev/null +++ b/LSFE.Infrastructure/Model/Admin/DeviceRegistrationRequest.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Model.Admin +{ + public class DeviceRegistrationRequest + { + public string? MedRepUsername { get; set; } + public string? MedRepPassword { get; set; } + public string? DeviceId { get; set; } + public string? DeviceModel { get; set; } + public string? DevicePlatform { get; set; } + public string? DeviceVersion { get; set; } + public string? DeviceManufacturer { get; set; } + public string? AppVersion { get; set; } + } +} diff --git a/LSFE.Infrastructure/Model/Admin/DeviceRegistrationResponse.cs b/LSFE.Infrastructure/Model/Admin/DeviceRegistrationResponse.cs new file mode 100644 index 0000000..c5ce560 --- /dev/null +++ b/LSFE.Infrastructure/Model/Admin/DeviceRegistrationResponse.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Model.Admin +{ + public class DeviceRegistrationResponse + { + public bool Success { get; set; } + public string? Message { get; set; } + public string? UserId { get; set; } + public string? DeviceId { get; set; } + public DateTime RegisteredDate { get; set; } + public HttpStatusCode StatusCode { get; set; } + } +} diff --git a/LSFE.Infrastructure/Model/ExcelUploadResponse.cs b/LSFE.Infrastructure/Model/ExcelUploadResponse.cs new file mode 100644 index 0000000..317ba9d --- /dev/null +++ b/LSFE.Infrastructure/Model/ExcelUploadResponse.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Model +{ + public class ExcelUploadResponse : Response + { + public int TotalRows { get; set; } + public int ProcessedRows { get; set; } + public int SkippedRows { get; set; } + public List Errors { get; set; } = new List(); + public List InsertedItems { get; set; } = new List(); + } +} diff --git a/LSFE.Infrastructure/Model/InventorySummary.cs b/LSFE.Infrastructure/Model/InventorySummary.cs new file mode 100644 index 0000000..4ee64a5 --- /dev/null +++ b/LSFE.Infrastructure/Model/InventorySummary.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Model +{ + public class InventorySummary + { + public string? Brand { get; set; } + public int TotalQty { get; set; } + public string? MedRepName { get; set; } + } +} diff --git a/LSFE.Infrastructure/Model/Response.cs b/LSFE.Infrastructure/Model/Response.cs new file mode 100644 index 0000000..1aaf04b --- /dev/null +++ b/LSFE.Infrastructure/Model/Response.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Model +{ + public class Response + { + public object? Data { get; set; } + public bool Success { get; set; } + public string? Message { get; set; } + public byte MessCode { get; set; } + } +} diff --git a/LSFE.Infrastructure/Model/Users.cs b/LSFE.Infrastructure/Model/Users.cs new file mode 100644 index 0000000..d069d1f --- /dev/null +++ b/LSFE.Infrastructure/Model/Users.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.Infrastructure.Model +{ + public class Users + { + public string? UserName { get; set; } + public string? Password { get; set; } + } +} diff --git a/LSFE.MobApp/App.xaml b/LSFE.MobApp/App.xaml new file mode 100644 index 0000000..542181c --- /dev/null +++ b/LSFE.MobApp/App.xaml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/LSFE.MobApp/App.xaml.cs b/LSFE.MobApp/App.xaml.cs new file mode 100644 index 0000000..108076d --- /dev/null +++ b/LSFE.MobApp/App.xaml.cs @@ -0,0 +1,171 @@ +using LSFE.MobApp.Services; +using System.Diagnostics; + +namespace LSFE.MobApp +{ + public partial class App : Application + { + private readonly Services.INavigationService _navigationService; + + public App(IDatabaseService databaseService, INavigationService navigationService) + { + InitializeComponent(); + _navigationService = navigationService; + + // Initialize database in background + Task.Run(async () => await databaseService.InitializeAsync()); + + // Setup exception handlers + AppDomain.CurrentDomain.UnhandledException += (s, e) => + { + var exception = e.ExceptionObject as Exception; + var message = $"Unhandled exception: {exception?.GetType().Name}\n" + + $"Message: {exception?.Message}\n" + + $"Stack: {exception?.StackTrace}"; + Debug.WriteLine(message); + LogToFile(message); + }; + + TaskScheduler.UnobservedTaskException += (s, e) => + { + var message = $"Unobserved task exception: {e.Exception?.GetType().Name}\n" + + $"Message: {e.Exception?.Message}\n" + + $"Stack: {e.Exception?.StackTrace}\n" + + $"Inner: {e.Exception?.InnerException?.Message}"; + Debug.WriteLine(message); + LogToFile(message); + e.SetObserved(); + }; + + // Create and set MainPage + MainPage = new AppShell(_navigationService); + } + + protected override Window CreateWindow(IActivationState? activationState) + { + var window = base.CreateWindow(activationState); + + // Perform authentication check after window is created + window.Created += async (s, e) => + { + await CheckAuthenticationAsync(); + }; + + return window; + } + + private async Task CheckAuthenticationAsync() + { + try + { + // Small delay to ensure Shell is fully initialized + await Task.Delay(50); + + var token = await SecureStorage.GetAsync("auth_token"); + + if (string.IsNullOrEmpty(token) || token == "N/A") + { + // No valid token - navigate to login page + await MainThread.InvokeOnMainThreadAsync(async () => + { + await Shell.Current.GoToAsync("//LoginPage"); + }); + } + else + { + // Has valid token - navigate to main tabs + await MainThread.InvokeOnMainThreadAsync(async () => + { + await Shell.Current.GoToAsync("//MainTabs"); + }); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Auth check error: {ex.Message}"); + // On error, default to login page + await MainThread.InvokeOnMainThreadAsync(async () => + { + try + { + await Shell.Current.GoToAsync("//LoginPage"); + } + catch (Exception navEx) + { + Debug.WriteLine($"Navigation error: {navEx.Message}"); + } + }); + } + } + + private void LogToFile(string message) + { + try + { + var logPath = Path.Combine(FileSystem.AppDataDirectory, "crash_log.txt"); + var logMessage = $"\n{new string('=', 60)}\n{DateTime.Now:yyyy-MM-dd HH:mm:ss}\n{message}\n"; + File.AppendAllText(logPath, logMessage); + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to log to file: {ex.Message}"); + } + } + } +} +/*using LSFE.MobApp.Services; +using System.Diagnostics; + +namespace LSFE.MobApp +{ + public partial class App : Application + { + public App(IDatabaseService databaseService) + { + InitializeComponent(); + Task.Run(async () => await databaseService.InitializeAsync()); + + AppDomain.CurrentDomain.UnhandledException += (s, e) => + { + var exception = e.ExceptionObject as Exception; + var message = $"Unhandled exception: {exception?.GetType().Name}\n" + + $"Message: {exception?.Message}\n" + + $"Stack: {exception?.StackTrace}"; + + Debug.WriteLine(message); + LogToFile(message); + }; + + TaskScheduler.UnobservedTaskException += (s, e) => + { + var message = $"Unobserved task exception: {e.Exception?.GetType().Name}\n" + + $"Message: {e.Exception?.Message}\n" + + $"Stack: {e.Exception?.StackTrace}\n" + + $"Inner: {e.Exception?.InnerException?.Message}"; + + Debug.WriteLine(message); + LogToFile(message); + e.SetObserved(); + }; + } + + private void LogToFile(string message) + { + try + { + var logPath = Path.Combine(FileSystem.AppDataDirectory, "crash_log.txt"); + var logMessage = $"\n{'=' * 60}\n{DateTime.Now:yyyy-MM-dd HH:mm:ss}\n{message}\n"; + File.AppendAllText(logPath, logMessage); + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to log to file: {ex.Message}"); + } + } + + protected override Window CreateWindow(IActivationState? activationState) + { + return new Window(new AppShell()); + } + } +}*/ \ No newline at end of file diff --git a/LSFE.MobApp/AppShell.xaml b/LSFE.MobApp/AppShell.xaml new file mode 100644 index 0000000..d8a6275 --- /dev/null +++ b/LSFE.MobApp/AppShell.xaml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LSFE.MobApp/AppShell.xaml.cs b/LSFE.MobApp/AppShell.xaml.cs new file mode 100644 index 0000000..1d8651f --- /dev/null +++ b/LSFE.MobApp/AppShell.xaml.cs @@ -0,0 +1,33 @@ +using LSFE.MobApp.Pages.Admin; +using LSFE.MobApp.Pages.Attendance; +using LSFE.MobApp.Pages.Authentication; +using LSFE.MobApp.Pages.CoveragePlan; + +namespace LSFE.MobApp +{ + public partial class AppShell : Shell + { + private readonly Services.INavigationService _navigationService; + public AppShell(Services.INavigationService navigationService) + { + InitializeComponent(); + _navigationService = navigationService; + + // Register routes + Routing.RegisterRoute(nameof(LoginPage), typeof(LoginPage)); + Routing.RegisterRoute(nameof(ForgotPasswordPage), typeof(ForgotPasswordPage)); + Routing.RegisterRoute(nameof(DeviceRegistrationPage), typeof(DeviceRegistrationPage)); + Routing.RegisterRoute(nameof(AddEditDoctorPage), typeof(AddEditDoctorPage)); + Routing.RegisterRoute(nameof(ViewDoctorPage), typeof(ViewDoctorPage)); + Routing.RegisterRoute(nameof(MarketProductPage), typeof(MarketProductPage)); + Routing.RegisterRoute(nameof(DoctorSignaturePage), typeof(DoctorSignaturePage)); + Routing.RegisterRoute(nameof(ActualCoveragePage), typeof(ActualCoveragePage)); + Routing.RegisterRoute(nameof(AttendancePage), typeof(AttendancePage)); + } + private void OnCloseFlyout(object sender, EventArgs e) + { + Shell.Current.FlyoutIsPresented = false; + _navigationService.NavigateToLoginPageAsync(); + } + } +} \ No newline at end of file diff --git a/LSFE.MobApp/AuthShell.xaml b/LSFE.MobApp/AuthShell.xaml new file mode 100644 index 0000000..f7589af --- /dev/null +++ b/LSFE.MobApp/AuthShell.xaml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/LSFE.MobApp/AuthShell.xaml.cs b/LSFE.MobApp/AuthShell.xaml.cs new file mode 100644 index 0000000..76b4cef --- /dev/null +++ b/LSFE.MobApp/AuthShell.xaml.cs @@ -0,0 +1,15 @@ +using LSFE.MobApp.Pages.Authentication; + +namespace LSFE.MobApp +{ + public partial class AuthShell : Shell + { + public AuthShell() + { + InitializeComponent(); + + Routing.RegisterRoute(nameof(LoginPage), typeof(LoginPage)); + Routing.RegisterRoute(nameof(ForgotPasswordPage), typeof(ForgotPasswordPage)); + } + } +} \ No newline at end of file diff --git a/LSFE.MobApp/Contracts/IApiClient.cs b/LSFE.MobApp/Contracts/IApiClient.cs new file mode 100644 index 0000000..366e655 --- /dev/null +++ b/LSFE.MobApp/Contracts/IApiClient.cs @@ -0,0 +1,24 @@ +using LSFE.Infrastructure.Model.Admin; +using LSFE.MobApp.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Contracts +{ + public interface IApiClient + { + Task LoginAsync(LoginRequest entity); + Task> GetAllAsync(); + Task GetByIdAsync(int id); + Task GetByIdAsync(Guid id); + Task> GetWithQueryAsync(Dictionary queryParams); + Task> CreateAsync(T entity); + Task> UpdateAsync(Guid id, T entity); + Task> DeleteAsync(Guid id); + Task> CreateAttendanceAsync(T entity); + Task> CreateCoveragePlanAsync(T entity); + } +} \ No newline at end of file diff --git a/LSFE.MobApp/Contracts/IApiClientFactory.cs b/LSFE.MobApp/Contracts/IApiClientFactory.cs new file mode 100644 index 0000000..b07bf07 --- /dev/null +++ b/LSFE.MobApp/Contracts/IApiClientFactory.cs @@ -0,0 +1,14 @@ +using LSFE.MobApp.Contracts; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Contracts +{ + public interface IApiClientFactory + { + IApiClient CreateClient(string endpoint) where T : class; + } +} diff --git a/LSFE.MobApp/Contracts/IQueryService.cs b/LSFE.MobApp/Contracts/IQueryService.cs new file mode 100644 index 0000000..342d21d --- /dev/null +++ b/LSFE.MobApp/Contracts/IQueryService.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Contracts +{ + public interface IQueryService + { + Task> ExecuteQueryAsync(string query, params object[] parameters) where T : new(); + Task ExecuteNonQueryAsync(string query, params object[] parameters); + } +} diff --git a/LSFE.MobApp/Contracts/IRepository.cs b/LSFE.MobApp/Contracts/IRepository.cs new file mode 100644 index 0000000..de43958 --- /dev/null +++ b/LSFE.MobApp/Contracts/IRepository.cs @@ -0,0 +1,25 @@ +using LSFE.MobApp.Entities.Location; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Contracts +{ + public interface IRepository where T : class, new() + { + Task> GetAllAsync(); + Task GetByIdAsync(int id); + Task> GetActiveAsync(); + Task SaveAsync(T item); + Task UpdateAsync(T item); + Task DeleteAsync(T item); + Task DeleteAsync(int id); + Task TruncateAsync(); + Task> QueryAsync(string query, params object[] args); + Task FirstOrDefaultAsync(Expression> predicate); + Task> WhereAsync(Expression> predicate); + } +} \ No newline at end of file diff --git a/LSFE.MobApp/Contracts/ISyncService.cs b/LSFE.MobApp/Contracts/ISyncService.cs new file mode 100644 index 0000000..eed3783 --- /dev/null +++ b/LSFE.MobApp/Contracts/ISyncService.cs @@ -0,0 +1,23 @@ +using LSFE.MobApp.Entities.Account; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Contracts +{ + public interface ISyncService + { + //Pull + Task SyncPullInstitutionsAsync(); + Task SyncPullDoctorsAsync(); + Task SyncPullSamplesAsync(); + Task SyncPullProductsAsync(); + Task SyncPullUsersAsync(User users); + //Post + Task SyncPostAttendanceAsync(); + Task SyncPostCoverageTodayAsync(Guid planId); + Task SyncPullCoveragePlanTodayAsync(); + } +} diff --git a/LSFE.MobApp/Converters/BoolToColorConverter.cs b/LSFE.MobApp/Converters/BoolToColorConverter.cs new file mode 100644 index 0000000..b70e370 --- /dev/null +++ b/LSFE.MobApp/Converters/BoolToColorConverter.cs @@ -0,0 +1,74 @@ +using System; +using System.Globalization; +using Microsoft.Maui.Graphics; + +namespace LSFE.MobApp.Converters +{ + public class BoolToColorConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool isCurrentMonth) + { + return isCurrentMonth ? Color.FromArgb("#FFFFFF") : Color.FromArgb("#F5F5F5"); + } + return Color.FromArgb("#FFFFFF"); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + + public class TodayHighlightConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool isToday) + { + return isToday ? Color.FromArgb("#E0FFFF") : Colors.Transparent; + } + return Colors.Transparent; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + + public class TodayFontConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool isToday) + { + return isToday ? FontAttributes.Bold : FontAttributes.None; + } + return FontAttributes.None; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + + public class MonthTextColorConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool isCurrentMonth) + { + return isCurrentMonth ? Color.FromArgb("#008B8B") : Color.FromArgb("#B0B0B0"); + } + return Color.FromArgb("#008B8B"); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/LSFE.MobApp/Converters/BoolToHeaderHeightConverter.cs b/LSFE.MobApp/Converters/BoolToHeaderHeightConverter.cs new file mode 100644 index 0000000..0863a63 --- /dev/null +++ b/LSFE.MobApp/Converters/BoolToHeaderHeightConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Converters +{ + public class BoolToHeaderHeightConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool isVisible && isVisible) + { + return 50; // Header height when visible + } + return 0; // No height when hidden + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/LSFE.MobApp/Converters/ByteToIntConverter.cs b/LSFE.MobApp/Converters/ByteToIntConverter.cs new file mode 100644 index 0000000..bcbdc8e --- /dev/null +++ b/LSFE.MobApp/Converters/ByteToIntConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Converters +{ + public class ByteToIntConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is byte byteValue) + return (int)byteValue - 1; // Subtract 1 because picker index is 0-based + return 0; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is int intValue) + return (byte)(intValue + 1); // Add 1 because your IDs start from 1 + return (byte)1; + } + } +} diff --git a/LSFE.MobApp/Converters/FilterButtonColorConverter.cs b/LSFE.MobApp/Converters/FilterButtonColorConverter.cs new file mode 100644 index 0000000..f297f33 --- /dev/null +++ b/LSFE.MobApp/Converters/FilterButtonColorConverter.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Converters +{ + public class FilterButtonColorConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var selectedFilter = value?.ToString(); + var buttonFilter = parameter?.ToString(); + + return selectedFilter == buttonFilter ? + Color.FromArgb("#20B2AA") : + Color.FromArgb("#ADB5BD"); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/LSFE.MobApp/Converters/InitialsConverter.cs b/LSFE.MobApp/Converters/InitialsConverter.cs new file mode 100644 index 0000000..530ba8e --- /dev/null +++ b/LSFE.MobApp/Converters/InitialsConverter.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Converters +{ + public class InitialsConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is string name && !string.IsNullOrWhiteSpace(name)) + { + var words = name.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (words.Length >= 2) + { + return $"{words[0].First()}{words[1].First()}".ToUpper(); + } + else if (words.Length == 1) + { + return words[0].Length >= 2 ? + words[0].Substring(0, 2).ToUpper() : + words[0].First().ToString().ToUpper(); + } + } + return "DR"; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/LSFE.MobApp/Converters/InverseBoolConverter.cs b/LSFE.MobApp/Converters/InverseBoolConverter.cs new file mode 100644 index 0000000..deab8f2 --- /dev/null +++ b/LSFE.MobApp/Converters/InverseBoolConverter.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Converters +{ + public class InverseBoolConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool boolValue) + return !boolValue; + return false; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool boolValue) + return !boolValue; + return false; + } + } + public class IsNotNullConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value != null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + + public class IsNotNullOrEmptyConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return !string.IsNullOrEmpty(value?.ToString()); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + public class ColorToLightColorConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is Color color) + { + // Create a lighter version of the color with low opacity + return Color.FromRgba(color.Red, color.Green, color.Blue, 0.15); + } + return Colors.Gray.WithAlpha(0.15f); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/LSFE.MobApp/Converters/InverseBoolToOpacityConverter.cs b/LSFE.MobApp/Converters/InverseBoolToOpacityConverter.cs new file mode 100644 index 0000000..bc98205 --- /dev/null +++ b/LSFE.MobApp/Converters/InverseBoolToOpacityConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Converters +{ + public class InverseBoolToOpacityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool boolValue) + { + return boolValue ? 0.3 : 1.0; // If loading (true), show 30% opacity, else full opacity + } + return 1.0; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/LSFE.MobApp/Converters/StatusColorConverter.cs b/LSFE.MobApp/Converters/StatusColorConverter.cs new file mode 100644 index 0000000..9e9ee81 --- /dev/null +++ b/LSFE.MobApp/Converters/StatusColorConverter.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Converters +{ + public class StatusColorConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is byte status) + { + return status switch + { + 1 => Colors.Green, + 2 => Colors.Red, + 3 => Colors.Orange, + 4 => Colors.Gray, + _ => Colors.Gray + }; + } + return Colors.Gray; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/LSFE.MobApp/Converters/StatusTextConverter.cs b/LSFE.MobApp/Converters/StatusTextConverter.cs new file mode 100644 index 0000000..ba30e9e --- /dev/null +++ b/LSFE.MobApp/Converters/StatusTextConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Converters +{ + public class StatusTextConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool isActive) + { + return isActive ? "ACTIVE" : "INACTIVE"; + } + return "UNKNOWN"; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/LSFE.MobApp/Converters/StringToBoolConverter.cs b/LSFE.MobApp/Converters/StringToBoolConverter.cs new file mode 100644 index 0000000..fe77e46 --- /dev/null +++ b/LSFE.MobApp/Converters/StringToBoolConverter.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Converters +{ + public class StringToBoolConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return !string.IsNullOrEmpty(value?.ToString()); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } + + public class InvertedBoolConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool boolValue) + return !boolValue; + return true; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool boolValue) + return !boolValue; + return false; + } + } +} diff --git a/LSFE.MobApp/Converters/ViewModeConverter.cs b/LSFE.MobApp/Converters/ViewModeConverter.cs new file mode 100644 index 0000000..e0070a6 --- /dev/null +++ b/LSFE.MobApp/Converters/ViewModeConverter.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Converters +{ + public class ViewModeConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool isCardView) + { + return isCardView ? "📋" : "📱"; // Table icon when in card view, card icon when in table view + } + return "📋"; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/LSFE.MobApp/DTO/AddressCache.cs b/LSFE.MobApp/DTO/AddressCache.cs new file mode 100644 index 0000000..fc25d42 --- /dev/null +++ b/LSFE.MobApp/DTO/AddressCache.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.DTO +{ + public class AddressCache + { + public double Lat { get; set; } + public double Lng { get; set; } + public string? Address { get; set; } + public DateTime CachedAt { get; set; } + } +} diff --git a/LSFE.MobApp/DTO/DoctorDto/DoctorWithInstitution.cs b/LSFE.MobApp/DTO/DoctorDto/DoctorWithInstitution.cs new file mode 100644 index 0000000..8fdd8cd --- /dev/null +++ b/LSFE.MobApp/DTO/DoctorDto/DoctorWithInstitution.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.DTO.DoctorDto +{ + public class DoctorWithInstitution + { + public int DoctorId { get; set; } + public string? DoctorName { get; set; } + public string? Specialization { get; set; } + public int InstitutionId { get; set; } + public string? InstitutionName { get; set; } + public bool IsActive { get; set; } + public DateTime CreatedDate { get; set; } + public string? TerritoryName { get; set; } + } +} diff --git a/LSFE.MobApp/DTO/LocationResult.cs b/LSFE.MobApp/DTO/LocationResult.cs new file mode 100644 index 0000000..f54a91a --- /dev/null +++ b/LSFE.MobApp/DTO/LocationResult.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.DTO +{ + public class LocationResult + { + public double Latitude { get; set; } + public double Longitude { get; set; } + public string Address { get; set; } = string.Empty; + } +} diff --git a/LSFE.MobApp/DTO/MaintenanceDto/MaintenanceDto.cs b/LSFE.MobApp/DTO/MaintenanceDto/MaintenanceDto.cs new file mode 100644 index 0000000..d187f70 --- /dev/null +++ b/LSFE.MobApp/DTO/MaintenanceDto/MaintenanceDto.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.DTO.MaintenanceDto +{ + public class MaintenanceDto + { + public int InstitutionId { get; set; } + public string? InstitutionName { get; set; } + public int TerritoryId { get; set; } + public bool IsActive { get; set; } + } +} diff --git a/LSFE.MobApp/Doctor/IDoctorService.cs b/LSFE.MobApp/Doctor/IDoctorService.cs new file mode 100644 index 0000000..173a0d8 --- /dev/null +++ b/LSFE.MobApp/Doctor/IDoctorService.cs @@ -0,0 +1,20 @@ +using LSFE.MobApp.DTO.DoctorDto; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Contracts.Doctor +{ + public interface IDoctorService + { + Task> GetAllDoctorsAsync(); + Task> GetActiveDoctorsAsync(); + Task> GetDoctorsWithInstitutionsAsync(); + Task> GetDoctorsByInstitutionAsync(int institutionId); + Task> GetDoctorsBySpecializationAsync(string specialization); + Task> GetDoctorsByTerritoryAsync(int territoryId); + Task GetDoctorCountByInstitutionAsync(int institutionId); + } +} diff --git a/LSFE.MobApp/Entities/Account/Attendance.cs b/LSFE.MobApp/Entities/Account/Attendance.cs new file mode 100644 index 0000000..191fea0 --- /dev/null +++ b/LSFE.MobApp/Entities/Account/Attendance.cs @@ -0,0 +1,31 @@ +using SQLite; + +namespace LSFE.MobApp.Entities.Account +{ + [Table("Attendance")] + public class Attendance + { + [PrimaryKey] + public Guid AppAttendanceId { get; set; } + public DateTime? TimeIn { get; set; } + public DateTime? TimeOut { get; set; } + + [MaxLength(450)] + public string? UserId { get; set; } + + [MaxLength(500)] + public string? TimeInLocation { get; set; } + [MaxLength(500)] + public string? TimeOutLocation { get; set; } + + [MaxLength(450)] + public string? SignatureFileNameIn { get; set; } + [MaxLength(450)] + public string? SignatureFileNameOut { get; set; } + public byte[]? SignatureImage { get; set; } + public bool IsSync { get; set; }=false; + public bool ForAddressTranslation { get; set; } + public double Latitude { get; set; } = 0; + public double Longitude { get; set; } = 0; + } +} \ No newline at end of file diff --git a/LSFE.MobApp/Entities/Account/DSMScope.cs b/LSFE.MobApp/Entities/Account/DSMScope.cs new file mode 100644 index 0000000..04ffdb9 --- /dev/null +++ b/LSFE.MobApp/Entities/Account/DSMScope.cs @@ -0,0 +1,21 @@ +using SQLite; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Entities.Account +{ + [Table("DSMScopes")] + public class DSMScope + { + [PrimaryKey, AutoIncrement] + public int DSMScopeId { get; set; } + + public string? DSMScopeName { get; set; } + public string? UserId { get; set; } + public int DistrictId { get; set; } + public bool IsActive { get; set; } = true; + } +} diff --git a/LSFE.MobApp/Entities/Account/Inventory.cs b/LSFE.MobApp/Entities/Account/Inventory.cs new file mode 100644 index 0000000..80c919d --- /dev/null +++ b/LSFE.MobApp/Entities/Account/Inventory.cs @@ -0,0 +1,17 @@ +using SQLite; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Entities.Account +{ + [Table("Inventory")] + public class Inventory : BaseEntity + { + public int DoctorId { get; set; } + public string? Description { get; set; } + public string? UserId { get; set; } + } +} diff --git a/LSFE.MobApp/Entities/Account/MobileInfo.cs b/LSFE.MobApp/Entities/Account/MobileInfo.cs new file mode 100644 index 0000000..94aa91e --- /dev/null +++ b/LSFE.MobApp/Entities/Account/MobileInfo.cs @@ -0,0 +1,17 @@ +using SQLite; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Entities.Account +{ + [Table("MobileInfo")] + public class MobileInfo : BaseEntity + { + public string? MobileInfoName { get; set; } + public string? IMEI { get; set; } + public string? UserId { get; set; } + } +} diff --git a/LSFE.MobApp/Entities/Account/User.cs b/LSFE.MobApp/Entities/Account/User.cs new file mode 100644 index 0000000..1c8e2bd --- /dev/null +++ b/LSFE.MobApp/Entities/Account/User.cs @@ -0,0 +1,26 @@ +using SQLite; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace LSFE.MobApp.Entities.Account +{ + [Table("Users")] + public class User : BaseEntity + { + public string? UserId { get; set; } + public string? UserName { get; set; } + public string? Password { get; set; } + public string? Email { get; set; } + public string? EmployeeId { get; set; } + public string? FullName { get; set; } + public string? Company { get; set; } + public string? PhoneNumber { get; set; } + public bool LockoutEnabled { get; set; } + public bool IsDeviceRegister { get; set; } + } +} diff --git a/LSFE.MobApp/Entities/AddressCache.cs b/LSFE.MobApp/Entities/AddressCache.cs new file mode 100644 index 0000000..dc6f118 --- /dev/null +++ b/LSFE.MobApp/Entities/AddressCache.cs @@ -0,0 +1,26 @@ +using SQLite; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Entities +{ + public class AddressCache + { + [PrimaryKey, AutoIncrement] + public int Id { get; set; } + + [Indexed] + public double Latitude { get; set; } + + [Indexed] + public double Longitude { get; set; } + + [Indexed] + public DateTime CachedAt { get; set; } + + public string Address { get; set; } = string.Empty; + } +} diff --git a/LSFE.MobApp/Entities/BaseEntity.cs b/LSFE.MobApp/Entities/BaseEntity.cs new file mode 100644 index 0000000..a690781 --- /dev/null +++ b/LSFE.MobApp/Entities/BaseEntity.cs @@ -0,0 +1,19 @@ +using SQLite; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Entities +{ + public abstract class BaseEntity + { + [PrimaryKey] + public Guid Id { get; set; } = Guid.NewGuid(); + + public bool IsActive { get; set; } = true; + + public DateTime CreatedDate { get; set; } = DateTime.UtcNow; + } +} \ No newline at end of file diff --git a/LSFE.MobApp/Entities/Doctor/Doctor.cs b/LSFE.MobApp/Entities/Doctor/Doctor.cs new file mode 100644 index 0000000..a96ba1d --- /dev/null +++ b/LSFE.MobApp/Entities/Doctor/Doctor.cs @@ -0,0 +1,32 @@ +using LSFE.Infrastructure.Entities.Maintenance; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Entities.Doctor +{ + public class Doctor : BaseEntity + { + public int DoctorId { get; set; } + public string? MiddleInitial { get; set; } + public string? LastName { get; set; } + public string? FirstName { get; set; } + public byte MaxVisit { get; set; } + public string? SpecializationName { get; set; } + public DateTime BirthDate { get; set; } + public string? EmailAddress { get; set; } + public string? LicenseNo { get; set; } + public string? PhoneNo { get; set; } + public int InstitutionId { get; set; } + public string? InstitutionName { get; set; } + public byte Status { get; set; } + public bool IsTerritorial { get; set; } + public bool IsCatchUpCall { get; set; } + public string? Address { get; set; } + public string? DistrictName { get; set; } + public string? AreaName { get; set; } + public Guid MRRawPlanId { get; set; } + } +} diff --git a/LSFE.MobApp/Entities/Doctor/DoctorBackup.cs b/LSFE.MobApp/Entities/Doctor/DoctorBackup.cs new file mode 100644 index 0000000..00bc887 --- /dev/null +++ b/LSFE.MobApp/Entities/Doctor/DoctorBackup.cs @@ -0,0 +1,228 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using LSFE.MobApp.Entities.Location; +using SQLite; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Entities.Doctor +{ + [Table("DoctorBackup")] + public class DoctorBackup : SyncEntity, INotifyPropertyChanged + { + private int _doctorId; + private string? _firstName; + private string? _lastName; + private string? _specialization; + private string? _middleInitial; + private int _institutionId; + private string? _institutionName; + private string? _emailAddress; + private string? _phoneNo; + private string? _address; + private string? _licenseNo; + private bool _isActive = true; + private byte _maxVisit; + private byte _specializationId; + private byte _stageLevelId; + private byte _status; + private string? _userId; + private string? _stageLevelName; + private DateTime _createdDate = DateTime.Now; + private DateTime _birthDate = DateTime.Now; + [PrimaryKey] + public Guid Id { get; set; } = Guid.NewGuid(); + + [Column("DoctorId"), NotNull] + public int DoctorId + { + get => _doctorId; + set + { + _doctorId = value; + OnPropertyChanged(); + } + } + + public string? FirstName + { + get => _firstName; + set + { + _firstName = value; + OnPropertyChanged(); + } + } + public string? LastName + { + get => _lastName; + set + { + _lastName = value; + OnPropertyChanged(); + } + } + public string? MiddleInitial + { + get => _middleInitial; + set + { + _middleInitial = value; + OnPropertyChanged(); + } + } + public string? Specialization + { + get => _specialization; + set + { + _specialization = value; + OnPropertyChanged(); + } + } + public string? EmailAddress + { + get => _emailAddress; + set + { + _emailAddress = value; + OnPropertyChanged(); + } + } + public string? PhoneNo + { + get => _phoneNo; + set + { + _phoneNo = value; + OnPropertyChanged(); + } + } + public string? Address + { + get => _address; + set + { + _address = value; + OnPropertyChanged(); + } + } + public string? LicenseNo + { + get => _licenseNo; + set + { + _licenseNo = value; + OnPropertyChanged(); + } + } + public DateTime BirthDate + { + get => _birthDate; + set + { + _birthDate = value; + OnPropertyChanged(); + } + } + public byte MaxVisit + { + get => _maxVisit; + set + { + _maxVisit = value; + OnPropertyChanged(); + } + } + public byte SpecializationId + { + get => _specializationId; + set + { + _specializationId = value; + OnPropertyChanged(); + } + } + public byte StageLevelId + { + get => _stageLevelId; + set + { + _stageLevelId = value; + OnPropertyChanged(); + } + } + public int InstitutionId + { + get => _institutionId; + set + { + _institutionId = value; + OnPropertyChanged(); + } + } + public byte Status + { + get => _status; + set + { + _status = value; + OnPropertyChanged(); + } + } + public string? UserId + { + get => _userId; + set + { + _userId = value; + OnPropertyChanged(); + } + } + + [Ignore] + public string? StageLevelName + { + get => _stageLevelName; + set + { + _stageLevelName = value; + OnPropertyChanged(); + } + } + + + + [Ignore] // This won't be stored, just for query results + public string? InstitutionName + { + get => _institutionName; + set + { + _institutionName = value; + OnPropertyChanged(); + } + } + + public bool IsActive + { + get => _isActive; + set + { + _isActive = value; + OnPropertyChanged(); + } + } + + public DateTime CreatedDate + { + get => _createdDate; + set + { + _createdDate = value; + OnPropertyChanged(); + } + } + } +} \ No newline at end of file diff --git a/LSFE.MobApp/Entities/Doctor/Rating.cs b/LSFE.MobApp/Entities/Doctor/Rating.cs new file mode 100644 index 0000000..9eb6114 --- /dev/null +++ b/LSFE.MobApp/Entities/Doctor/Rating.cs @@ -0,0 +1,17 @@ +using SQLite; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Entities.Doctor +{ + [Table("Ratings")] + public class Rating : BaseEntity + { + public int DoctorId { get; set; } + public byte Rate { get; set; } + public string? UserId { get; set; } + } +} diff --git a/LSFE.MobApp/Entities/Doctor/Specialization.cs b/LSFE.MobApp/Entities/Doctor/Specialization.cs new file mode 100644 index 0000000..1a86b4c --- /dev/null +++ b/LSFE.MobApp/Entities/Doctor/Specialization.cs @@ -0,0 +1,19 @@ +using SQLite; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Entities.Doctor +{ + [Table("Specialization")] + public class Specialization + { + [PrimaryKey, AutoIncrement] + public byte SpecializationId { get; set; } + public byte SpecializationName { get; set; } + public bool IsActive { get; set; } = true; + } +} + diff --git a/LSFE.MobApp/Entities/Location/Area.cs b/LSFE.MobApp/Entities/Location/Area.cs new file mode 100644 index 0000000..01e8033 --- /dev/null +++ b/LSFE.MobApp/Entities/Location/Area.cs @@ -0,0 +1,16 @@ +using SQLite; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Entities.Location +{ + [Table("Areas")] + public class Area : BaseEntity + { + public int AreaId { get; set; } + public string? AreaName { get; set; } + } +} diff --git a/LSFE.MobApp/Entities/Location/District.cs b/LSFE.MobApp/Entities/Location/District.cs new file mode 100644 index 0000000..4d54c67 --- /dev/null +++ b/LSFE.MobApp/Entities/Location/District.cs @@ -0,0 +1,17 @@ +using SQLite; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Entities.Location +{ + [Table("Districts")] + public class District : BaseEntity + { + public int DistricId { get; set; } + public string? DistrictName { get; set; } + public int AreaId { get; set; } + } +} diff --git a/LSFE.MobApp/Entities/Location/Division.cs b/LSFE.MobApp/Entities/Location/Division.cs new file mode 100644 index 0000000..a7f82bf --- /dev/null +++ b/LSFE.MobApp/Entities/Location/Division.cs @@ -0,0 +1,16 @@ +using SQLite; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Entities.Location +{ + [Table("Divisions")] + public class Division : BaseEntity + { + public int DivisionId { get; set; } + public string? DivisionName { get; set; } + } +} diff --git a/LSFE.MobApp/Entities/Location/Institution.cs b/LSFE.MobApp/Entities/Location/Institution.cs new file mode 100644 index 0000000..0853c22 --- /dev/null +++ b/LSFE.MobApp/Entities/Location/Institution.cs @@ -0,0 +1,117 @@ +using SQLite; +using System.ComponentModel; +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace LSFE.MobApp.Entities.Location +{ + [Table("Institutions")] + public class Institution : SyncEntity + { + private int _institutionId; + private string? _institutionName; + private int _territoryId; + private bool _isActive = true; + private DateTime _createdDate = DateTime.UtcNow; + private ObservableCollection _doctors = new(); + private bool _isExpanded = false; + + [PrimaryKey] + public Guid Id { get; set; } = Guid.NewGuid(); + + [Column("InstitutionId"), NotNull] + public int InstitutionId + { + get => _institutionId; + set + { + _institutionId = value; + OnPropertyChanged(); + } + } + + [Column("InstitutionName")] + public string? InstitutionName + { + get => _institutionName; + set + { + _institutionName = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(Name)); + } + } + + [Column("TerritoryId"), NotNull] + public int TerritoryId + { + get => _territoryId; + set + { + _territoryId = value; + OnPropertyChanged(); + } + } + + [Column("IsActive"), NotNull] + public bool IsActive + { + get => _isActive; + set + { + _isActive = value; + OnPropertyChanged(); + } + } + + [Column("CreatedDate"), NotNull] + public DateTime CreatedDate + { + get => _createdDate; + set + { + _createdDate = value; + OnPropertyChanged(); + } + } + + // Expand/collapse property - NOT stored in database + [Ignore] + public bool IsExpanded + { + get => _isExpanded; + set + { + if (_isExpanded != value) + { + _isExpanded = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ArrowIcon)); + } + } + } + + // Computed property for arrow direction + [Ignore] + public string ArrowIcon => IsExpanded ? "▲" : "▼"; + + // For backward compatibility + [Ignore] + public string? Name + { + get => InstitutionName; + set => InstitutionName = value; + } + + [Ignore] + public ObservableCollection Doctors + { + get => _doctors; + set + { + _doctors = value; + OnPropertyChanged(); + } + } + } +} \ No newline at end of file diff --git a/LSFE.MobApp/Entities/Location/Territory.cs b/LSFE.MobApp/Entities/Location/Territory.cs new file mode 100644 index 0000000..77ca15c --- /dev/null +++ b/LSFE.MobApp/Entities/Location/Territory.cs @@ -0,0 +1,18 @@ +using SQLite; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Entities.Location +{ + [Table("Territories")] + public class Territory : BaseEntity + { + public int TerritoryId { get; set; } + + public string? TerritoryName { get; set; } + public int DistrictId { get; set; } + } +} diff --git a/LSFE.MobApp/Entities/Planning/Cycle.cs b/LSFE.MobApp/Entities/Planning/Cycle.cs new file mode 100644 index 0000000..fc32306 --- /dev/null +++ b/LSFE.MobApp/Entities/Planning/Cycle.cs @@ -0,0 +1,17 @@ +using SQLite; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Entities.Planning +{ + [Table("Cycles")] + public class Cycle : BaseEntity + { + public byte CycleId { get; set; } + + public string? CycleName { get; set; } + } +} diff --git a/LSFE.MobApp/Entities/Planning/InventoryTransaction.cs b/LSFE.MobApp/Entities/Planning/InventoryTransaction.cs new file mode 100644 index 0000000..12264c1 --- /dev/null +++ b/LSFE.MobApp/Entities/Planning/InventoryTransaction.cs @@ -0,0 +1,27 @@ +using SQLite; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Entities.Planning +{ + [Table("InventoryTransaction")] + public class InventoryTransaction : BaseEntity + { + public Guid InventoryTransId { get; set; } + public string? Description { get; set; } + public int QtyIn { get; set; } + public byte IconEmojiId { get; set; } + public Guid InventoryId { get; set; } + public Guid MRRawPlanId { get; set; } + public int DoctorId { get; set; } + public DateTime AppointmentDate { get; set; } + public int QtyOut { get; set; } + public string? UserId { get; set; } + public string? CreatedBy { get; set; } + public string? UpdatedBy { get; set; } + public DateTime? UpdatedDate { get; set; } + } +} diff --git a/LSFE.MobApp/Entities/Planning/MRPlanDetail.cs b/LSFE.MobApp/Entities/Planning/MRPlanDetail.cs new file mode 100644 index 0000000..d2f7fc0 --- /dev/null +++ b/LSFE.MobApp/Entities/Planning/MRPlanDetail.cs @@ -0,0 +1,21 @@ +using SQLite; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Entities.Planning +{ + [Table("MRPlanDetails")] + public class MRPlanDetail : BaseEntity + { + public string? MRPlanDetailId { get; set; } + public int MRPlanHeaderId { get; set; } + public byte MaxVisit { get; set; } + public int DoctorId { get; set; } + public DateTime PlanDate { get; set; } + public int TerritoryId { get; set; } + public int DivisionId { get; set; } + } +} diff --git a/LSFE.MobApp/Entities/Planning/MRPlanHeader.cs b/LSFE.MobApp/Entities/Planning/MRPlanHeader.cs new file mode 100644 index 0000000..c2ad973 --- /dev/null +++ b/LSFE.MobApp/Entities/Planning/MRPlanHeader.cs @@ -0,0 +1,16 @@ +using SQLite; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Entities.Planning +{ + [Table("MRPlanHeader")] + public class MRPlanHeader : BaseEntity + { + public int MRPlanHeaderId { get; set; } + public string? MRUserId { get; set; } + } +} diff --git a/LSFE.MobApp/Entities/Planning/MRRawPlan.cs b/LSFE.MobApp/Entities/Planning/MRRawPlan.cs new file mode 100644 index 0000000..c78a87c --- /dev/null +++ b/LSFE.MobApp/Entities/Planning/MRRawPlan.cs @@ -0,0 +1,46 @@ +using SQLite; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Metadata; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Entities.Planning +{ + [Table("MRRawPlans")] + public class MRRawPlan : BaseEntity + { + public byte Status { get; set; } + public Guid MRRawPlanId { get; set; } + public Guid? MRPlanDetailId { get; set; } + public int? DoctorId { get; set; } + public byte? VisitCount { get; set; } + public DateTime? CallDate { get; set; } + public DateTime? ActualCallDate { get; set; } + public DateTime? StartTime { get; set; } + public DateTime? EndTime { get; set; } + public string? Longitude { get; set; } + public string? Latitude { get; set; } + public string? Location { get; set; } + public bool? IsSignature { get; set; } + public string? SignatureFileName { get; set; } + public bool? IsMissed { get; set; } + public string? MissedReason { get; set; } + public bool? IsDoctorProxy { get; set; } = false; + public bool? IsReschedule { get; set; } = false; + public string? RescheduleReason { get; set; } + public string? CallType { get; set; } + public bool? IsJoinCall { get; set; } = false; + public byte? CycleId { get; set; } + public bool? IsApproved { get; set; } = false; + public bool? IsRecovered { get; set; } = false; + public bool IsCatchUpCall { get; set; } = false; + public bool IsSync { get; set; } = false; + public bool ForAddressTranslation { get; set; } = false; + public string? SLP { get; set; } + public string? ApprovedBy { get; set; } + public DateTime? ApprovedDate { get; set; } + public DateTime LastSyncDate { get; internal set; } + } +} diff --git a/LSFE.MobApp/Entities/Planning/Product.cs b/LSFE.MobApp/Entities/Planning/Product.cs new file mode 100644 index 0000000..cf43734 --- /dev/null +++ b/LSFE.MobApp/Entities/Planning/Product.cs @@ -0,0 +1,18 @@ +using SQLite; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Entities.Planning +{ + [Table("Products")] + public class Product : BaseEntity + { + public int ProductId { get; set; } + public string? ProductName { get; set; } + public string? ProductLink { get; set; } + public string? UserId { get; set; } + } +} diff --git a/LSFE.MobApp/Entities/Planning/ProductTransaction.cs b/LSFE.MobApp/Entities/Planning/ProductTransaction.cs new file mode 100644 index 0000000..1f03d3a --- /dev/null +++ b/LSFE.MobApp/Entities/Planning/ProductTransaction.cs @@ -0,0 +1,23 @@ +using SQLite; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Entities.Planning +{ + [Table("ProductTransaction")] + public class ProductTransaction : BaseEntity + { + public Guid ProductTransId { get; set; } + public int ProductId { get; set; } + public string? ProductName { get; set; } + public int DoctorId { get; set; } + public Guid MRRawPlanId { get; set; } + public DateTime AppointmentDate { get; set; } + public string? CreatedBy { get; set; } + public string? UserId { get; set; } + public string? ProductLink { get; set; } + } +} diff --git a/LSFE.MobApp/Entities/SyncEntity.cs b/LSFE.MobApp/Entities/SyncEntity.cs new file mode 100644 index 0000000..c083a50 --- /dev/null +++ b/LSFE.MobApp/Entities/SyncEntity.cs @@ -0,0 +1,42 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using SQLite; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Entities +{ + public partial class SyncEntity : ObservableObject + { + private DateTime _lastSyncedAt = DateTime.MinValue; + private bool _needsSync = false; + + // Sync tracking properties + [Column("LastSyncedAt")] + public DateTime LastSyncedAt + { + get => _lastSyncedAt; + set + { + _lastSyncedAt = value; + OnPropertyChanged(); + } + } + + [Column("NeedsSync"), NotNull] + public bool NeedsSync + { + get => _needsSync; + set + { + _needsSync = value; + OnPropertyChanged(); + } + } + } +} \ No newline at end of file diff --git a/LSFE.MobApp/GlobalXmlns.cs b/LSFE.MobApp/GlobalXmlns.cs new file mode 100644 index 0000000..4f581bf --- /dev/null +++ b/LSFE.MobApp/GlobalXmlns.cs @@ -0,0 +1,2 @@ +[assembly: XmlnsDefinition("http://schemas.microsoft.com/dotnet/maui/global", "LSFE.MobApp")] +[assembly: XmlnsDefinition("http://schemas.microsoft.com/dotnet/maui/global", "LSFE.MobApp.Pages")] diff --git a/LSFE.MobApp/Helpers/DeviceHelper.cs b/LSFE.MobApp/Helpers/DeviceHelper.cs new file mode 100644 index 0000000..6453724 --- /dev/null +++ b/LSFE.MobApp/Helpers/DeviceHelper.cs @@ -0,0 +1,88 @@ +using Microsoft.Maui.Devices.Sensors; +using Microsoft.Maui.ApplicationModel; +using Microsoft.Extensions.Logging; +using LSFE.MobApp.Services; + +namespace LSFE.MobApp.Helpers +{ + public class DeviceHelper + { + private readonly ILogger _logger; + + public DeviceHelper(ILogger logger) + { + _logger = logger; + } + //public async Task<(double Latitude, double Longitude, string Address)> + //GetCurrentLocationAsync(AddressService addressService) + //{ + // var location = await Geolocation.Default.GetLocationAsync( + // new GeolocationRequest(GeolocationAccuracy.High, TimeSpan.FromSeconds(30))); + + // if (location == null) + // throw new Exception("Unable to get location."); + + // var address = await addressService.GetAddressAsync( + // location.Latitude, + // location.Longitude); + + // return (location.Latitude, location.Longitude, address); + //} + + /*public async Task<(double Latitude, double Longitude, string Address)> GetCurrentLocationAsync() + { + try + { + Location location = await Geolocation.Default.GetLastKnownLocationAsync(); + + if (location == null) + { + location = await Geolocation.Default.GetLocationAsync(new GeolocationRequest + { + DesiredAccuracy = GeolocationAccuracy.High, + Timeout = TimeSpan.FromSeconds(30) + }); + } + + if (location == null) + throw new Exception("Unable to get GPS coordinates."); + + double lat = location.Latitude; + double lng = location.Longitude; + string address = $"Lat: {lat:F6}, Long: {lng:F6}"; + + try + { + // Reverse geocoding (requires internet) + var placemarks = await Geocoding.Default.GetPlacemarksAsync(lat, lng); + var placemark = placemarks?.FirstOrDefault(); + + if (placemark != null) + address = $"{placemark.Thoroughfare}, {placemark.Locality}, {placemark.AdminArea}, {placemark.CountryName}"; + } + catch (Exception geoEx) + { + _logger.LogWarning(geoEx, "Reverse geocoding failed (possibly offline)."); + // fallback to raw coordinates + } + + return (lat, lng, address); + } + catch (FeatureNotEnabledException) + { + _logger.LogError("Location services disabled."); + throw new Exception("Please enable GPS/location services."); + } + catch (PermissionException) + { + _logger.LogError("Location permission denied."); + throw new Exception("Location permission is required to continue."); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving location."); + throw; + } + }*/ + } +} \ No newline at end of file diff --git a/LSFE.MobApp/Helpers/DoctorStatusHelper.cs b/LSFE.MobApp/Helpers/DoctorStatusHelper.cs new file mode 100644 index 0000000..442beb4 --- /dev/null +++ b/LSFE.MobApp/Helpers/DoctorStatusHelper.cs @@ -0,0 +1,90 @@ +using CommunityToolkit.Mvvm.Messaging; +using CommunityToolkit.Mvvm.Messaging.Messages; + +namespace LSFE.MobApp.Helpers; + +/// +/// Centralized helper for doctor status styling and updates +/// +public static class DoctorStatusHelper +{ + #region Status Text + public static string GetStatusText(byte status) + { + return status switch + { + 0 => "Pending", + 1 => "Started", + 2 => "Completed", + 3 => "Reschedule", + 4 => "Missed", + _ => "Unknown" + }; + } + #endregion + + #region Status Colors + public static Color GetStatusBackgroundColor(byte status) + { + return status switch + { + 0 => Color.FromArgb("#FEF3C7"), // Pending - soft amber + 1 => Color.FromArgb("#D1FAE5"), // Started - mint green + 2 => Color.FromArgb("#DBEAFE"), // Completed - soft blue + 3 => Color.FromArgb("#FDE68A"), // Rescheduled - soft yellow + 4 => Color.FromArgb("#FECACA"), // Missed - gentle red + _ => Color.FromArgb("#E5E7EB") // Default - gray + }; + } + + public static Color GetStatusTextColor(byte status) + { + return status switch + { + 0 => Color.FromArgb("#92400E"), // Pending - amber dark + 1 => Color.FromArgb("#065F46"), // Started - deep green + 2 => Color.FromArgb("#1E40AF"), // Completed - deep blue + 3 => Color.FromArgb("#92400E"), // Rescheduled - amber dark + 4 => Color.FromArgb("#B91C1C"), // Missed - deep red + _ => Color.FromArgb("#374151") // Default - charcoal + }; + } + #endregion + + #region Status Update Broadcasting + /// + /// Broadcast status update to all listening ViewModels + /// + public static void BroadcastStatusUpdate(int doctorId) + { + WeakReferenceMessenger.Default.Send(new DoctorStatusUpdatedMessage(doctorId)); + } + + /// + /// Register a ViewModel to listen for status updates + /// + public static void RegisterForStatusUpdates(object recipient, Action onStatusUpdate) + { + WeakReferenceMessenger.Default.Register(recipient, (r, message) => + { + onStatusUpdate(message.Value); + }); + } + + /// + /// Unregister a ViewModel from status updates + /// + public static void UnregisterFromStatusUpdates(object recipient) + { + WeakReferenceMessenger.Default.Unregister(recipient); + } + #endregion +} + +/// +/// Message for broadcasting doctor status updates +/// +public class DoctorStatusUpdatedMessage : ValueChangedMessage +{ + public DoctorStatusUpdatedMessage(int doctorId) : base(doctorId) { } +} \ No newline at end of file diff --git a/LSFE.MobApp/Helpers/ServiceHelper.cs b/LSFE.MobApp/Helpers/ServiceHelper.cs new file mode 100644 index 0000000..53426c0 --- /dev/null +++ b/LSFE.MobApp/Helpers/ServiceHelper.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Helpers +{ + public static class ServiceHelper + { + public static T GetService() => Current.GetService(); + public static IServiceProvider Current => + Application.Current.Handler.MauiContext.Services; + } +} diff --git a/LSFE.MobApp/LSFE.MobApp.csproj b/LSFE.MobApp/LSFE.MobApp.csproj new file mode 100644 index 0000000..34bc30c --- /dev/null +++ b/LSFE.MobApp/LSFE.MobApp.csproj @@ -0,0 +1,151 @@ + + + + net9.0-android;net9.0-ios;net9.0-maccatalyst + $(TargetFrameworks);net9.0-windows10.0.19041.0 + + + + + + + Exe + LSFE.MobApp + true + true + enable + enable + + + LSFE.MobApp + + + com.companyname.lsfe.mobapp + + + 1.0 + 1 + + + None + + 15.0 + 15.0 + 21.0 + 10.0.17763.0 + 10.0.17763.0 + 6.5 + + + + apk + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + + + + + + + + + + + + + + + + + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + MSBuild:Compile + + + + + + Always + + + + diff --git a/LSFE.MobApp/MainPage.xaml b/LSFE.MobApp/MainPage.xaml new file mode 100644 index 0000000..c24bed8 --- /dev/null +++ b/LSFE.MobApp/MainPage.xaml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/LSFE.MobApp/MainPage.xaml.cs b/LSFE.MobApp/MainPage.xaml.cs new file mode 100644 index 0000000..38b5799 --- /dev/null +++ b/LSFE.MobApp/MainPage.xaml.cs @@ -0,0 +1,12 @@ +namespace LSFE.MobApp +{ + public partial class MainPage : ContentPage + { + int count = 0; + + public MainPage() + { + InitializeComponent(); + } + } +} diff --git a/LSFE.MobApp/Mapper/ApiEntityMapper.cs b/LSFE.MobApp/Mapper/ApiEntityMapper.cs new file mode 100644 index 0000000..7212c49 --- /dev/null +++ b/LSFE.MobApp/Mapper/ApiEntityMapper.cs @@ -0,0 +1,312 @@ +using LSFE.Infrastructure.Dto.Maintenance; +using LSFE.Infrastructure.Dto.Planning; +using LSFE.Infrastructure.Entities; +using LSFE.Infrastructure.Entities.Doctor; +using LSFE.Infrastructure.Entities.Maintenance; +using LSFE.Infrastructure.Entities.Planning; +using LSFE.Infrastructure.Entities.TimeKeeping; +using LSFE.MobApp.Entities.Location; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Mapper +{ + public static class ApiEntityMapper + { + #region Get Mapping + public static Entities.Doctor.Doctor MapDoctorFromApi(Entities.Doctor.Doctor apiEntity) + { + return new Entities.Doctor.Doctor + { + Id=Guid.NewGuid(), + DoctorId = apiEntity.DoctorId, + FirstName = apiEntity.FirstName, + LastName = apiEntity.LastName, + MiddleInitial = apiEntity.MiddleInitial, + MaxVisit = apiEntity.MaxVisit, + SpecializationName = apiEntity.SpecializationName, + PhoneNo = apiEntity.PhoneNo, + BirthDate = apiEntity.BirthDate, + EmailAddress = apiEntity.EmailAddress, + LicenseNo = apiEntity.LicenseNo, + InstitutionId = apiEntity.InstitutionId, + InstitutionName = apiEntity.InstitutionName, + Address = apiEntity.Address, + Status = apiEntity.Status, + IsTerritorial = apiEntity.IsTerritorial, + DistrictName = apiEntity.DistrictName, + MRRawPlanId = apiEntity.MRRawPlanId, + IsCatchUpCall = apiEntity.IsCatchUpCall, + CreatedDate =DateTime.Now + }; + } + public static Institution MapInstutionFromApi(Institutions apiEntity) + { + return new Institution + { + Id = Guid.NewGuid(), // Generate new GUID for local unique ID + InstitutionId = apiEntity.InstitutionId, // Store the server ID + InstitutionName = apiEntity.InstitutionName, + TerritoryId = apiEntity.TerritoryId, + IsActive = apiEntity.IsActive, + LastSyncedAt = DateTime.UtcNow, + NeedsSync = false, + CreatedDate = DateTime.UtcNow + }; + } + public static Entities.Account.Attendance MapAttendanceFromApi(Attendance apiEntity) + { + return new Entities.Account.Attendance + { + AppAttendanceId = Guid.Parse(apiEntity.AppAttendanceId), + TimeIn = apiEntity.TimeIn, + TimeOut = apiEntity.TimeOut, + UserId = apiEntity.UserId, + TimeInLocation = apiEntity.TimeInLocation, + TimeOutLocation = apiEntity.TimeOutLocation, + SignatureFileNameIn = apiEntity.SignatureFileNameIn, + SignatureFileNameOut= apiEntity.SignatureFileNameOut + }; + } + + public static Entities.Planning.ProductTransaction MapProductTransFromApi(ProductTransactionDto apiEntity) + { + return new Entities.Planning.ProductTransaction + { + Id = Guid.NewGuid(), + ProductTransId=apiEntity.ProductTransId, + ProductId = apiEntity.ProductId, + DoctorId = apiEntity.DoctorId, + AppointmentDate = apiEntity.AppointmentDate.HasValue + ? apiEntity.AppointmentDate.Value.ToDateTime(TimeOnly.MinValue) + : default, + ProductName = apiEntity.ProductName, + ProductLink = apiEntity.ProductLink, + UserId = apiEntity.UserId, + IsActive = apiEntity.IsActive + }; + } + public static Entities.Planning.Product MapProductFromApi(Products apiEntity) + { + return new Entities.Planning.Product + { + Id = Guid.NewGuid(), + ProductId = apiEntity.ProductId, + ProductName = apiEntity.ProductName, + ProductLink = apiEntity.ProductLink, + UserId = apiEntity.UserId, + IsActive = apiEntity.IsActive + }; + } + public static Entities.Planning.InventoryTransaction MapInventoryTransFromApi(InventoryTransaction apiEntity) + { + return new Entities.Planning.InventoryTransaction + { + Id = Guid.NewGuid(), + InventoryTransId = apiEntity.InventoryTransId, + InventoryId = apiEntity.InventoryId, + Description = apiEntity.Description, + DoctorId= apiEntity.DoctorId, + MRRawPlanId= apiEntity.MRRawPlanId, + AppointmentDate = apiEntity.AppointmentDate.HasValue + ? apiEntity.AppointmentDate.Value.ToDateTime(TimeOnly.MinValue) + : default, + QtyIn = apiEntity.QtyIn, + UserId = apiEntity.UserId, + IconEmojiId = apiEntity.IconEmojiId, + CreatedBy = apiEntity.CreatedBy, + UpdatedDate= apiEntity.UpdatedDate, + UpdatedBy= apiEntity.UpdatedBy, + CreatedDate= apiEntity.CreatedDate, + IsActive = apiEntity.IsActive + }; + } + public static Entities.Planning.MRRawPlan MapMRRawPlanFromApi(MRRawPlansDto apiEntity) + { + return new Entities.Planning.MRRawPlan + { + Id = Guid.NewGuid(), + MRRawPlanId = apiEntity.MRRawPlanId, + MRPlanDetailId = apiEntity.MRPlanDetailId, + DoctorId = apiEntity.DoctorId, + VisitCount = apiEntity.VisitCount, + CallDate = apiEntity.CallDate.HasValue + ? apiEntity.CallDate.Value.ToDateTime(TimeOnly.MinValue) + : default, + ActualCallDate = apiEntity.ActualCallDate.HasValue + ? apiEntity.ActualCallDate.Value.ToDateTime(TimeOnly.MinValue) + : default, + StartTime = apiEntity.StartTime, + EndTime = apiEntity.EndTime, + Longitude = apiEntity.Longitude, + Latitude = apiEntity.Latitude, + Location = apiEntity.Location, + IsSignature = apiEntity.IsSignature, + SignatureFileName = apiEntity.SignatureFileName, + IsMissed = apiEntity.IsMissed, + MissedReason = apiEntity.MissedReason, + IsDoctorProxy = apiEntity.IsDoctorProxy, + IsReschedule = apiEntity.IsReschedule, + RescheduleReason = apiEntity.RescheduleReason, + CallType = apiEntity.CallType, + IsJoinCall = apiEntity.IsJoinCall, + IsCatchUpCall = apiEntity.IsCatchUpCall, + CycleId = apiEntity.CycleId, + IsApproved = apiEntity.IsApproved, + IsRecovered = apiEntity.IsRecovered, + SLP = apiEntity.SLP, + Status= apiEntity.Status, + ApprovedBy = apiEntity.ApprovedBy, + ApprovedDate = apiEntity.ApprovedDate + }; + } + public static Entities.Account.User MapUserFromApi(AppsUsers apiEntity) + { + return new Entities.Account.User + { + Id = Guid.NewGuid(), + UserId = apiEntity.Id, + UserName = apiEntity.UserName, + FullName = apiEntity.FullName, + Password = apiEntity.PasswordHash, + Email = apiEntity.Email ?? "N/A", + PhoneNumber = apiEntity.PhoneNumber ?? "N/A", + EmployeeId = apiEntity.EmployeeId, + Company = apiEntity.Company ?? "N/A", + LockoutEnabled = apiEntity.LockoutEnabled + }; + } + #endregion + + #region Update Mapping + public static void UpdateUserFromApi(Entities.Account.User existing, + AppsUsers apiEntity) + { + existing.UserId = apiEntity.Id; + existing.UserName = apiEntity.UserName; + existing.FullName = apiEntity.FullName; + existing.Password = apiEntity.PasswordHash; + existing.Email = apiEntity.Email ?? "N/A"; + existing.PhoneNumber = apiEntity.PhoneNumber ?? "N/A"; + existing.EmployeeId = apiEntity.EmployeeId; + existing.Company = apiEntity.Company ?? "N/A"; + existing.LockoutEnabled = apiEntity.LockoutEnabled; + } + public static void UpdateMRRawPlanFromApi(Entities.Planning.MRRawPlan existing, + MRRawPlansDto apiEntity) + { + existing.MRRawPlanId = apiEntity.MRRawPlanId; + existing.MRPlanDetailId = apiEntity.MRPlanDetailId; + existing.DoctorId = apiEntity.DoctorId; + existing.VisitCount = apiEntity.VisitCount; + existing.CallDate = apiEntity.CallDate.HasValue + ? apiEntity.CallDate.Value.ToDateTime(TimeOnly.MinValue) + : default; + existing.ActualCallDate = apiEntity.ActualCallDate.HasValue + ? apiEntity.ActualCallDate.Value.ToDateTime(TimeOnly.MinValue) + : default; + existing.StartTime = apiEntity.StartTime; + existing.EndTime = apiEntity.EndTime; + existing.Longitude = apiEntity.Longitude; + existing.Latitude = apiEntity.Latitude; + existing.Location = apiEntity.Location; + existing.IsSignature = apiEntity.IsSignature; + existing.SignatureFileName = apiEntity.SignatureFileName; + existing.IsMissed = apiEntity.IsMissed; + existing.MissedReason = apiEntity.MissedReason; + existing.IsDoctorProxy = apiEntity.IsDoctorProxy; + existing.IsReschedule = apiEntity.IsReschedule; + existing.RescheduleReason = apiEntity.RescheduleReason; + existing.CallType = apiEntity.CallType; + existing.IsJoinCall = apiEntity.IsJoinCall; + existing.CycleId = apiEntity.CycleId; + existing.IsApproved = apiEntity.IsApproved; + existing.IsRecovered = apiEntity.IsRecovered; + existing.SLP = apiEntity.SLP; + existing.ApprovedBy = apiEntity.ApprovedBy; + existing.ApprovedDate = apiEntity.ApprovedDate; + existing.IsCatchUpCall = apiEntity.IsCatchUpCall; + existing.Status = apiEntity.Status; + existing.ForAddressTranslation = false; + } + public static void UpdateInventoryTransFromApi(Entities.Planning.InventoryTransaction existing, + InventoryTransaction apiEntity) + { + existing.InventoryTransId = apiEntity.InventoryTransId; + existing.InventoryId = apiEntity.InventoryId; + existing.Description = apiEntity.Description; + existing.DoctorId = apiEntity.DoctorId; + existing.MRRawPlanId = apiEntity.MRRawPlanId; + + // Convert DateOnly to DateTime + existing.AppointmentDate = apiEntity.AppointmentDate.HasValue + ? apiEntity.AppointmentDate.Value.ToDateTime(TimeOnly.MinValue) + : default; + + existing.QtyIn = apiEntity.QtyIn; + existing.UserId = apiEntity.UserId; + existing.CreatedBy = apiEntity.CreatedBy; + existing.UpdatedDate = apiEntity.UpdatedDate; + existing.UpdatedBy = apiEntity.UpdatedBy; + existing.CreatedDate = apiEntity.CreatedDate; + existing.IconEmojiId = apiEntity.IconEmojiId; + existing.IsActive = apiEntity.IsActive; + } + public static void UpdateProductTransFromApi(Entities.Planning.ProductTransaction existing, + ProductTransactionDto apiEntity) + { + existing.ProductTransId = apiEntity.ProductTransId; + existing.ProductId = apiEntity.ProductId; + existing.ProductName = apiEntity.ProductName; + existing.AppointmentDate =apiEntity.AppointmentDate.HasValue + ? apiEntity.AppointmentDate.Value.ToDateTime(TimeOnly.MinValue) + : default; + existing.DoctorId = apiEntity.DoctorId; + existing.ProductLink = apiEntity.ProductLink; + existing.UserId = apiEntity.UserId; + existing.IsActive = apiEntity.IsActive; + } + public static void UpdateProductFromApi(Entities.Planning.Product existing, + Products apiEntity) + { + existing.ProductId = apiEntity.ProductId; + existing.ProductName = apiEntity.ProductName; + existing.ProductLink = apiEntity.ProductLink; + existing.UserId = apiEntity.UserId; + existing.IsActive = apiEntity.IsActive; + } + public static void UpdateDoctorFromApi(Entities.Doctor.Doctor existing, + Entities.Doctor.Doctor apiEntity) + { + existing.DoctorId = apiEntity.DoctorId; + existing.FirstName = apiEntity.FirstName; + existing.LastName = apiEntity.LastName; + existing.MiddleInitial = apiEntity.MiddleInitial; + existing.MaxVisit = apiEntity.MaxVisit; + existing.SpecializationName = apiEntity.SpecializationName; + existing.PhoneNo = apiEntity.PhoneNo; + existing.BirthDate = apiEntity.BirthDate; + existing.EmailAddress = apiEntity.EmailAddress; + existing.LicenseNo = apiEntity.LicenseNo; + existing.InstitutionId = apiEntity.InstitutionId; + existing.InstitutionName = apiEntity.InstitutionName; + existing.Address = apiEntity.Address; + existing.Status = apiEntity.Status; + existing.IsTerritorial = apiEntity.IsTerritorial; + existing.DistrictName = apiEntity.DistrictName; + existing.MRRawPlanId = apiEntity.MRRawPlanId; + existing.IsCatchUpCall = apiEntity.IsCatchUpCall; + existing.CreatedDate = DateTime.Now; + } + public static void UpdateInstutionFromApi(Institution existing, + Institutions apiEntity) + { + existing.InstitutionId = apiEntity.InstitutionId; + existing.InstitutionName = apiEntity.InstitutionName; + existing.TerritoryId = apiEntity.TerritoryId; + existing.IsActive = apiEntity.IsActive; + existing.LastSyncedAt = DateTime.UtcNow; + existing.NeedsSync = false; + } + #endregion + } +} diff --git a/LSFE.MobApp/MauiProgram.cs b/LSFE.MobApp/MauiProgram.cs new file mode 100644 index 0000000..0304740 --- /dev/null +++ b/LSFE.MobApp/MauiProgram.cs @@ -0,0 +1,448 @@ +using LSFE.MobApp.Contracts; +using LSFE.MobApp.Contracts.Doctor; +using LSFE.MobApp.Entities; +using LSFE.MobApp.Entities.Account; +using LSFE.MobApp.Entities.Doctor; +using LSFE.MobApp.Entities.Location; +using LSFE.MobApp.Entities.Planning; +using LSFE.MobApp.Helpers; +using LSFE.MobApp.Pages.Admin; +using LSFE.MobApp.Pages.Attendance; +using LSFE.MobApp.Pages.Authentication; +using LSFE.MobApp.Pages.CoveragePlan; +using LSFE.MobApp.Pages.Utilities; +using LSFE.MobApp.Services; +using LSFE.MobApp.Services.Coverage; +using LSFE.MobApp.Services.Doctor; +using LSFE.MobApp.ViewModels; +using LSFE.MobApp.ViewModels.Admin; +using LSFE.MobApp.ViewModels.Authentication; +using LSFE.MobApp.ViewModels.Coverage; +using LSFE.MobApp.ViewModels.Utility; +using LSFE.MobApp.Views.CoveragePlan; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using System.Reflection; + +namespace LSFE.MobApp +{ + public static class MauiProgram + { + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .ConfigureFonts(fonts => + { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); + fonts.AddFont("Font-Awesome-7-Free-Solid-900.otf", "FontAwesome"); + fonts.AddFont("materialdesignicons-webfont.ttf", "MaterialIconsRegular"); + }); + // Register logging + builder.Services.AddLogging(); +#if DEBUG + builder.Logging.AddDebug(); +#endif + // Add HttpClient + builder.Services.AddHttpClient("ApiClient") + .ConfigurePrimaryHttpMessageHandler(() => + { +#if DEBUG + return new HttpClientHandler + { + ServerCertificateCustomValidationCallback = + (message, cert, chain, errors) => true + }; +#else + return new HttpClientHandler + { + ServerCertificateCustomValidationCallback = + (message, cert, chain, errors) => true + }; +#endif + }); + builder.Services.AddHttpClient(); + // Add API and sync services + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(provider => + { + var apiClientFactory = provider.GetService(); + var addressService = provider.GetService(); + + var institutionRepo = provider.GetService>(); + var doctorRepo = provider.GetService>(); + var attendanceRepo = provider.GetService>(); + var productRepo = provider.GetService>(); + var inventoryTransactionRepo = provider.GetService>(); + var mrRawPlanRepo = provider.GetService>(); + var userRepo = provider.GetService>(); + + var configuration = provider.GetService(); + var fileDownloadService = provider.GetService(); + + // Create wrappers + var institutionWrapper = new RepositoryWrapper(institutionRepo); + var doctorWrapper = new RepositoryWrapper(doctorRepo); + var attendanceWrapper = new RepositoryWrapper(attendanceRepo); + var productWrapper = new RepositoryWrapper(productRepo); + var inventoryWrapper = new RepositoryWrapper(inventoryTransactionRepo); + var mrRawPlanWrapper = new RepositoryWrapper(mrRawPlanRepo); + var userWrapper = new RepositoryWrapper(userRepo); + + return new SyncService(apiClientFactory, institutionWrapper, doctorWrapper, + attendanceWrapper, productWrapper, inventoryWrapper, mrRawPlanWrapper, userWrapper, + configuration,fileDownloadService, addressService); + }); + + + // Load appsettings.json from EmbeddedResource + var assembly = Assembly.GetExecutingAssembly(); + using var stream = assembly.GetManifestResourceStream("LSFE.MobApp.appsettings.json"); + + var configuration = new ConfigurationBuilder() + .AddJsonStream(stream) + .Build(); + + #region Register IConfiguration for DI + builder.Configuration.AddConfiguration(configuration); + builder.Services.AddSingleton(configuration); + + // Register database service + builder.Services.AddSingleton(); + // Register file download service + builder.Services.AddSingleton(); + #endregion + + #region Register IRepository wrappers dependencies + builder.Services.AddTransient(provider => + { + var databaseService = provider.GetRequiredService(); + var logger = provider.GetRequiredService>>(); + return new Repository(databaseService.Database, logger); + }); + + builder.Services.AddTransient(provider => + { + var databaseService = provider.GetRequiredService(); + var logger = provider.GetRequiredService>>(); + return new Repository(databaseService.Database, logger); + }); + + builder.Services.AddTransient(provider => + { + var databaseService = provider.GetRequiredService(); + var logger = provider.GetRequiredService>>(); + return new Repository(databaseService.Database, logger); + }); + + builder.Services.AddTransient(provider => + { + var databaseService = provider.GetRequiredService(); + var logger = provider.GetRequiredService>>(); + return new Repository(databaseService.Database, logger); + }); + + builder.Services.AddTransient(provider => + { + var databaseService = provider.GetRequiredService(); + var logger = provider.GetRequiredService>>(); + return new Repository(databaseService.Database, logger); + }); + + builder.Services.AddTransient(provider => + { + var databaseService = provider.GetRequiredService(); + var logger = provider.GetRequiredService>>(); + return new Repository(databaseService.Database, logger); + }); + + builder.Services.AddTransient(provider => + { + var databaseService = provider.GetRequiredService(); + var logger = provider.GetRequiredService>>(); + return new Repository(databaseService.Database, logger); + }); + + builder.Services.AddTransient(provider => + { + var databaseService = provider.GetRequiredService(); + var logger = provider.GetRequiredService>>(); + return new Repository(databaseService.Database, logger); + }); + + builder.Services.AddTransient(provider => + { + var databaseService = provider.GetRequiredService(); + var logger = provider.GetRequiredService>>(); + return new Repository(databaseService.Database, logger); + }); + + builder.Services.AddTransient(provider => + { + var databaseService = provider.GetRequiredService(); + var logger = provider.GetRequiredService>>(); + return new Repository(databaseService.Database, logger); + }); + + builder.Services.AddTransient(provider => + { + var databaseService = provider.GetRequiredService(); + var logger = provider.GetRequiredService>>(); + return new Repository(databaseService.Database, logger); + }); + + builder.Services.AddTransient(provider => + { + var databaseService = provider.GetRequiredService(); + var logger = provider.GetRequiredService>>(); + return new Repository(databaseService.Database, logger); + }); + + builder.Services.AddTransient(provider => + { + var databaseService = provider.GetRequiredService(); + var logger = provider.GetRequiredService>>(); + return new Repository(databaseService.Database, logger); + }); + + builder.Services.AddTransient(provider => + { + var databaseService = provider.GetRequiredService(); + var logger = provider.GetRequiredService>>(); + return new Repository(databaseService.Database, logger); + }); + + builder.Services.AddTransient(provider => + { + var databaseService = provider.GetRequiredService(); + var logger = provider.GetRequiredService>>(); + return new Repository(databaseService.Database, logger); + }); + + builder.Services.AddTransient(provider => + { + var databaseService = provider.GetRequiredService(); + var logger = provider.GetRequiredService>>(); + return new Repository(databaseService.Database, logger); + }); + + builder.Services.AddTransient(provider => + { + var databaseService = provider.GetRequiredService(); + var logger = provider.GetRequiredService>>(); + return new Repository(databaseService.Database, logger); + }); + + builder.Services.AddTransient(provider => + { + var databaseService = provider.GetRequiredService(); + var logger = provider.GetRequiredService>>(); + return new Repository(databaseService.Database, logger); + }); + + builder.Services.AddTransient(provider => + { + var databaseService = provider.GetRequiredService(); + var logger = provider.GetRequiredService>>(); + return new Repository(databaseService.Database, logger); + }); + + builder.Services.AddTransient(provider => + { + var databaseService = provider.GetRequiredService(); + var logger = provider.GetRequiredService>>(); + return new Repository(databaseService.Database, logger); + }); + + builder.Services.AddTransient(provider => + { + var databaseService = provider.GetRequiredService(); + var logger = provider.GetRequiredService>>(); + return new Repository(databaseService.Database, logger); + }); + + builder.Services.AddTransient>(provider => + { + var repo = provider.GetRequiredService>(); + return new RepositoryWrapper(repo); + }); + + builder.Services.AddTransient>(provider => + { + var repo = provider.GetRequiredService>(); + return new RepositoryWrapper(repo); + }); + builder.Services.AddTransient>(provider => + { + var repo = provider.GetRequiredService>(); + return new RepositoryWrapper(repo); + }); + + builder.Services.AddTransient>(provider => + { + var repo = provider.GetRequiredService>(); + return new RepositoryWrapper(repo); + }); + + builder.Services.AddTransient>(provider => + { + var repo = provider.GetRequiredService>(); + return new RepositoryWrapper(repo); + }); + + builder.Services.AddTransient>(provider => + { + var repo = provider.GetRequiredService>(); + return new RepositoryWrapper(repo); + }); + + builder.Services.AddTransient>(provider => + { + var repo = provider.GetRequiredService>(); + return new RepositoryWrapper(repo); + }); + + builder.Services.AddTransient>(provider => + { + var repo = provider.GetRequiredService>(); + return new RepositoryWrapper(repo); + }); + + builder.Services.AddTransient>(provider => + { + var repo = provider.GetRequiredService>(); + return new RepositoryWrapper(repo); + }); + + builder.Services.AddTransient>(provider => + { + var repo = provider.GetRequiredService>(); + return new RepositoryWrapper(repo); + }); + + builder.Services.AddTransient>(provider => + { + var repo = provider.GetRequiredService>(); + return new RepositoryWrapper(repo); + }); + + builder.Services.AddTransient>(provider => + { + var repo = provider.GetRequiredService>(); + return new RepositoryWrapper(repo); + }); + + builder.Services.AddTransient>(provider => + { + var repo = provider.GetRequiredService>(); + return new RepositoryWrapper(repo); + }); + + builder.Services.AddTransient>(provider => + { + var repo = provider.GetRequiredService>(); + return new RepositoryWrapper(repo); + }); + + builder.Services.AddTransient>(provider => + { + var repo = provider.GetRequiredService>(); + return new RepositoryWrapper(repo); + }); + + builder.Services.AddTransient>(provider => + { + var repo = provider.GetRequiredService>(); + return new RepositoryWrapper(repo); + }); + + builder.Services.AddTransient>(provider => + { + var repo = provider.GetRequiredService>(); + return new RepositoryWrapper(repo); + }); + + builder.Services.AddTransient>(provider => + { + var repo = provider.GetRequiredService>(); + return new RepositoryWrapper(repo); + }); + builder.Services.AddTransient>(provider => + { + var repo = provider.GetRequiredService>(); + return new RepositoryWrapper(repo); + }); + builder.Services.AddTransient>(provider => + { + var repo = provider.GetRequiredService>(); + return new RepositoryWrapper(repo); + }); + + builder.Services.AddTransient>(provider => + { + var repo = provider.GetRequiredService>(); + return new RepositoryWrapper(repo); + }); + #endregion + + #region Register Services + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddSingleton(); + #endregion + + #region VM and Pages + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.Services.AddTransient(); + builder.Services.AddTransient();//binding for UI + builder.Services.AddTransient(); + + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + // Register ViewModel + builder.Services.AddTransient(); + builder.Services.AddTransient(); + #endregion + + return builder.Build(); + } + } +} \ No newline at end of file diff --git a/LSFE.MobApp/Models/ApiResponse.cs b/LSFE.MobApp/Models/ApiResponse.cs new file mode 100644 index 0000000..c106df7 --- /dev/null +++ b/LSFE.MobApp/Models/ApiResponse.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Models +{ + public class ApiResponse + { + public bool Success { get; set; } + public T? Data { get; set; } + public string? Message { get; set; } + public int MessCode { get; set; } + public string? Token { get; internal set; } + } +} diff --git a/LSFE.MobApp/Models/LoginRequest.cs b/LSFE.MobApp/Models/LoginRequest.cs new file mode 100644 index 0000000..c5c55b6 --- /dev/null +++ b/LSFE.MobApp/Models/LoginRequest.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Models +{ + public class LoginRequest + { + public string? UserName { get; set; } + public string? Password { get; set; } + } +} diff --git a/LSFE.MobApp/Models/LoginResponse.cs b/LSFE.MobApp/Models/LoginResponse.cs new file mode 100644 index 0000000..bf4a4fa --- /dev/null +++ b/LSFE.MobApp/Models/LoginResponse.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Models +{ + public class LoginResponse + { + public string? Token { get; set; } + public string? UserId { get; set; } + public string? UserName { get; set; } + public string? Email { get; set; } + public string? Password { get; set; } + public string? EmployeeId { get; set; } + public string? FullName { get; set; } + public string? Company { get; set; } + public string? PhoneNumber { get; set; } + public bool LockoutEnabled { get; set; } + public bool IsDeviceRegister { get; set; } + public bool Success { get; set; } + public string? Message { get; set; } + } +} diff --git a/LSFE.MobApp/Models/ProductMaterial.cs b/LSFE.MobApp/Models/ProductMaterial.cs new file mode 100644 index 0000000..1ae09e8 --- /dev/null +++ b/LSFE.MobApp/Models/ProductMaterial.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Models +{ + public class ProductMaterial : INotifyPropertyChanged + { + private bool _isDownloaded; + private bool _isDownloading; + private double _downloadProgress; + + public Guid ProductTransId { get; set; } + public int ProductId { get; set; } + public string? ProductName { get; set; } + public string? ProductLink { get; set; } + public string? FileUrl { get; set; } + public string? FileExtension { get; set; } + public string IconEmoji => FileExtension?.ToLower() switch + { + ".pdf" => "📄", + ".ppt" or ".pptx" => "📊", + ".doc" or ".docx" => "📝", + _ => "📎" + }; + + public bool IsDownloaded + { + get => _isDownloaded; + set + { + _isDownloaded = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(DownloadButtonText)); + OnPropertyChanged(nameof(CanDownload)); + } + } + + public bool IsDownloading + { + get => _isDownloading; + set + { + _isDownloading = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(CanDownload)); + } + } + + public double DownloadProgress + { + get => _downloadProgress; + set + { + _downloadProgress = value; + OnPropertyChanged(); + } + } + private bool _isChecked; + public bool IsChecked + { + get => _isChecked; + set + { + _isChecked = value; + OnPropertyChanged(); + } + } + public string DownloadButtonText => IsDownloaded ? "✓ Downloaded" : "⬇️ Download"; + public bool CanDownload => !IsDownloading && !IsDownloaded; + + public event PropertyChangedEventHandler PropertyChanged; + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/LSFE.MobApp/Models/SamplingItem.cs b/LSFE.MobApp/Models/SamplingItem.cs new file mode 100644 index 0000000..cf54078 --- /dev/null +++ b/LSFE.MobApp/Models/SamplingItem.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Models +{ + public class SamplingItem : INotifyPropertyChanged + { + public Guid InventoryTransId { get; set; } + public string? Description { get; set; } + public int QtyIn { get; set; } + public int Qty { get; set; } + public byte IconEmojiId { get; set; } + public string IconEmoji { get; set; } = "💊"; + + private bool _isChecked; + public bool IsChecked + { + get => _isChecked; + set + { + _isChecked = value; + OnPropertyChanged(); + } + } + + public event PropertyChangedEventHandler PropertyChanged; + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/LSFE.MobApp/Models/SpecializationItem.cs b/LSFE.MobApp/Models/SpecializationItem.cs new file mode 100644 index 0000000..8f6e259 --- /dev/null +++ b/LSFE.MobApp/Models/SpecializationItem.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Models +{ + public class SpecializationItem + { + public byte Id { get; set; } + public string Name { get; set; } = string.Empty; + } +} diff --git a/LSFE.MobApp/Models/SyncResult.cs b/LSFE.MobApp/Models/SyncResult.cs new file mode 100644 index 0000000..d138665 --- /dev/null +++ b/LSFE.MobApp/Models/SyncResult.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LSFE.MobApp.Models +{ + public class SyncResult + { + public int TotalCount { get; set; } + public int SuccessCount { get; set; } + public int FailCount => TotalCount - SuccessCount; + public List FailedIds { get; set; } = new List(); + + public bool AllSucceeded => FailCount == 0; + public double SuccessRate => TotalCount > 0 ? (double)SuccessCount / TotalCount * 100 : 0; + } +} diff --git a/LSFE.MobApp/Pages/Admin/DeviceRegistrationPage.xaml b/LSFE.MobApp/Pages/Admin/DeviceRegistrationPage.xaml new file mode 100644 index 0000000..b83d0d2 --- /dev/null +++ b/LSFE.MobApp/Pages/Admin/DeviceRegistrationPage.xaml @@ -0,0 +1,449 @@ + + + + + + #1DB9A6 + #2C3E50 + #7F8C9A + #FFFFFF + #EFF3F8 + #E74C3C + #27AE60 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LSFE.MobApp/Pages/Attendance/AttendancePage.xaml.cs b/LSFE.MobApp/Pages/Attendance/AttendancePage.xaml.cs new file mode 100644 index 0000000..5ce3dc2 --- /dev/null +++ b/LSFE.MobApp/Pages/Attendance/AttendancePage.xaml.cs @@ -0,0 +1,21 @@ +using LSFE.MobApp.ViewModels; + +namespace LSFE.MobApp.Pages.Attendance; + +public partial class AttendancePage : ContentPage +{ + private readonly AttendanceVM _viewModel; + + public AttendancePage(AttendanceVM viewModel) + { + InitializeComponent(); + _viewModel = viewModel; + BindingContext = _viewModel; + } + + protected override async void OnAppearing() + { + base.OnAppearing(); + await _viewModel.InitializeAsync(); + } +} \ No newline at end of file diff --git a/LSFE.MobApp/Pages/Authentication/ForgotPasswordPage.xaml b/LSFE.MobApp/Pages/Authentication/ForgotPasswordPage.xaml new file mode 100644 index 0000000..a48cd67 --- /dev/null +++ b/LSFE.MobApp/Pages/Authentication/ForgotPasswordPage.xaml @@ -0,0 +1,257 @@ + + + + + + #1DB9A6 + #2C3E50 + #7F8C9A + #FFFFFF + #EFF3F8 + #E74C3C + #27AE60 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LSFE.MobApp/Pages/Authentication/ForgotPasswordPage.xaml.cs b/LSFE.MobApp/Pages/Authentication/ForgotPasswordPage.xaml.cs new file mode 100644 index 0000000..ba38da1 --- /dev/null +++ b/LSFE.MobApp/Pages/Authentication/ForgotPasswordPage.xaml.cs @@ -0,0 +1,12 @@ +using LSFE.MobApp.ViewModels.Authentication; + +namespace LSFE.MobApp.Pages.Authentication; + +public partial class ForgotPasswordPage : ContentPage +{ + public ForgotPasswordPage(ForgotPasswordVM viewModel) + { + InitializeComponent(); + BindingContext = viewModel; + } +} \ No newline at end of file diff --git a/LSFE.MobApp/Pages/Authentication/LoginPage.xaml b/LSFE.MobApp/Pages/Authentication/LoginPage.xaml new file mode 100644 index 0000000..e73f12f --- /dev/null +++ b/LSFE.MobApp/Pages/Authentication/LoginPage.xaml @@ -0,0 +1,342 @@ + + + + + + + #1DB9A6 + #17A593 + #B4E9E0 + #7DD4C5 + #2C3E50 + #7F8C9A + #E8F5F3 + #FFFFFF + #EFF3F8 + #E74C3C + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LSFE.MobApp/Pages/Authentication/LoginPage.xaml.cs b/LSFE.MobApp/Pages/Authentication/LoginPage.xaml.cs new file mode 100644 index 0000000..e2b95e6 --- /dev/null +++ b/LSFE.MobApp/Pages/Authentication/LoginPage.xaml.cs @@ -0,0 +1,12 @@ +using LSFE.MobApp.ViewModels.Authentication; + +namespace LSFE.MobApp.Pages.Authentication; + +public partial class LoginPage : ContentPage +{ + public LoginPage(LoginVM viewModel) + { + InitializeComponent(); + BindingContext = viewModel; + } +} \ No newline at end of file diff --git a/LSFE.MobApp/Pages/CoveragePlan/ActualCoveragePage.xaml b/LSFE.MobApp/Pages/CoveragePlan/ActualCoveragePage.xaml new file mode 100644 index 0000000..17805a4 --- /dev/null +++ b/LSFE.MobApp/Pages/CoveragePlan/ActualCoveragePage.xaml @@ -0,0 +1,307 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +