diff --git a/Marco.Pms.DataAccess/Data/ApplicationDbContext.cs b/Marco.Pms.DataAccess/Data/ApplicationDbContext.cs index 85ea792..82e1e68 100644 --- a/Marco.Pms.DataAccess/Data/ApplicationDbContext.cs +++ b/Marco.Pms.DataAccess/Data/ApplicationDbContext.cs @@ -395,7 +395,7 @@ namespace Marco.Pms.DataAccess.Data Name = "Draft", DisplayName = "Draft", Description = "Expense has been created but not yet submitted.", - Color = "#212529", + Color = "#6c757d", IsSystem = true, IsActive = true, TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26") diff --git a/Marco.Pms.DataAccess/Migrations/20250721124928_Added_Expense_Related_Tables.Designer.cs b/Marco.Pms.DataAccess/Migrations/20250721124928_Added_Expense_Related_Tables.Designer.cs index ad83f62..27368b8 100644 --- a/Marco.Pms.DataAccess/Migrations/20250721124928_Added_Expense_Related_Tables.Designer.cs +++ b/Marco.Pms.DataAccess/Migrations/20250721124928_Added_Expense_Related_Tables.Designer.cs @@ -1934,7 +1934,7 @@ namespace Marco.Pms.DataAccess.Migrations new { Id = new Guid("297e0d8f-f668-41b5-bfea-e03b354251c8"), - Color = "#212529", + Color = "#6c757d", Description = "Expense has been created but not yet submitted.", DisplayName = "Draft", IsActive = true, diff --git a/Marco.Pms.DataAccess/Migrations/20250721124928_Added_Expense_Related_Tables.cs b/Marco.Pms.DataAccess/Migrations/20250721124928_Added_Expense_Related_Tables.cs index 1d1e2f9..22a0444 100644 --- a/Marco.Pms.DataAccess/Migrations/20250721124928_Added_Expense_Related_Tables.cs +++ b/Marco.Pms.DataAccess/Migrations/20250721124928_Added_Expense_Related_Tables.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -373,7 +372,7 @@ namespace Marco.Pms.DataAccess.Migrations columns: new[] { "Id", "Color", "Description", "DisplayName", "IsActive", "IsSystem", "Name", "TenantId" }, values: new object[,] { - { new Guid("297e0d8f-f668-41b5-bfea-e03b354251c8"), "#212529", "Expense has been created but not yet submitted.", "Draft", true, true, "Draft", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") }, + { new Guid("297e0d8f-f668-41b5-bfea-e03b354251c8"), "#6c757d", "Expense has been created but not yet submitted.", "Draft", true, true, "Draft", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") }, { new Guid("4068007f-c92f-4f37-a907-bc15fe57d4d8"), "#0dcaf0", "Review is completed, waiting for action of approver.", "Approve", true, true, "Approval Pending", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") }, { new Guid("61578360-3a49-4c34-8604-7b35a3787b95"), "#198754", "Expense has been settled.", "Paid", true, true, "Processed", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") }, { new Guid("6537018f-f4e9-4cb3-a210-6c3b2da999d7"), "#0d6efd", "Reviewer is currently reviewing the expense.", "Review", true, true, "Review Pending", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") }, diff --git a/Marco.Pms.DataAccess/Migrations/ApplicationDbContextModelSnapshot.cs b/Marco.Pms.DataAccess/Migrations/ApplicationDbContextModelSnapshot.cs index c15054f..242e512 100644 --- a/Marco.Pms.DataAccess/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Marco.Pms.DataAccess/Migrations/ApplicationDbContextModelSnapshot.cs @@ -1931,7 +1931,7 @@ namespace Marco.Pms.DataAccess.Migrations new { Id = new Guid("297e0d8f-f668-41b5-bfea-e03b354251c8"), - Color = "#212529", + Color = "#6c757d", Description = "Expense has been created but not yet submitted.", DisplayName = "Draft", IsActive = true, diff --git a/Marco.Pms.Services/Service/ExpensesService.cs b/Marco.Pms.Services/Service/ExpensesService.cs index 9ea150f..8d1c974 100644 --- a/Marco.Pms.Services/Service/ExpensesService.cs +++ b/Marco.Pms.Services/Service/ExpensesService.cs @@ -155,6 +155,9 @@ namespace Marco.Pms.Services.Service // 4. --- Apply Ordering and Pagination --- // This should be the last step before executing the query. + + var totalEntites = await expensesQuery.CountAsync(); + var paginatedQuery = expensesQuery .OrderByDescending(e => e.CreatedAt) .Skip((pageNumber - 1) * pageSize) @@ -169,7 +172,7 @@ namespace Marco.Pms.Services.Service return ApiResponse.SuccessResponse(new List(), "No expenses found for the given criteria.", 200); } - var response = _mapper.Map>(expensesList); + var expenseVM = _mapper.Map>(expensesList); // 6. --- Efficiently Fetch and Append 'Next Status' Information --- var statusIds = expensesList.Select(e => e.StatusId).Distinct().ToList(); @@ -182,7 +185,7 @@ namespace Marco.Pms.Services.Service // Use a Lookup for efficient O(1) mapping. This is much better than repeated `.Where()` in a loop. var statusMapLookup = statusMappings.ToLookup(sm => sm.StatusId); - foreach (var expense in response) + foreach (var expense in expenseVM) { if (expense.Status?.Id != null && statusMapLookup.Contains(expense.Status.Id)) { @@ -197,8 +200,17 @@ namespace Marco.Pms.Services.Service } // 7. --- Return Final Success Response --- - var message = $"{response.Count} expense records fetched successfully."; + var message = $"{expenseVM.Count} expense records fetched successfully."; _logger.LogInfo(message); + var totalPages = (int)Math.Ceiling((double)totalEntites / pageSize); + var response = new + { + CurrentPage = pageNumber, + TotalPages = totalPages, + TotalEntites = totalEntites, + Data = expenseVM, + }; + return ApiResponse.SuccessResponse(response, message, 200); } catch (DbUpdateException dbEx) @@ -415,152 +427,6 @@ namespace Marco.Pms.Services.Service } } - public async Task> ChangeStatus(ExpenseRecordDto model, Employee loggedInEmployee, Guid tenantId) - { - - var existingExpense = await _context.Expenses - .Include(e => e.ExpensesType) - .Include(e => e.Project) - .Include(e => e.PaidBy) - .ThenInclude(e => e!.JobRole) - .Include(e => e.PaymentMode) - .Include(e => e.Status) - .Include(e => e.CreatedBy) - .FirstOrDefaultAsync(e => e.Id == model.ExpenseId && e.StatusId != model.StatusId && e.TenantId == tenantId); - - if (existingExpense == null) - { - return ApiResponse.ErrorResponse("Expense not found", "Expense not found", 404); - } - - var statusMappingTask = Task.Run(async () => - { - await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); - return await dbContext.ExpensesStatusMapping - .Include(s => s.NextStatus) - .FirstOrDefaultAsync(s => s.StatusId == existingExpense.StatusId && s.NextStatus != null && s.NextStatusId == model.StatusId && s.TenantId == tenantId); - }); - var statusPermissionMappingTask = Task.Run(async () => - { - await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); - return await dbContext.StatusPermissionMapping.Where(sp => sp.StatusId == model.StatusId).ToListAsync(); - }); - - // Await all prerequisite checks at once. - await Task.WhenAll(statusMappingTask, statusPermissionMappingTask); - - var statusMapping = await statusMappingTask; - var statusPermissions = await statusPermissionMappingTask; - if (statusMapping == null) - { - return ApiResponse.ErrorResponse("There is no follow-up status for currect status"); - } - if (statusPermissions.Any()) - { - foreach (var sp in statusPermissions) - { - using var scope = _serviceScopeFactory.CreateScope(); - var permissionService = scope.ServiceProvider.GetRequiredService(); - - if (!await permissionService.HasPermission(sp.PermissionId, loggedInEmployee.Id)) - { - _logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to change status of expense from status {StatusId} to {NextStatusId}", - loggedInEmployee.Id, statusMapping.StatusId, statusMapping.NextStatusId); - return ApiResponse.ErrorResponse("You do not have permission", "Access Denied", 403); - } - } - } - else if (existingExpense.CreatedById != loggedInEmployee.Id) - { - _logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to change status of expense from status {StatusId} to {NextStatusId}", - loggedInEmployee.Id, statusMapping.StatusId, statusMapping.NextStatusId); - return ApiResponse.ErrorResponse("You do not have permission", "Access Denied", 403); - } - var existingEntity = _updateLogHelper.EntityToBsonDocument(existingExpense); - - existingExpense.StatusId = statusMapping.NextStatusId; - existingExpense.Status = statusMapping.NextStatus; - - _context.ExpenseLogs.Add(new ExpenseLog - { - ExpenseId = existingExpense.Id, - Action = statusMapping.NextStatus!.Name, - UpdatedById = loggedInEmployee.Id, - Comment = model.Comment - }); - - - try - { - await _context.SaveChangesAsync(); - } - catch (DbUpdateConcurrencyException dbEx) - { - _logger.LogError(dbEx, "Error occured while update status of expanse."); - return ApiResponse.ErrorResponse("Error occured while update status of expanse.", new - { - Message = dbEx.Message, - StackTrace = dbEx.StackTrace, - Source = dbEx.Source, - innerexcption = new - { - Message = dbEx.InnerException?.Message, - StackTrace = dbEx.InnerException?.StackTrace, - Source = dbEx.InnerException?.Source, - } - }, 500); - } - try - { - var updateLog = new UpdateLogsObject - { - EntityId = existingExpense.Id.ToString(), - UpdatedById = loggedInEmployee.Id.ToString(), - OldObject = existingEntity, - UpdatedAt = DateTime.UtcNow - }; - var mongoDBTask = Task.Run(async () => - { - await _updateLogHelper.PushToUpdateLogsAsync(updateLog, Collection); - }); - - var getNextStatusTask = Task.Run(async () => - { - await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); - return await dbContext.ExpensesStatusMapping - .Include(s => s.NextStatus) - .FirstOrDefaultAsync(s => s.StatusId == existingExpense.StatusId && s.NextStatus != null && s.TenantId == tenantId); - }); - - await Task.WhenAll(mongoDBTask, getNextStatusTask); - - var getNextStatus = await getNextStatusTask; - - var response = _mapper.Map(existingExpense); - if (getNextStatus != null) - { - response.NextStatus = _mapper.Map>(getNextStatus.NextStatus); - } - - return ApiResponse.SuccessResponse(response); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error occured while Saving old entity in mongoDb"); - return ApiResponse.ErrorResponse("Error occured while update status of expanse.", new - { - Message = ex.Message, - StackTrace = ex.StackTrace, - Source = ex.Source, - innerexcption = new - { - Message = ex.InnerException?.Message, - StackTrace = ex.InnerException?.StackTrace, - Source = ex.InnerException?.Source, - } - }, 500); - } - } /// /// Changes the status of an expense record, performing validation, permission checks, and logging. ///