Merge branch 'Purchase_Invoice_Management' of https://git.marcoaiot.com/admin/marco.pms.api into Response_Encryption
This commit is contained in:
commit
5459a24a58
@ -659,6 +659,7 @@ namespace Marco.Pms.DataAccess.Data
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static void ManageApplicationStructure(ModelBuilder modelBuilder)
|
||||
{
|
||||
// Configure ApplicationRole to Tenant relationship (if Tenant exists)
|
||||
|
||||
@ -38,5 +38,7 @@ namespace Marco.Pms.Model.Collection
|
||||
[ValidateNever]
|
||||
[ForeignKey("UpdatedById")]
|
||||
public Employee? UpdatedBy { get; set; }
|
||||
|
||||
public ICollection<ReceivedInvoicePayment> ReceivedInvoicePayments { get; set; } = new List<ReceivedInvoicePayment>();
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,9 +15,8 @@ namespace Marco.Pms.Model.Expenses
|
||||
public Guid ProjectId { get; set; }
|
||||
public Guid ExpensesTypeId { get; set; }
|
||||
|
||||
//[ValidateNever]
|
||||
//[ForeignKey("ExpensesTypeId")]
|
||||
//public ExpensesTypeMaster? ExpensesType { get; set; }
|
||||
public ICollection<BillAttachments> Attachments { get; set; } = new List<BillAttachments>();
|
||||
public ICollection<ExpensesReimburseMapping> ExpensesReimburseMappings { get; set; } = new List<ExpensesReimburseMapping>();
|
||||
|
||||
public Guid ExpenseCategoryId { get; set; }
|
||||
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
namespace Marco.Pms.Model.Master
|
||||
using Marco.Pms.Model.Expenses.Masters;
|
||||
|
||||
namespace Marco.Pms.Model.Master
|
||||
{
|
||||
public class ExpensesStatusMaster
|
||||
{
|
||||
@ -9,5 +11,7 @@
|
||||
public string Color { get; set; } = string.Empty;
|
||||
public bool IsSystem { get; set; } = false;
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
public ICollection<StatusPermissionMapping> StatusPermissionMappings { get; set; } = new List<StatusPermissionMapping>();
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,12 +12,14 @@ namespace Marco.Pms.Model.ViewModels.Collection
|
||||
public string InvoiceNumber { get; set; } = default!;
|
||||
public string? EInvoiceNumber { get; set; }
|
||||
public BasicOrganizationVm? BilledTo { get; set; }
|
||||
public BasicProjectVM? Project { get; set; }
|
||||
public Guid ProjectId { get; set; }
|
||||
public BasicProjectVM Project { get; set; } = new BasicProjectVM();
|
||||
public DateTime InvoiceDate { get; set; }
|
||||
public DateTime ClientSubmitedDate { get; set; }
|
||||
public DateTime ExceptedPaymentDate { get; set; }
|
||||
public double BasicAmount { get; set; }
|
||||
public double TaxAmount { get; set; }
|
||||
public double TotalAmount { get; set; }
|
||||
public double BalanceAmount { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public bool MarkAsCompleted { get; set; }
|
||||
|
||||
@ -9,6 +9,7 @@ namespace Marco.Pms.Model.ViewModels.Expanses
|
||||
public class ExpenseList
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid ProjectId { get; set; }
|
||||
public BasicProjectVM? Project { get; set; }
|
||||
public ExpenseCategoryMasterVM? ExpenseCategory { get; set; }
|
||||
public PaymentModeMatserVM? PaymentMode { get; set; }
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public List<Guid>? PermissionIds { get; set; }
|
||||
public List<Guid> PermissionIds { get; set; } = new List<Guid>();
|
||||
public string? Color { get; set; }
|
||||
public bool IsSystem { get; set; } = false;
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Helpers.Utility;
|
||||
using Marco.Pms.Model.Collection;
|
||||
@ -145,7 +146,9 @@ namespace Marco.Pms.Services.Controllers
|
||||
.Include(i => i.BilledTo)
|
||||
.Include(i => i.CreatedBy).ThenInclude(e => e!.JobRole)
|
||||
.Include(i => i.UpdatedBy).ThenInclude(e => e!.JobRole)
|
||||
.Where(i => projIds.Contains(i.ProjectId) && i.IsActive == isActive && i.TenantId == tenantId);
|
||||
.Include(i => i.ReceivedInvoicePayments)
|
||||
.Where(i => projIds.Contains(i.ProjectId) && i.IsActive == isActive && i.TenantId == tenantId)
|
||||
.ProjectTo<InvoiceListVM>(_mapper.ConfigurationProvider);
|
||||
|
||||
// Filter by date, ensuring date boundaries are correct
|
||||
if (fromDate.HasValue && toDate.HasValue)
|
||||
@ -179,6 +182,12 @@ namespace Marco.Pms.Services.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
if (isPending)
|
||||
{
|
||||
query = query.Where(i => i.BalanceAmount > 0 && !i.MarkAsCompleted);
|
||||
_logger.LogDebug("Pending filter applied.");
|
||||
}
|
||||
|
||||
var totalItems = await query.CountAsync();
|
||||
var totalPages = (int)Math.Ceiling((double)totalItems / pageSize);
|
||||
_logger.LogInfo("Total invoices found: {TotalItems}", totalItems);
|
||||
@ -197,45 +206,14 @@ namespace Marco.Pms.Services.Controllers
|
||||
));
|
||||
}
|
||||
|
||||
// Fetch related payments in a single query to minimize DB calls
|
||||
var invoiceIds = pagedInvoices.Select(i => i.Id).ToList();
|
||||
var paymentGroups = await _context.ReceivedInvoicePayments
|
||||
.AsNoTracking()
|
||||
.Where(p => invoiceIds.Contains(p.InvoiceId) && p.TenantId == tenantId)
|
||||
.GroupBy(p => p.InvoiceId)
|
||||
.Select(g => new { InvoiceId = g.Key, PaidAmount = g.Sum(p => p.Amount) })
|
||||
.ToDictionaryAsync(g => g.InvoiceId, g => g.PaidAmount);
|
||||
|
||||
_logger.LogDebug("Received payment data for {Count} invoices.", paymentGroups.Count);
|
||||
pagedInvoices.ForEach(i => i.Project = projects.Where(sp => sp.Id == i.ProjectId).FirstOrDefault()!);
|
||||
|
||||
// Build results and compute balances in memory for tight control
|
||||
var results = new List<InvoiceListVM>();
|
||||
|
||||
foreach (var invoice in pagedInvoices)
|
||||
{
|
||||
var total = invoice.BasicAmount + invoice.TaxAmount;
|
||||
var paid = paymentGroups.GetValueOrDefault(invoice.Id, 0);
|
||||
var balance = total - paid;
|
||||
|
||||
// Filter pending
|
||||
if (isPending && (balance <= 0 || invoice.MarkAsCompleted))
|
||||
continue;
|
||||
|
||||
var vm = _mapper.Map<InvoiceListVM>(invoice);
|
||||
|
||||
// Project mapping logic - minimize nested object allocations
|
||||
vm.Project = projects.Where(sp => sp.Id == invoice.ProjectId).FirstOrDefault();
|
||||
|
||||
|
||||
vm.BalanceAmount = balance;
|
||||
results.Add(vm);
|
||||
}
|
||||
|
||||
_logger.LogInfo("Returning {Count} invoices (page {PageNumber} of {TotalPages}).", results.Count, pageNumber, totalPages);
|
||||
_logger.LogInfo("Returning {Count} invoices (page {PageNumber} of {TotalPages}).", pagedInvoices.Count, pageNumber, totalPages);
|
||||
|
||||
return Ok(ApiResponse<object>.SuccessResponse(
|
||||
new { CurrentPage = pageNumber, TotalPages = totalPages, TotalEntities = totalItems, Data = results },
|
||||
$"{results.Count} invoices fetched successfully."
|
||||
new { CurrentPage = pageNumber, TotalPages = totalPages, TotalEntities = totalItems, Data = pagedInvoices },
|
||||
$"{pagedInvoices.Count} invoices fetched successfully."
|
||||
));
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@ -169,13 +169,13 @@ namespace Marco.Pms.Services.MappingProfiles
|
||||
CreateMap<ProjectBasicMongoDB, BasicProjectVM>()
|
||||
.ForMember(
|
||||
dest => dest.Id,
|
||||
opt => opt.MapFrom(src => new Guid(src.Id)));
|
||||
opt => opt.MapFrom(src => string.IsNullOrWhiteSpace(src.Id) ? Guid.Empty : new Guid(src.Id)));
|
||||
|
||||
CreateMap<ProjectMongoDB, Project>()
|
||||
.ForMember(
|
||||
dest => dest.Id,
|
||||
// Explicitly and safely convert string Id to Guid Id
|
||||
opt => opt.MapFrom(src => new Guid(src.Id))
|
||||
opt => opt.MapFrom(src => string.IsNullOrWhiteSpace(src.Id) ? Guid.Empty : new Guid(src.Id))
|
||||
).ForMember(
|
||||
dest => dest.ProjectStatusId,
|
||||
// Explicitly and safely convert string ProjectStatusId to Guid ProjectStatusId
|
||||
@ -293,7 +293,13 @@ namespace Marco.Pms.Services.MappingProfiles
|
||||
|
||||
#region ======================================================= Expenses =======================================================
|
||||
|
||||
CreateMap<Expenses, ExpenseList>();
|
||||
CreateMap<Expenses, ExpenseList>()
|
||||
.ForMember(
|
||||
dest => dest.ExpenseUId,
|
||||
opt => opt.MapFrom(src => $"{src.UIDPrefix}/{src.UIDPostfix:D5}"))
|
||||
.ForMember(
|
||||
dest => dest.PaymentRequestUID,
|
||||
opt => opt.MapFrom(src => src.PaymentRequest != null ? $"{src.PaymentRequest.UIDPrefix}/{src.PaymentRequest.UIDPostfix:D5}" : null));
|
||||
CreateMap<CreateExpensesDto, Expenses>();
|
||||
CreateMap<UpdateExpensesDto, Expenses>();
|
||||
CreateMap<ExpenseLog, ExpenseLogVM>();
|
||||
@ -320,7 +326,17 @@ namespace Marco.Pms.Services.MappingProfiles
|
||||
dest => dest.Id,
|
||||
opt => opt.MapFrom(src => Guid.Parse(src.Id)));
|
||||
|
||||
CreateMap<Expenses, ExpenseDetailsVM>();
|
||||
CreateMap<Expenses, ExpenseDetailsVM>()
|
||||
.ForMember(
|
||||
dest => dest.ExpenseUId,
|
||||
opt => opt.MapFrom(src => $"{src.UIDPrefix}/{src.UIDPostfix:D5}"))
|
||||
.ForMember(
|
||||
dest => dest.PaymentRequestUID,
|
||||
opt => opt.MapFrom(src => src.PaymentRequest != null ? $"{src.PaymentRequest.UIDPrefix}/{src.PaymentRequest.UIDPostfix:D5}" : null))
|
||||
.ForMember(
|
||||
dest => dest.ExpensesReimburse,
|
||||
opt => opt.MapFrom(src => src.ExpensesReimburseMappings.Select(erm => erm.ExpensesReimburse).FirstOrDefault()));
|
||||
|
||||
CreateMap<ExpenseDetailsMongoDB, ExpenseDetailsVM>()
|
||||
.ForMember(
|
||||
dest => dest.Id,
|
||||
@ -370,7 +386,13 @@ namespace Marco.Pms.Services.MappingProfiles
|
||||
|
||||
#region ======================================================= Collection =======================================================
|
||||
CreateMap<InvoiceDto, Invoice>();
|
||||
CreateMap<Invoice, InvoiceListVM>();
|
||||
CreateMap<Invoice, InvoiceListVM>()
|
||||
.ForMember(
|
||||
dest => dest.TotalAmount,
|
||||
opt => opt.MapFrom(src => src.BasicAmount + src.TaxAmount))
|
||||
.ForMember(
|
||||
dest => dest.BalanceAmount,
|
||||
opt => opt.MapFrom(src => (src.BasicAmount + src.TaxAmount) - src.ReceivedInvoicePayments.Sum(rip => rip.Amount)));
|
||||
CreateMap<Invoice, InvoiceDetailsVM>();
|
||||
|
||||
CreateMap<ReceivedInvoicePaymentDto, ReceivedInvoicePayment>();
|
||||
@ -471,7 +493,10 @@ namespace Marco.Pms.Services.MappingProfiles
|
||||
.ForMember(
|
||||
dest => dest.DisplayName,
|
||||
opt => opt.MapFrom(src => string.IsNullOrWhiteSpace(src.DisplayName) ? src.Name : src.DisplayName));
|
||||
CreateMap<ExpensesStatusMaster, ExpensesStatusMasterVM>();
|
||||
CreateMap<ExpensesStatusMaster, ExpensesStatusMasterVM>()
|
||||
.ForMember(
|
||||
dest => dest.PermissionIds,
|
||||
opt => opt.MapFrom(src => src.StatusPermissionMappings.Select(spm => spm.PermissionId)));
|
||||
|
||||
CreateMap<ExpensesStatusMaster, ExpensesStatusMasterMongoDB>()
|
||||
.ForMember(
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Marco.Pms.DataAccess.Data;
|
||||
using Marco.Pms.Helpers.Utility;
|
||||
using Marco.Pms.Model.Dtos.Expenses;
|
||||
@ -16,6 +17,7 @@ using Marco.Pms.Model.MongoDBModels.Utility;
|
||||
using Marco.Pms.Model.TenantModels;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Model.ViewModels.Activities;
|
||||
using Marco.Pms.Model.ViewModels.DocumentManager;
|
||||
using Marco.Pms.Model.ViewModels.Expanses;
|
||||
using Marco.Pms.Model.ViewModels.Expenses;
|
||||
using Marco.Pms.Model.ViewModels.Expenses.Masters;
|
||||
@ -283,10 +285,10 @@ namespace Marco.Pms.Services.Service
|
||||
var response = _mapper.Map<ExpenseList>(m);
|
||||
if (response.Status != null && (response.NextStatus?.Any() ?? false))
|
||||
{
|
||||
response.Status.PermissionIds = permissionStatusMapping.Where(ps => ps.StatusId == Guid.Parse(m.Status.Id)).Select(ps => ps.PermissionIds).FirstOrDefault();
|
||||
response.Status.PermissionIds = permissionStatusMapping.Where(ps => ps.StatusId == Guid.Parse(m.Status.Id)).Select(ps => ps.PermissionIds).FirstOrDefault() ?? new List<Guid>();
|
||||
foreach (var status in response.NextStatus)
|
||||
{
|
||||
status.PermissionIds = permissionStatusMapping.Where(ps => ps.StatusId == status.Id).Select(ps => ps.PermissionIds).FirstOrDefault();
|
||||
status.PermissionIds = permissionStatusMapping.Where(ps => ps.StatusId == status.Id).Select(ps => ps.PermissionIds).FirstOrDefault() ?? new List<Guid>();
|
||||
}
|
||||
}
|
||||
return response;
|
||||
@ -319,13 +321,11 @@ namespace Marco.Pms.Services.Service
|
||||
return ApiResponse<object>.ErrorResponse("Error Occured", ExceptionMapper(ex), 500);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<object>> GetExpensesListDynamicAsync(Employee loggedInEmployee, Guid tenantId, string? searchString, string? filter, int pageSize, int pageNumber)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInfo(
|
||||
"Attempting to fetch expenses list for PageNumber: {PageNumber}, PageSize: {PageSize} with Filter: {Filter}",
|
||||
_logger.LogInfo("Attempting to fetch expenses list for PageNumber: {PageNumber}, PageSize: {PageSize} with Filter: {Filter}",
|
||||
pageNumber, pageSize, filter ?? "");
|
||||
|
||||
// 1. --- Get User Permissions ---
|
||||
@ -335,9 +335,6 @@ namespace Marco.Pms.Services.Service
|
||||
_logger.LogWarning("Could not find an employee for the current logged-in user.");
|
||||
return ApiResponse<object>.ErrorResponse("User not found or not authenticated.", 403);
|
||||
}
|
||||
Guid loggedInEmployeeId = loggedInEmployee.Id;
|
||||
List<ExpenseList> expenseVM = new List<ExpenseList>();
|
||||
var totalEntites = 0;
|
||||
|
||||
var hasViewSelfPermissionTask = HasPermissionAsync(PermissionsMaster.ExpenseViewSelf, loggedInEmployee.Id);
|
||||
var hasViewAllPermissionTask = HasPermissionAsync(PermissionsMaster.ExpenseViewAll, loggedInEmployee.Id);
|
||||
@ -350,7 +347,7 @@ namespace Marco.Pms.Services.Service
|
||||
if (!hasViewAllPermission && !hasViewSelfPermission)
|
||||
{
|
||||
// User has neither required permission. Deny access.
|
||||
_logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get expenses list.", loggedInEmployeeId);
|
||||
_logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get expenses list.", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "You do not have permission to view any expenses.", 200);
|
||||
}
|
||||
|
||||
@ -358,13 +355,6 @@ namespace Marco.Pms.Services.Service
|
||||
// 2. --- Deserialize Filter and Apply ---
|
||||
AdvanceFilter? advanceFilter = TryDeserializeAdvanceFilter(filter);
|
||||
|
||||
//var (totalPages, totalCount, cacheList) = await _cache.GetExpenseListAsync(tenantId, loggedInEmployeeId, hasViewAllPermissionTask.Result, hasViewSelfPermissionTask.Result,
|
||||
// pageNumber, pageSize, expenseFilter, searchString);
|
||||
|
||||
List<ExpenseDetailsMongoDB>? cacheList = null;
|
||||
var totalPages = 0;
|
||||
var totalCount = 0;
|
||||
|
||||
// 3. --- Build Base Query and Apply Permissions ---
|
||||
// Start with a base IQueryable. Filters will be chained onto this.
|
||||
var expensesQuery = _context.Expenses
|
||||
@ -381,133 +371,96 @@ namespace Marco.Pms.Services.Service
|
||||
.Include(e => e.Currency)
|
||||
.Where(e => e.TenantId == tenantId); // Always filter by TenantId first.
|
||||
|
||||
if (cacheList == null)
|
||||
// Apply permission-based filtering BEFORE any other filters or pagination.
|
||||
if (hasViewAllPermission)
|
||||
{
|
||||
//await _cache.AddExpensesListToCache(expenses: await expensesQuery.ToListAsync(), tenantId);
|
||||
|
||||
// Apply permission-based filtering BEFORE any other filters or pagination.
|
||||
if (hasViewAllPermission)
|
||||
{
|
||||
expensesQuery = expensesQuery.Where(e => e.CreatedById == loggedInEmployeeId || e.StatusId != Draft);
|
||||
}
|
||||
else if (hasViewSelfPermission)
|
||||
{
|
||||
// User only has 'View Self' permission, so restrict the query to their own expenses.
|
||||
_logger.LogInfo("User {EmployeeId} has 'View Self' permission. Restricting query to their expenses.", loggedInEmployeeId);
|
||||
expensesQuery = expensesQuery.Where(e => e.CreatedById == loggedInEmployeeId);
|
||||
}
|
||||
expensesQuery = expensesQuery.ApplyCustomFilters(advanceFilter, "CreatedAt");
|
||||
if (advanceFilter != null)
|
||||
{
|
||||
if (advanceFilter.Filters != null)
|
||||
{
|
||||
expensesQuery = expensesQuery.ApplyListFilters(advanceFilter.Filters);
|
||||
}
|
||||
if (advanceFilter.DateFilter != null)
|
||||
{
|
||||
expensesQuery = expensesQuery.ApplyDateFilter(advanceFilter.DateFilter);
|
||||
}
|
||||
if (advanceFilter.SearchFilters != null)
|
||||
{
|
||||
var invoiceSearchFilter = advanceFilter.SearchFilters.Where(f => f.Column != "ProjectName").ToList();
|
||||
if (invoiceSearchFilter.Any())
|
||||
{
|
||||
expensesQuery = expensesQuery.ApplySearchFilters(invoiceSearchFilter);
|
||||
}
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(advanceFilter.GroupByColumn))
|
||||
{
|
||||
expensesQuery = expensesQuery.ApplyGroupByFilters(advanceFilter.GroupByColumn);
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchString))
|
||||
{
|
||||
var searchStringLower = searchString.ToLower();
|
||||
expensesQuery = expensesQuery.Include(e => e.PaidBy).Include(e => e.CreatedBy)
|
||||
.Where(e => e.Description.ToLower().Contains(searchStringLower) ||
|
||||
(e.TransactionId != null && e.TransactionId.ToLower().Contains(searchStringLower)) ||
|
||||
(e.PaidBy != null && (e.PaidBy.FirstName + " " + e.PaidBy.LastName).ToLower().Contains(searchStringLower)) ||
|
||||
(e.CreatedBy != null && (e.CreatedBy.FirstName + " " + e.CreatedBy.LastName).ToLower().Contains(searchStringLower)) ||
|
||||
(e.UIDPrefix + "/" + e.UIDPostfix.ToString().PadLeft(5, '0')).Contains(searchString));
|
||||
}
|
||||
|
||||
// 4. --- Apply Ordering and Pagination ---
|
||||
// This should be the last step before executing the query.
|
||||
|
||||
totalEntites = await expensesQuery.CountAsync();
|
||||
|
||||
// 5. --- Execute Query and Map Results ---
|
||||
var expensesList = await expensesQuery
|
||||
//.OrderByDescending(e => e.CreatedAt)
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize).ToListAsync();
|
||||
|
||||
if (!expensesList.Any())
|
||||
{
|
||||
_logger.LogInfo("No expenses found matching the criteria for employee {EmployeeId}.", loggedInEmployeeId);
|
||||
return ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "No expenses found for the given criteria.", 200);
|
||||
}
|
||||
|
||||
var projectIds = expensesList.Select(e => e.ProjectId).ToList();
|
||||
|
||||
var infraProjectTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).Select(p => _mapper.Map<BasicProjectVM>(p)).ToListAsync();
|
||||
});
|
||||
|
||||
var serviceProjectTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.ServiceProjects.Where(sp => projectIds.Contains(sp.Id) && sp.TenantId == tenantId).Select(sp => _mapper.Map<BasicProjectVM>(sp)).ToListAsync();
|
||||
});
|
||||
|
||||
await Task.WhenAll(infraProjectTask, serviceProjectTask);
|
||||
|
||||
var projects = infraProjectTask.Result;
|
||||
projects.AddRange(serviceProjectTask.Result);
|
||||
|
||||
//expenseVM = await GetAllExpnesRelatedTables(expensesList, tenantId);
|
||||
expenseVM = expensesList.Select(e =>
|
||||
{
|
||||
var result = _mapper.Map<ExpenseList>(e);
|
||||
result.ExpenseUId = $"{e.UIDPrefix}/{e.UIDPostfix:D5}";
|
||||
if (e.PaymentRequest != null)
|
||||
result.PaymentRequestUID = $"{e.PaymentRequest.UIDPrefix}/{e.PaymentRequest.UIDPostfix:D5}";
|
||||
result.Project = projects.FirstOrDefault(p => p.Id == e.ProjectId);
|
||||
return result;
|
||||
}).ToList();
|
||||
totalPages = (int)Math.Ceiling((double)totalEntites / pageSize);
|
||||
|
||||
expensesQuery = expensesQuery.Where(e => e.CreatedById == loggedInEmployee.Id || e.StatusId != Draft);
|
||||
}
|
||||
else
|
||||
else if (hasViewSelfPermission)
|
||||
{
|
||||
var permissionStatusMapping = await _context.StatusPermissionMapping
|
||||
.GroupBy(ps => ps.StatusId)
|
||||
.Select(g => new
|
||||
{
|
||||
StatusId = g.Key,
|
||||
PermissionIds = g.Select(ps => ps.PermissionId).ToList()
|
||||
}).ToListAsync();
|
||||
|
||||
expenseVM = cacheList.Select(m =>
|
||||
{
|
||||
var response = _mapper.Map<ExpenseList>(m);
|
||||
if (response.Status != null && (response.NextStatus?.Any() ?? false))
|
||||
{
|
||||
response.Status.PermissionIds = permissionStatusMapping.Where(ps => ps.StatusId == Guid.Parse(m.Status.Id)).Select(ps => ps.PermissionIds).FirstOrDefault();
|
||||
foreach (var status in response.NextStatus)
|
||||
{
|
||||
status.PermissionIds = permissionStatusMapping.Where(ps => ps.StatusId == status.Id).Select(ps => ps.PermissionIds).FirstOrDefault();
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}).ToList();
|
||||
totalEntites = (int)totalCount;
|
||||
// User only has 'View Self' permission, so restrict the query to their own expenses.
|
||||
_logger.LogInfo("User {EmployeeId} has 'View Self' permission. Restricting query to their expenses.", loggedInEmployee.Id);
|
||||
expensesQuery = expensesQuery.Where(e => e.CreatedById == loggedInEmployee.Id);
|
||||
}
|
||||
expensesQuery = expensesQuery.ApplyCustomFilters(advanceFilter, "CreatedAt");
|
||||
if (advanceFilter != null)
|
||||
{
|
||||
if (advanceFilter.Filters != null)
|
||||
{
|
||||
expensesQuery = expensesQuery.ApplyListFilters(advanceFilter.Filters);
|
||||
}
|
||||
if (advanceFilter.DateFilter != null)
|
||||
{
|
||||
expensesQuery = expensesQuery.ApplyDateFilter(advanceFilter.DateFilter);
|
||||
}
|
||||
if (advanceFilter.SearchFilters != null)
|
||||
{
|
||||
var invoiceSearchFilter = advanceFilter.SearchFilters.Where(f => f.Column != "ProjectName").ToList();
|
||||
if (invoiceSearchFilter.Any())
|
||||
{
|
||||
expensesQuery = expensesQuery.ApplySearchFilters(invoiceSearchFilter);
|
||||
}
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(advanceFilter.GroupByColumn))
|
||||
{
|
||||
expensesQuery = expensesQuery.ApplyGroupByFilters(advanceFilter.GroupByColumn);
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchString))
|
||||
{
|
||||
var searchStringLower = searchString.ToLower();
|
||||
expensesQuery = expensesQuery.Include(e => e.PaidBy).Include(e => e.CreatedBy)
|
||||
.Where(e => e.Description.ToLower().Contains(searchStringLower) ||
|
||||
(e.TransactionId != null && e.TransactionId.ToLower().Contains(searchStringLower)) ||
|
||||
(e.PaidBy != null && (e.PaidBy.FirstName + " " + e.PaidBy.LastName).ToLower().Contains(searchStringLower)) ||
|
||||
(e.CreatedBy != null && (e.CreatedBy.FirstName + " " + e.CreatedBy.LastName).ToLower().Contains(searchStringLower)) ||
|
||||
(e.UIDPrefix + "/" + e.UIDPostfix.ToString().PadLeft(5, '0')).Contains(searchString));
|
||||
}
|
||||
|
||||
// 4. --- Apply Ordering and Pagination ---
|
||||
// This should be the last step before executing the query.
|
||||
|
||||
var totalEntites = await expensesQuery.CountAsync();
|
||||
|
||||
var totalPages = (int)Math.Ceiling((double)totalEntites / pageSize);
|
||||
|
||||
// 5. --- Execute Query and Map Results ---
|
||||
var expensesList = await expensesQuery
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ProjectTo<ExpenseList>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
if (!expensesList.Any())
|
||||
{
|
||||
_logger.LogInfo("No expenses found matching the criteria for employee {EmployeeId}.", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.SuccessResponse(new List<ExpenseList>(), "No expenses found for the given criteria.", 200);
|
||||
}
|
||||
|
||||
var projectIds = expensesList.Select(e => e.ProjectId).ToList();
|
||||
|
||||
var infraProjectTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).Select(p => _mapper.Map<BasicProjectVM>(p)).ToListAsync();
|
||||
});
|
||||
|
||||
var serviceProjectTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.ServiceProjects.Where(sp => projectIds.Contains(sp.Id) && sp.TenantId == tenantId).Select(sp => _mapper.Map<BasicProjectVM>(sp)).ToListAsync();
|
||||
});
|
||||
|
||||
await Task.WhenAll(infraProjectTask, serviceProjectTask);
|
||||
|
||||
var projects = infraProjectTask.Result;
|
||||
projects.AddRange(serviceProjectTask.Result);
|
||||
|
||||
expensesList.ForEach(e => e.Project = projects.FirstOrDefault(p => p.Id == e.ProjectId));
|
||||
|
||||
// 7. --- Return Final Success Response ---
|
||||
var message = $"{expenseVM.Count} expense records fetched successfully.";
|
||||
var message = $"{expensesList.Count} expense records fetched successfully.";
|
||||
_logger.LogInfo(message);
|
||||
|
||||
|
||||
@ -516,7 +469,7 @@ namespace Marco.Pms.Services.Service
|
||||
CurrentPage = pageNumber,
|
||||
TotalPages = totalPages,
|
||||
TotalEntites = totalEntites,
|
||||
Data = expenseVM,
|
||||
Data = expensesList,
|
||||
};
|
||||
return ApiResponse<object>.SuccessResponse(response, message, 200);
|
||||
}
|
||||
@ -532,112 +485,199 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ApiResponse<object>> GetExpenseDetailsAsync(Guid? id, string? expenseUId, Employee loggedInEmployee, Guid tenantId)
|
||||
/// <summary>
|
||||
/// Retrieves detailed information for a specific expense, including its status workflow, project details, and attachments.
|
||||
/// Applies permission filtering and S3 URL generation.
|
||||
/// </summary>
|
||||
/// <param name="id">The GUID of the expense (optional if expenseUId is provided).</param>
|
||||
/// <param name="expenseUId">The formatted Unique ID of the expense (optional if id is provided).</param>
|
||||
/// <param name="loggedInEmployee">The currently authenticated employee requesting the data.</param>
|
||||
/// <param name="tenantId">The tenant identifier for data isolation.</param>
|
||||
/// <returns>A standardized API response containing the Expense Detail View Model.</returns>
|
||||
public async Task<ApiResponse<ExpenseDetailsVM>> GetExpenseDetailsAsync(Guid? id, string? expenseUId, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
// Log entry with structured data for traceability
|
||||
_logger.LogInfo("Starting execution of GetExpenseDetailsAsync. EmployeeId: {EmployeeId}, TenantId: {TenantId}", loggedInEmployee.Id, tenantId);
|
||||
|
||||
try
|
||||
{
|
||||
// 1. Validation: Ensure at least one identifier is present
|
||||
if (!id.HasValue && string.IsNullOrWhiteSpace(expenseUId))
|
||||
{
|
||||
_logger.LogWarning("Invalid parameters: Both Id and PaymentRequestUID are null or empty.");
|
||||
return ApiResponse<object>.ErrorResponse("At least one parameter (Id or expenseUId) must be provided.", "Invalid argument.", 400);
|
||||
_logger.LogWarning("Validation failed: Both ExpenseId and ExpenseUId are null or empty.");
|
||||
return ApiResponse<ExpenseDetailsVM>.ErrorResponse("At least one parameter (Id or expenseUId) must be provided.", "Invalid Argument", 400);
|
||||
}
|
||||
ExpenseDetailsMongoDB? expenseDetails = null;
|
||||
if (expenseDetails == null)
|
||||
{
|
||||
var expense = await _context.Expenses
|
||||
.Include(e => e.PaidBy)
|
||||
.Include(e => e.CreatedBy)
|
||||
.Include(e => e.ProcessedBy)
|
||||
.Include(e => e.ApprovedBy)
|
||||
.Include(e => e.ReviewedBy)
|
||||
.Include(e => e.PaymentMode)
|
||||
.Include(e => e.PaymentMode)
|
||||
.Include(e => e.ExpenseCategory)
|
||||
.Include(e => e.Status)
|
||||
.Include(e => e.Currency)
|
||||
.Include(e => e.PaymentRequest)
|
||||
.AsNoTracking().FirstOrDefaultAsync(e => (e.Id == id || (e.UIDPrefix + "/" + e.UIDPostfix.ToString().PadLeft(5, '0')) == expenseUId) && e.TenantId == tenantId);
|
||||
|
||||
if (expense == null)
|
||||
// 2. Main Data Fetch: Using AsSplitQuery to prevent Cartesian explosion with multiple Includes
|
||||
// Note: Ensure _context is injected as Scoped.
|
||||
var expense = await _context.Expenses
|
||||
.AsNoTracking() // Read-only query optimization
|
||||
.AsSplitQuery() // CRITICAL: Splits query into multiple SQL statements to improve performance with many JOINs
|
||||
.Include(e => e.PaidBy).ThenInclude(e => e!.JobRole)
|
||||
.Include(e => e.CreatedBy).ThenInclude(e => e!.JobRole)
|
||||
.Include(e => e.ProcessedBy).ThenInclude(e => e!.JobRole)
|
||||
.Include(e => e.ApprovedBy).ThenInclude(e => e!.JobRole)
|
||||
.Include(e => e.ReviewedBy).ThenInclude(e => e!.JobRole)
|
||||
.Include(e => e.PaymentMode)
|
||||
.Include(e => e.ExpenseCategory)
|
||||
.Include(e => e.Status).ThenInclude(s => s!.StatusPermissionMappings)
|
||||
.Include(e => e.Currency)
|
||||
.Include(e => e.PaymentRequest)
|
||||
.Include(e => e.Attachments).ThenInclude(ba => ba.Document)
|
||||
.Include(e => e.ExpensesReimburseMappings)
|
||||
.ThenInclude(erm => erm.ExpensesReimburse)
|
||||
.ThenInclude(er => er!.ReimburseBy)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.FirstOrDefaultAsync(e =>
|
||||
(e.Id == id || (e.UIDPrefix + "/" + e.UIDPostfix.ToString().PadLeft(5, '0')) == expenseUId)
|
||||
&& e.TenantId == tenantId);
|
||||
|
||||
// 3. Not Found Handling
|
||||
if (expense == null)
|
||||
{
|
||||
_logger.LogWarning("Expense not found. Id: {Id}, UId: {UId}, TenantId: {TenantId}", id ?? Guid.Empty, expenseUId ?? "", tenantId);
|
||||
return ApiResponse<ExpenseDetailsVM>.ErrorResponse("Expense not found.", "Resource Not Found", 404);
|
||||
}
|
||||
|
||||
// 4. Map Entity to ViewModel
|
||||
var response = _mapper.Map<ExpenseDetailsVM>(expense);
|
||||
|
||||
// 5. Fetch Permissions
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var _permissionServices = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||
var permissionIds = await _permissionServices.GetPermissionIdsByEmployeeId(loggedInEmployee.Id);
|
||||
|
||||
// 6. Fetch Next Valid Statuses
|
||||
// Logic extracted to private helper method for cleaner code
|
||||
var nextStatusList = await GetNextStatusesAsync(expense.StatusId, tenantId);
|
||||
|
||||
// 7. Filter and Sort Statuses based on permissions and business logic ("Reject" priority)
|
||||
response.NextStatus = ProcessNextStatuses(nextStatusList, expense.CreatedById, loggedInEmployee.Id, permissionIds);
|
||||
|
||||
// 8. Fetch Project Details
|
||||
// We run these sequentially to avoid DbContext threading issues.
|
||||
// Given indexed columns, the performance hit is negligible compared to the overhead of creating new threads/contexts.
|
||||
var projectVm = await GetProjectDetailsAsync(expense.ProjectId, tenantId);
|
||||
response.Project = projectVm;
|
||||
|
||||
response.ExpenseLogs = await _context.ExpenseLogs
|
||||
.AsNoTracking() // Read-only query optimization
|
||||
.Include(el => el.UpdatedBy)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.Where(el => el.ExpenseId == expense.Id)
|
||||
.OrderByDescending(el => el.UpdateAt)
|
||||
.ProjectTo<ExpenseLogVM>(_mapper.ConfigurationProvider) // Projection
|
||||
.ToListAsync();
|
||||
|
||||
// 9. Generate S3 Pre-signed URLs for attachments
|
||||
response.Documents = expense.Attachments
|
||||
.Where(ba => ba.Document != null)
|
||||
.Select(ba =>
|
||||
{
|
||||
if (id.HasValue)
|
||||
{
|
||||
_logger.LogWarning("User attempted to fetch expense details with ID {ExpenseId}, but not found in both database and cache", id);
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(expenseUId))
|
||||
{
|
||||
_logger.LogWarning("User attempted to fetch expense details with expenseUId {ExpenseUId}, but not found in both database and cache", expenseUId);
|
||||
}
|
||||
return ApiResponse<object>.ErrorResponse("Expense Not Found", "Expense Not Found", 404);
|
||||
}
|
||||
var document = ba.Document!;
|
||||
var result = _mapper.Map<BasicDocumentVM>(document);
|
||||
|
||||
expenseDetails = await GetAllExpnesRelatedTablesForSingle(expense, loggedInEmployee.Id, expense.TenantId);
|
||||
}
|
||||
var vm = _mapper.Map<ExpenseDetailsVM>(expenseDetails);
|
||||
result!.PreSignedUrl = _s3Service.GeneratePreSignedUrl(document.S3Key);
|
||||
result!.ThumbPreSignedUrl = string.IsNullOrWhiteSpace(document.ThumbS3Key) ? result!.PreSignedUrl : _s3Service.GeneratePreSignedUrl(document.ThumbS3Key);
|
||||
return result;
|
||||
}).ToList();
|
||||
|
||||
var permissionStatusMappingTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.StatusPermissionMapping
|
||||
.GroupBy(ps => ps.StatusId)
|
||||
.Select(g => new
|
||||
{
|
||||
StatusId = g.Key,
|
||||
PermissionIds = g.Select(ps => ps.PermissionId).ToList()
|
||||
}).ToListAsync();
|
||||
});
|
||||
var expenseReimburseTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
return await context.ExpensesReimburseMapping
|
||||
.Include(er => er.ExpensesReimburse)
|
||||
.ThenInclude(er => er!.ReimburseBy)
|
||||
.ThenInclude(e => e!.JobRole)
|
||||
.Where(er => er.TenantId == tenantId && er.ExpensesId == vm.Id)
|
||||
.Select(er => er.ExpensesReimburse).FirstOrDefaultAsync();
|
||||
});
|
||||
var permissionStatusMappings = permissionStatusMappingTask.Result;
|
||||
var expensesReimburse = expenseReimburseTask.Result;
|
||||
|
||||
if (vm.Status != null && (vm.NextStatus?.Any() ?? false))
|
||||
{
|
||||
vm.Status.PermissionIds = permissionStatusMappings.Where(ps => ps.StatusId == vm.Status.Id).Select(ps => ps.PermissionIds).FirstOrDefault();
|
||||
foreach (var status in vm.NextStatus)
|
||||
{
|
||||
status.PermissionIds = permissionStatusMappings.Where(ps => ps.StatusId == status.Id).Select(ps => ps.PermissionIds).FirstOrDefault();
|
||||
}
|
||||
int index = vm.NextStatus.FindIndex(ns => ns.DisplayName == "Reject");
|
||||
if (index > -1)
|
||||
{
|
||||
var item = vm.NextStatus[index];
|
||||
vm.NextStatus.RemoveAt(index);
|
||||
vm.NextStatus.Insert(0, item);
|
||||
}
|
||||
}
|
||||
vm.ExpensesReimburse = _mapper.Map<ExpensesReimburseVM>(expensesReimburse);
|
||||
|
||||
foreach (var document in expenseDetails.Documents)
|
||||
{
|
||||
var response = vm.Documents.FirstOrDefault(d => d.DocumentId == Guid.Parse(document.DocumentId));
|
||||
|
||||
response!.PreSignedUrl = _s3Service.GeneratePreSignedUrl(document.S3Key);
|
||||
response!.ThumbPreSignedUrl = _s3Service.GeneratePreSignedUrl(document.ThumbS3Key);
|
||||
}
|
||||
|
||||
var expenselogs = await _context.ExpenseLogs.Include(el => el.UpdatedBy).Where(el => el.ExpenseId == vm.Id).Select(el => _mapper.Map<ExpenseLogVM>(el)).ToListAsync();
|
||||
|
||||
vm.ExpenseLogs = expenselogs;
|
||||
|
||||
_logger.LogInfo("Employee {EmployeeId} successfully fetched expense details with ID {ExpenseId}", loggedInEmployee.Id, vm.Id);
|
||||
return ApiResponse<object>.SuccessResponse(vm, "Successfully fetched the details of expense", 200);
|
||||
_logger.LogInfo("Successfully fetched expense details. ExpenseId: {ExpenseId}", response.Id);
|
||||
|
||||
return ApiResponse<ExpenseDetailsVM>.SuccessResponse(response, "Successfully fetched the details of expense", 200);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "An unhandled exception occurred while fetching an expense details {ExpenseId} or {ExpenseUId}.", id ?? Guid.Empty, expenseUId ?? "EX_00000");
|
||||
return ApiResponse<object>.ErrorResponse("An internal server error occurred.", ExceptionMapper(ex), 500);
|
||||
// Capture specific context in the error log
|
||||
_logger.LogError(ex, "Unhandled exception in GetExpenseDetailsAsync. Id: {Id}, UId: {UId}", id ?? Guid.Empty, expenseUId ?? "");
|
||||
return ApiResponse<ExpenseDetailsVM>.ErrorResponse("An internal server error occurred.", ExceptionMapper(ex), 500);
|
||||
}
|
||||
}
|
||||
|
||||
#region Helper Methods (Private)
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the list of possible next statuses based on current status.
|
||||
/// </summary>
|
||||
private async Task<List<ExpensesStatusMaster>> GetNextStatusesAsync(Guid? currentStatusId, Guid tenantId)
|
||||
{
|
||||
if (!currentStatusId.HasValue) return new List<ExpensesStatusMaster>();
|
||||
|
||||
return await _context.ExpensesStatusMapping
|
||||
.AsNoTracking()
|
||||
.Include(esm => esm.NextStatus).ThenInclude(s => s!.StatusPermissionMappings)
|
||||
.Where(esm => esm.StatusId == currentStatusId && esm.Status != null)
|
||||
.Select(esm => esm.NextStatus!) // Select only the NextStatus entity
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters statuses by permission and reorders specific actions (e.g., Reject).
|
||||
/// </summary>
|
||||
private List<ExpensesStatusMasterVM> ProcessNextStatuses(List<ExpensesStatusMaster> nextStatuses, Guid createdById, Guid loggedInEmployeeId, List<Guid> userPermissionIds)
|
||||
{
|
||||
if (nextStatuses == null || !nextStatuses.Any()) return new List<ExpensesStatusMasterVM>();
|
||||
|
||||
// Business Logic: Move "Reject" to the top
|
||||
var rejectStatus = nextStatuses.FirstOrDefault(ns => ns.DisplayName == "Reject");
|
||||
if (rejectStatus != null)
|
||||
{
|
||||
nextStatuses.Remove(rejectStatus);
|
||||
nextStatuses.Insert(0, rejectStatus);
|
||||
}
|
||||
|
||||
var resultVMs = new List<ExpensesStatusMasterVM>();
|
||||
|
||||
foreach (var item in nextStatuses)
|
||||
{
|
||||
var vm = _mapper.Map<ExpensesStatusMasterVM>(item);
|
||||
|
||||
// Case 1: If Creator is viewing and status is Review (Assuming Review is a constant GUID or Enum)
|
||||
if (item.Id == Review && createdById == loggedInEmployeeId)
|
||||
{
|
||||
resultVMs.Add(vm);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Case 2: Standard Permission Check
|
||||
bool hasPermission = vm.PermissionIds.Any(pid => userPermissionIds.Contains(pid));
|
||||
|
||||
// Exclude "Done" status (Assuming Done is a constant GUID)
|
||||
if (hasPermission && item.Id != Done)
|
||||
{
|
||||
resultVMs.Add(vm);
|
||||
}
|
||||
}
|
||||
|
||||
return resultVMs.Distinct().ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to fetch project details from Projects table, falling back to ServiceProjects.
|
||||
/// </summary>
|
||||
private async Task<BasicProjectVM?> GetProjectDetailsAsync(Guid? projectId, Guid tenantId)
|
||||
{
|
||||
if (!projectId.HasValue) return null;
|
||||
|
||||
// Check Infrastructure Projects
|
||||
var infraProject = await _context.Projects
|
||||
.AsNoTracking()
|
||||
.Where(p => p.Id == projectId && p.TenantId == tenantId)
|
||||
.ProjectTo<BasicProjectVM>(_mapper.ConfigurationProvider) // Optimized: Project directly to VM inside SQL
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
if (infraProject != null) return infraProject;
|
||||
|
||||
// Fallback to Service Projects
|
||||
return await _context.ServiceProjects
|
||||
.AsNoTracking()
|
||||
.Where(sp => sp.Id == projectId && sp.TenantId == tenantId)
|
||||
.ProjectTo<BasicProjectVM>(_mapper.ConfigurationProvider)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public async Task<ApiResponse<object>> GetSupplerNameListAsync(Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
try
|
||||
|
||||
@ -2510,7 +2510,7 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
foreach (var status in response)
|
||||
{
|
||||
status.PermissionIds = permissionStatusMapping.Where(ps => ps.StatusId == status.Id).Select(ps => ps.PermissionIds).FirstOrDefault();
|
||||
status.PermissionIds = permissionStatusMapping.Where(ps => ps.StatusId == status.Id).Select(ps => ps.PermissionIds).FirstOrDefault() ?? new List<Guid>();
|
||||
}
|
||||
|
||||
_logger.LogInfo("{Count} records of expense status have been fetched successfully by employee {EmployeeId}", response.Count, loggedInEmployee.Id);
|
||||
|
||||
@ -129,14 +129,17 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
// Fetch corresponding employee details in one query
|
||||
var employeeIds = createdByIds.Union(updatedByIds).ToList();
|
||||
var employees = await _context.Employees.Where(e => employeeIds.Contains(e.Id)).ToListAsync();
|
||||
var employees = await _context.Employees
|
||||
.Where(e => employeeIds.Contains(e.Id))
|
||||
.ProjectTo<BasicEmployeeVM>(_mapper.ConfigurationProvider)
|
||||
.ToListAsync();
|
||||
|
||||
// Map data to view models including created and updated by employees
|
||||
var vm = organizations.Select(o =>
|
||||
{
|
||||
var orgVm = _mapper.Map<OrganizationVM>(o);
|
||||
orgVm.CreatedBy = employees.Where(e => e.Id == o.CreatedById).Select(e => _mapper.Map<BasicEmployeeVM>(e)).FirstOrDefault();
|
||||
orgVm.UpdatedBy = employees.Where(e => e.Id == o.UpdatedById).Select(e => _mapper.Map<BasicEmployeeVM>(e)).FirstOrDefault();
|
||||
orgVm.CreatedBy = employees.Where(e => e.Id == o.CreatedById).FirstOrDefault();
|
||||
orgVm.UpdatedBy = employees.Where(e => e.Id == o.UpdatedById).FirstOrDefault();
|
||||
return orgVm;
|
||||
}).ToList();
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using Marco.Pms.Model.Dtos.Expenses;
|
||||
using Marco.Pms.Model.Employees;
|
||||
using Marco.Pms.Model.Utilities;
|
||||
using Marco.Pms.Model.ViewModels.Expenses;
|
||||
|
||||
namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||
{
|
||||
@ -9,7 +10,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||
#region =================================================================== Expenses Functions ===================================================================
|
||||
Task<ApiResponse<object>> GetExpensesListAsync(Employee loggedInEmployee, Guid tenantId, string? searchString, string? filter, int pageSize, int pageNumber);
|
||||
Task<ApiResponse<object>> GetExpensesListDynamicAsync(Employee loggedInEmployee, Guid tenantId, string? searchString, string? filter, int pageSize, int pageNumber);
|
||||
Task<ApiResponse<object>> GetExpenseDetailsAsync(Guid? id, string? expenseUId, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<ExpenseDetailsVM>> GetExpenseDetailsAsync(Guid? id, string? expenseUId, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> GetSupplerNameListAsync(Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> GetFilterObjectAsync(Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> CreateExpenseAsync(CreateExpensesDto dto, Employee loggedInEmployee, Guid tenantId);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user