using AutoMapper; using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Dtos.ServiceProject; using Marco.Pms.Model.Employees; using Marco.Pms.Model.ServiceProject; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Activities; using Marco.Pms.Model.ViewModels.Master; using Marco.Pms.Model.ViewModels.Organization; using Marco.Pms.Model.ViewModels.ServiceProject; using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Service; using Microsoft.EntityFrameworkCore; namespace Marco.Pms.Services.Service { public class ServiceProjectService : IServiceProject { private readonly IDbContextFactory _dbContextFactory; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly ApplicationDbContext _context; // Keeping this for direct scoped context use where appropriate private readonly ILoggingService _logger; private readonly CacheUpdateHelper _cache; private readonly IMapper _mapper; public ServiceProjectService(IDbContextFactory dbContextFactory, IServiceScopeFactory serviceScopeFactory, ApplicationDbContext context, ILoggingService logger, CacheUpdateHelper cache, IMapper mapper) { _serviceScopeFactory = serviceScopeFactory; _context = context; _logger = logger; _cache = cache; _mapper = mapper; _dbContextFactory = dbContextFactory; } public async Task> CreateServiceProject(ServiceProjectDto model, Guid tenantId, Employee loggedInEmployee) { var serviceIds = model.Services.Where(s => s.IsActive).Select(s => s.ServiceId).ToList(); var clientTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.ClientId && o.IsActive); }); var serviceTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.ServiceMasters.Where(s => serviceIds.Contains(s.Id) && s.TenantId == tenantId && s.IsActive).ToListAsync(); }); var statusTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.StatusMasters.FirstOrDefaultAsync(s => s.Id == model.StatusId); }); await Task.WhenAll(clientTask, serviceTask, statusTask); var client = clientTask.Result; var services = serviceTask.Result; var status = statusTask.Result; if (client == null) { return ApiResponse.ErrorResponse("Client not found", "Client not found", 404); } if (status == null) { return ApiResponse.ErrorResponse("Project Status not found", "Project Status not found", 404); } var serviceProject = _mapper.Map(model); serviceProject.Id = Guid.NewGuid(); serviceProject.CreatedById = loggedInEmployee.Id; serviceProject.CreatedAt = DateTime.UtcNow; serviceProject.IsActive = true; serviceProject.TenantId = tenantId; var projectServiceMapping = model.Services.Where(sdto => services.Any(s => s.Id == sdto.ServiceId)).Select(sdto => new ServiceProjectServiceMapping { ServiceId = sdto.ServiceId, ProjectId = serviceProject.Id, TenantId = tenantId }).ToList(); try { _context.ServiceProjects.Add(serviceProject); _context.ServiceProjectServiceMapping.AddRange(projectServiceMapping); await _context.SaveChangesAsync(); _logger.LogInfo("Service Project {ProjectId} created successfully for TenantId={TenantId}, by Employee {EmployeeId}.", serviceProject.Id, tenantId, loggedInEmployee); var serviceProjectVM = _mapper.Map(serviceProject); serviceProjectVM.Client = _mapper.Map(client); serviceProjectVM.Status = status; serviceProjectVM.Services = services.Where(s => serviceIds.Contains(s.Id)).Select(s => _mapper.Map(s)).ToList(); serviceProjectVM.CreatedAt = DateTime.UtcNow; serviceProjectVM.CreatedBy = _mapper.Map(loggedInEmployee); return ApiResponse.SuccessResponse(serviceProjectVM, "An Successfullly occurred while saving the project.", 201); } catch (Exception ex) { _logger.LogError(ex, "DB Failure: Service Project creation failed for TenantId={TenantId}. Rolling back.", tenantId); return ApiResponse.ErrorResponse("An error occurred while saving the project.", ex.Message, 500); } } public async Task> GetServiceProjectList(int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId) { try { var serviceProjectQuery = _context.ServiceProjects .Include(sp => sp.Client) .Include(sp => sp.Status) .Include(sp => sp.CreatedBy).ThenInclude(e => e!.JobRole) .Include(sp => sp.UpdatedBy).ThenInclude(e => e!.JobRole) .Where(sp => sp.TenantId == tenantId && sp.IsActive); var serviceProjects = await serviceProjectQuery .OrderByDescending(e => e.CreatedAt) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToListAsync(); var serviceProjectIds = serviceProjects.Select(sp => sp.Id).ToList(); var serviceProjectServiceMappings = await _context.ServiceProjectServiceMapping .Include(sps => sps.Service) .Where(sps => serviceProjectIds.Contains(sps.ProjectId) && sps.Service != null && sps.TenantId == tenantId) .ToListAsync(); var serviceProjectVMs = serviceProjects.Select(sp => { var services = serviceProjectServiceMappings.Where(sps => sps.ProjectId == sp.Id).Select(sps => sps.Service!).ToList(); var result = _mapper.Map(sp); result.Services = _mapper.Map>(services); return result; }).ToList(); _logger.LogInfo("Successfully retrieved a total of {ProjectCount} projects.", serviceProjectVMs.Count); return ApiResponse.SuccessResponse(serviceProjectVMs, "Projects retrieved successfully.", 200); } catch (Exception ex) { // --- Step 5: Graceful Error Handling --- _logger.LogError(ex, "An unexpected error occurred in GetAllProjects for tenant {TenantId}.", tenantId); return ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500); } } public async Task> UpdateServiceProject(Guid id, ServiceProjectDto model, Guid tenantId, Employee loggedInEmployee) { try { var serviceIds = model.Services.Select(s => s.ServiceId).ToList(); var clientTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.Organizations.FirstOrDefaultAsync(o => o.Id == model.ClientId && o.IsActive); }); var serviceTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.ServiceMasters.Where(s => serviceIds.Contains(s.Id) && s.TenantId == tenantId && s.IsActive).ToListAsync(); }); var statusTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.StatusMasters.FirstOrDefaultAsync(s => s.Id == model.StatusId); }); await Task.WhenAll(clientTask, serviceTask, statusTask); var client = clientTask.Result; var services = serviceTask.Result; var status = statusTask.Result; if (client == null) { return ApiResponse.ErrorResponse("Client not found", "Client not found", 404); } if (status == null) { return ApiResponse.ErrorResponse("Project Status not found", "Project Status not found", 404); } var serviceProject = await _context.ServiceProjects.Where(sp => sp.Id == id && sp.TenantId == tenantId).FirstOrDefaultAsync(); if (serviceProject == null) { _logger.LogWarning("Attempt to update non-existent Service project with ID {ProjectId} by user {UserId}.", id, loggedInEmployee.Id); return ApiResponse.ErrorResponse("Project not found.", $"No project found with ID {id}.", 404); } _mapper.Map(model, serviceProject); serviceProject.UpdatedAt = DateTime.UtcNow; serviceProject.UpdatedById = loggedInEmployee.Id; var serviceProjectServiceMappings = await _context.ServiceProjectServiceMapping .AsNoTracking() .Where(sps => sps.ProjectId == serviceProject.Id && sps.TenantId == tenantId) .ToListAsync(); var newMapping = new List(); var removedMapping = new List(); foreach (var dto in model.Services) { var serviceProjectServiceMapping = serviceProjectServiceMappings .FirstOrDefault(sps => sps.ServiceId == dto.ServiceId); if (dto.IsActive && serviceProjectServiceMapping == null) { newMapping.Add(new ServiceProjectServiceMapping { Id = Guid.NewGuid(), ServiceId = dto.ServiceId, ProjectId = serviceProject.Id, TenantId = tenantId, }); } else if (!dto.IsActive && serviceProjectServiceMapping != null) { removedMapping.Add(serviceProjectServiceMapping); } } _context.ServiceProjectServiceMapping.AddRange(newMapping); _context.ServiceProjectServiceMapping.RemoveRange(removedMapping); await _context.SaveChangesAsync(); var serviceProjectTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.ServiceProjects .Include(sp => sp.Client) .Include(sp => sp.Status) .Include(sp => sp.CreatedBy).ThenInclude(e => e!.JobRole) .Include(sp => sp.UpdatedBy).ThenInclude(e => e!.JobRole) .Where(sp => sp.TenantId == tenantId && sp.IsActive).FirstOrDefaultAsync(); }); var servicesTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.ServiceProjectServiceMapping .Include(sps => sps.Service) .Where(sps => sps.ProjectId == serviceProject.Id && sps.Service != null && sps.TenantId == tenantId) .Select(sps => sps.Service!) .ToListAsync(); }); await Task.WhenAll(serviceProjectTask, servicesTask); serviceProject = serviceProjectTask.Result; services = servicesTask.Result; ServiceProjectVM serviceProjectVm = _mapper.Map(serviceProject); serviceProjectVm.Services = _mapper.Map>(services); return ApiResponse.SuccessResponse(serviceProjectVm, "Service Project updated successfully.", 200); } catch (Exception ex) { _logger.LogError(ex, "An unexpected error occurred in Updating Service Project for tenant {TenantId}.", tenantId); return ApiResponse.ErrorResponse("An internal server error occurred. Please try again later.", null, 500); } } } }