marco.pms.api/Marco.Pms.Services/Service/ServiceProjectService.cs

549 lines
26 KiB
C#

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<ApplicationDbContext> _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;
private readonly Guid NewStatus = Guid.Parse("32d76a02-8f44-4aa0-9b66-c3716c45a918");
private readonly Guid AssignedStatus = Guid.Parse("cfa1886d-055f-4ded-84c6-42a2a8a14a66");
public ServiceProjectService(IDbContextFactory<ApplicationDbContext> dbContextFactory,
IServiceScopeFactory serviceScopeFactory,
ApplicationDbContext context,
ILoggingService logger,
CacheUpdateHelper cache,
IMapper mapper)
{
_serviceScopeFactory = serviceScopeFactory;
_context = context;
_logger = logger;
_cache = cache;
_mapper = mapper;
_dbContextFactory = dbContextFactory;
}
#region =================================================================== Service Project Functions ===================================================================
public async Task<ApiResponse<object>> GetServiceProjectListAsync(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)
.Where(sp => sp.TenantId == tenantId && sp.IsActive);
var totalEntites = await serviceProjectQuery.CountAsync();
var totalPages = (int)Math.Ceiling((double)totalEntites / pageSize);
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<ServiceProjectVM>(sp);
result.Services = _mapper.Map<List<ServiceMasterVM>>(services);
return result;
}).ToList();
var response = new
{
CurrentPage = pageNumber,
TotalPages = totalPages,
TotalEntites = totalEntites,
Data = serviceProjectVMs,
};
_logger.LogInfo("Successfully retrieved a total of {ProjectCount} projects.", serviceProjectVMs.Count);
return ApiResponse<object>.SuccessResponse(response, "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<object>.ErrorResponse("An internal server error occurred. Please try again later.", null, 500);
}
}
public async Task<ApiResponse<object>> GetServiceProjectDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId)
{
var serviceProject = 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)
.FirstOrDefaultAsync(sp => sp.Id == id && sp.TenantId == tenantId);
if (serviceProject == null)
{
return ApiResponse<object>.ErrorResponse("Service Project not found", "Service Project not found", 404);
}
var services = await _context.ServiceProjectServiceMapping
.Include(sps => sps.Service)
.Where(sps => sps.ProjectId == serviceProject.Id &&
sps.Service != null &&
sps.TenantId == tenantId)
.Select(sps => sps.Service!)
.ToListAsync();
//var numberOfJobs = await _context.JobTickets.Where(jt => jt.ProjectId == serviceProject.Id && jt.TenantId == tenantId).CountAsync();
var response = _mapper.Map<ServiceProjectDetailsVM>(serviceProject);
response.Services = _mapper.Map<List<ServiceMasterVM>>(services);
//response.NumberOfJobs = numberOfJobs;
response.NumberOfJobs = 0;
return ApiResponse<object>.SuccessResponse(response, "Service Project Details fetched successfully", 200);
}
public async Task<ApiResponse<object>> CreateServiceProjectAsync(ServiceProjectDto model, Employee loggedInEmployee, Guid tenantId)
{
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<object>.ErrorResponse("Client not found", "Client not found", 404);
}
if (status == null)
{
return ApiResponse<object>.ErrorResponse("Project Status not found", "Project Status not found", 404);
}
var serviceProject = _mapper.Map<ServiceProject>(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<ServiceProjectVM>(serviceProject);
serviceProjectVM.Client = _mapper.Map<BasicOrganizationVm>(client);
serviceProjectVM.Status = status;
serviceProjectVM.Services = services.Where(s => serviceIds.Contains(s.Id)).Select(s => _mapper.Map<ServiceMasterVM>(s)).ToList();
serviceProjectVM.CreatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
return ApiResponse<object>.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<object>.ErrorResponse("An error occurred while saving the project.", ex.Message, 500);
}
}
public async Task<ApiResponse<object>> UpdateServiceProjectAsync(Guid id, ServiceProjectDto model, Employee loggedInEmployee, Guid tenantId)
{
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<object>.ErrorResponse("Client not found", "Client not found", 404);
}
if (status == null)
{
return ApiResponse<object>.ErrorResponse("Project Status not found", "Project Status not found", 404);
}
var serviceProject = await _context.ServiceProjects.Where(sp => sp.Id == id && sp.TenantId == tenantId && sp.IsActive).FirstOrDefaultAsync();
if (serviceProject == null)
{
_logger.LogWarning("Attempt to update non-existent Service project with ID {ProjectId} by user {UserId}.", id, loggedInEmployee.Id);
return ApiResponse<object>.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<ServiceProjectServiceMapping>();
var removedMapping = new List<ServiceProjectServiceMapping>();
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)
.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<ServiceProjectVM>(serviceProject);
serviceProjectVm.Services = _mapper.Map<List<ServiceMasterVM>>(services);
return ApiResponse<object>.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<object>.ErrorResponse("An internal server error occurred. Please try again later.", null, 500);
}
}
public async Task<ApiResponse<object>> DeActivateServiceProjectAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId)
{
try
{
var serviceProject = await _context.ServiceProjects
.Where(sp => sp.Id == id && sp.TenantId == tenantId).FirstOrDefaultAsync();
if (serviceProject == null)
{
_logger.LogWarning("Attempt to de-activate non-existent Service project with ID {ProjectId} by user {UserId}.", id, loggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("Project not found.", $"No project found with ID {id}.", 404);
}
serviceProject.IsActive = isActive;
await _context.SaveChangesAsync();
_logger.LogInfo("Successfully de-activated service project {ProjectId}", id);
return ApiResponse<object>.SuccessResponse(new { }, "Projects de-activated successfully.", 200);
}
catch (Exception ex)
{
// --- Step 5: Graceful Error Handling ---
_logger.LogError(ex, "An unexpected error occurred in DeActivateServiceProject for tenant {TenantId}.", tenantId);
return ApiResponse<object>.ErrorResponse("An internal server error occurred. Please try again later.", null, 500);
}
}
#endregion
#region =================================================================== Expense Functions ===================================================================
#endregion
#region =================================================================== Job Tickets Functions ===================================================================
/// <summary>
/// Creates a new job ticket with optional assignees and tags within a transactional scope.
/// </summary>
/// <param name="model">Data transfer object containing job ticket details.</param>
/// <param name="loggedInEmployee">Employee initiating the creation.</param>
/// <param name="tenantId">Tenant identifier for multi-tenant context.</param>
/// <returns>ApiResponse containing the created job ticket view or error details.</returns>
public async Task<ApiResponse<object>> CreateJobTicketAsync(CreateJobTicketDto model, Employee loggedInEmployee, Guid tenantId)
{
await using var transaction = await _context.Database.BeginTransactionAsync();
try
{
_logger.LogInfo("Starting job ticket creation for project {ProjectId} by employee {EmployeeId} in tenant {TenantId}",
model.ProjectId, loggedInEmployee.Id, tenantId);
// Load project and relevant statuses in parallel with separate DbContext instances for concurrency efficiency
var serviceProjectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ServiceProjects.FirstOrDefaultAsync(sp => sp.Id == model.ProjectId && sp.TenantId == tenantId && sp.IsActive);
});
var statusTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.JobStatus.Where(js => js.Id == NewStatus || js.Id == AssignedStatus).ToListAsync();
});
await Task.WhenAll(serviceProjectTask, statusTask);
var serviceProject = serviceProjectTask.Result;
var statusList = statusTask.Result;
if (serviceProject == null)
{
_logger.LogWarning("Service project with ID {ProjectId} not found or inactive in tenant {TenantId}", model.ProjectId, tenantId);
return ApiResponse<object>.ErrorResponse("Service project not found", "Service project not found or inactive", 404);
}
var hasAssignees = model.Assignees?.Any(a => a.IsActive) ?? false;
// Map DTO to entity
var jobTicket = _mapper.Map<JobTicket>(model);
jobTicket.Id = Guid.NewGuid();
jobTicket.StatusId = hasAssignees ? AssignedStatus : NewStatus;
jobTicket.CreatedAt = DateTime.UtcNow;
jobTicket.CreatedById = loggedInEmployee.Id;
jobTicket.TenantId = tenantId;
_context.JobTickets.Add(jobTicket);
await _context.SaveChangesAsync();
// Handle assignees if any
List<BasicEmployeeVM> assigneeVMs = new();
if (hasAssignees)
{
var activeAssigneeIds = model.Assignees!.Where(a => a.IsActive).Select(a => a.EmployeeId).ToList();
var assignees = await _context.Employees
.Include(e => e.JobRole)
.Where(e => activeAssigneeIds.Contains(e.Id) && e.IsActive)
.ToListAsync();
var jobEmployeeMappings = assignees.Select(e => new JobEmployeeMapping
{
AssigneeId = e.Id,
JobTicketId = jobTicket.Id,
TenantId = tenantId
}).ToList();
_context.JobEmployeeMappings.AddRange(jobEmployeeMappings);
assigneeVMs = _mapper.Map<List<BasicEmployeeVM>>(assignees);
}
// Handle tags if any
var jobTags = new List<JobTag>();
if (model.Tags?.Any(t => t.IsActive) ?? false)
{
var activeTagNames = model.Tags.Where(t => t.IsActive).Select(t => t.Name).Distinct().ToList();
var existingTags = await _context.JobTags.Where(jt => activeTagNames.Contains(jt.Name) && jt.TenantId == tenantId).ToListAsync();
var newTags = new List<JobTag>();
var tagMappings = new List<JobTagMapping>();
foreach (var tagDto in model.Tags.Where(t => t.IsActive).DistinctBy(t => t.Name))
{
var tag = existingTags.FirstOrDefault(jt => jt.Name == tagDto.Name);
if (tag == null)
{
tag = new JobTag
{
Id = Guid.NewGuid(),
Name = tagDto.Name,
TenantId = tenantId
};
newTags.Add(tag);
}
tagMappings.Add(new JobTagMapping
{
Id = Guid.NewGuid(),
JobTagId = tag.Id,
JobTicketId = jobTicket.Id,
TenantId = tenantId
});
}
if (newTags.Any()) _context.JobTags.AddRange(newTags);
_context.JobTagMappings.AddRange(tagMappings);
jobTags.AddRange(existingTags);
jobTags.AddRange(newTags);
}
// Create job update logs for status change
var updateLogs = new List<JobUpdateLog>();
const string creationMessage = "A new job has been successfully created and added to the system. It is now ready for assignment and further processing.";
updateLogs.Add(new JobUpdateLog
{
JobTicketId = jobTicket.Id,
NextStatusId = NewStatus,
Description = creationMessage,
Comment = creationMessage,
UpdatedAt = DateTime.UtcNow,
UpdatedById = loggedInEmployee.Id,
TenantId = tenantId
});
if (hasAssignees)
{
const string assignmentMessage = "The designated assignee(s) have been successfully assigned to the job and are now responsible for managing and completing the associated tasks.";
updateLogs.Add(new JobUpdateLog
{
JobTicketId = jobTicket.Id,
StatusId = NewStatus,
NextStatusId = AssignedStatus,
Description = assignmentMessage,
Comment = assignmentMessage,
UpdatedAt = DateTime.UtcNow.AddTicks(10), // Small offset to preserve time order
UpdatedById = loggedInEmployee.Id,
TenantId = tenantId
});
}
_context.JobUpdateLogs.AddRange(updateLogs);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
// Prepare response VM
var currentStatus = statusList.FirstOrDefault(js => js.Id == jobTicket.StatusId);
var vm = _mapper.Map<JobTicketVM>(jobTicket);
vm.Status = currentStatus;
vm.Project = _mapper.Map<BasicServiceProjectVM>(serviceProject);
vm.Assignees = assigneeVMs;
vm.CreatedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
vm.Tags = _mapper.Map<List<TagVM>>(jobTags.Distinct().ToList());
_logger.LogInfo("Job ticket {JobTicketId} successfully created with status {StatusId} by employee {EmployeeId} in tenant {TenantId}",
jobTicket.Id, jobTicket.StatusId, loggedInEmployee.Id, tenantId);
return ApiResponse<object>.SuccessResponse(vm, "Job created successfully", 201);
}
catch (DbUpdateException dbEx)
{
await transaction.RollbackAsync();
_logger.LogError(dbEx, "Database error while creating job ticket for project {ProjectId} by employee {EmployeeId} in tenant {TenantId}",
model.ProjectId, loggedInEmployee.Id, tenantId);
return ApiResponse<object>.ErrorResponse("Database Error", "An error occurred while saving data to the database.", 500);
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Unhandled exception while creating job ticket for project {ProjectId} by employee {EmployeeId} in tenant {TenantId}",
model.ProjectId, loggedInEmployee.Id, tenantId);
return ApiResponse<object>.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500);
}
}
#endregion
}
}