Added Google map url in service project table
This commit is contained in:
parent
c61ef92f6e
commit
2806dceab2
8628
Marco.Pms.DataAccess/Migrations/20251114120630_Added_GoogleMapUrl_In_ServiceProject_Table.Designer.cs
generated
Normal file
8628
Marco.Pms.DataAccess/Migrations/20251114120630_Added_GoogleMapUrl_In_ServiceProject_Table.Designer.cs
generated
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Marco.Pms.DataAccess.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Added_GoogleMapUrl_In_ServiceProject_Table : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "GoogleMapUrl",
|
||||
table: "ServiceProjects",
|
||||
type: "longtext",
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "GoogleMapUrl",
|
||||
table: "ServiceProjects");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5463,6 +5463,9 @@ namespace Marco.Pms.DataAccess.Migrations
|
||||
b.Property<Guid>("CreatedById")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<string>("GoogleMapUrl")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
|
||||
@ -17,6 +17,6 @@ namespace Marco.Pms.Model.Dtos.ServiceProject
|
||||
public required string ContactName { get; set; }
|
||||
public required string ContactPhone { get; set; }
|
||||
public required string ContactEmail { get; set; }
|
||||
|
||||
public string? GoogleMapUrl { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ namespace Marco.Pms.Model.ServiceProject
|
||||
public string ContactName { get; set; } = string.Empty;
|
||||
public string ContactPhone { get; set; } = string.Empty;
|
||||
public string ContactEmail { get; set; } = string.Empty;
|
||||
public string? GoogleMapUrl { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public Guid CreatedById { get; set; }
|
||||
|
||||
@ -18,6 +18,7 @@ namespace Marco.Pms.Model.ViewModels.ServiceProject
|
||||
public string? ContactName { get; set; }
|
||||
public string? ContactPhone { get; set; }
|
||||
public string? ContactEmail { get; set; }
|
||||
public string? GoogleMapUrl { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public BasicEmployeeVM? CreatedBy { get; set; }
|
||||
}
|
||||
|
||||
@ -119,6 +119,7 @@ namespace Marco.Pms.Services.Controllers
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Job Tickets Functions ===================================================================
|
||||
|
||||
[HttpGet("job/list")]
|
||||
|
||||
@ -19,22 +19,23 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
||||
Task<ApiResponse<object>> GetServiceProjectAllocationListAsync(Guid? projectId, Guid? employeeId, bool isActive, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> ManageServiceProjectAllocationAsync(List<ServiceProjectAllocationDto> model, Employee loggedInEmployee, Guid tenantId);
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Job Tickets Functions ===================================================================
|
||||
Task<ApiResponse<object>> GetJobTicketsListAsync(Guid? projectId, int pageNumber, int pageSize, bool isActive, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> GetJobTicketDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> GetCommentListByJobTicketAsync(Guid? jobTicketId, int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> GetJobTagListAsync(Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> CreateJobTicketAsync(CreateJobTicketDto model, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> ChangeJobsStatusAsync(ChangeJobStatusDto model, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> UpdateJobTicketAsync(Guid id, JobTicket jobTicket, UpdateJobTicketDto model, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> AddCommentToJobTicketAsync(JobCommentDto model, Employee loggedInEmployee, Guid tenantId);
|
||||
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Job Comments Functions ===================================================================
|
||||
|
||||
Task<ApiResponse<object>> GetCommentListByJobTicketAsync(Guid? jobTicketId, int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId);
|
||||
Task<ApiResponse<object>> AddCommentToJobTicketAsync(JobCommentDto model, Employee loggedInEmployee, Guid tenantId);
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Pubic Helper Functions ===================================================================
|
||||
#region =================================================================== Helper Functions ===================================================================
|
||||
Task<JobTicket?> GetJobTicketByIdAsync(Guid id, Guid tenantId);
|
||||
#endregion
|
||||
}
|
||||
|
||||
@ -49,27 +49,51 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
|
||||
#region =================================================================== Service Project Functions ===================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a paginated list of active service projects including their clients, status, creators, and related services for a given tenant.
|
||||
/// </summary>
|
||||
/// <param name="pageNumber">Page number (1-based) for pagination.</param>
|
||||
/// <param name="pageSize">Number of records per page.</param>
|
||||
/// <param name="loggedInEmployee">Employee making the request (for logging).</param>
|
||||
/// <param name="tenantId">Tenant identifier for multi-tenant isolation.</param>
|
||||
/// <returns>ApiResponse containing paged service projects with related data or error information.</returns>
|
||||
public async Task<ApiResponse<object>> GetServiceProjectListAsync(int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("GetServiceProjectListAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||
}
|
||||
|
||||
if (pageNumber < 1 || pageSize < 1)
|
||||
{
|
||||
_logger.LogInfo("Invalid pagination parameters: PageNumber={PageNumber}, PageSize={PageSize}", pageNumber, pageSize);
|
||||
return ApiResponse<object>.ErrorResponse("Bad Request", "Page number and size must be greater than zero.", 400);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
// Base query for active projects scoped by tenant including necessary related entities
|
||||
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 totalEntities = await serviceProjectQuery.CountAsync();
|
||||
var totalPages = (int)Math.Ceiling((double)totalEntities / pageSize);
|
||||
|
||||
// Fetch paged projects ordered by creation date descending
|
||||
var serviceProjects = await serviceProjectQuery
|
||||
.OrderByDescending(e => e.CreatedAt)
|
||||
.OrderByDescending(sp => sp.CreatedAt)
|
||||
.Skip((pageNumber - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync();
|
||||
|
||||
var serviceProjectIds = serviceProjects.Select(sp => sp.Id).ToList();
|
||||
|
||||
// Load related service mappings with services for current page projects (avoid N+1)
|
||||
var serviceProjectServiceMappings = await _context.ServiceProjectServiceMapping
|
||||
.Include(sps => sps.Service)
|
||||
.Where(sps => serviceProjectIds.Contains(sps.ProjectId) &&
|
||||
@ -77,46 +101,73 @@ namespace Marco.Pms.Services.Service
|
||||
sps.TenantId == tenantId)
|
||||
.ToListAsync();
|
||||
|
||||
// Map each project with its related services into the view models
|
||||
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;
|
||||
var relatedServices = serviceProjectServiceMappings
|
||||
.Where(sps => sps.ProjectId == sp.Id)
|
||||
.Select(sps => sps.Service!)
|
||||
.ToList();
|
||||
|
||||
var projectVm = _mapper.Map<ServiceProjectVM>(sp);
|
||||
projectVm.Services = _mapper.Map<List<ServiceMasterVM>>(relatedServices);
|
||||
return projectVm;
|
||||
}).ToList();
|
||||
|
||||
var response = new
|
||||
{
|
||||
CurrentPage = pageNumber,
|
||||
TotalPages = totalPages,
|
||||
TotalEntites = totalEntites,
|
||||
TotalEntities = totalEntities,
|
||||
Data = serviceProjectVMs,
|
||||
};
|
||||
|
||||
_logger.LogInfo("Successfully retrieved a total of {ProjectCount} projects.", serviceProjectVMs.Count);
|
||||
return ApiResponse<object>.SuccessResponse(response, "Projects retrieved successfully.", 200);
|
||||
_logger.LogInfo("Retrieved {Count} service projects for tenant {TenantId} by employee {EmployeeId}. Page {PageNumber}/{TotalPages}",
|
||||
serviceProjectVMs.Count, tenantId, loggedInEmployee.Id, pageNumber, totalPages);
|
||||
|
||||
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);
|
||||
_logger.LogError(ex, "An unexpected error occurred in GetServiceProjectListAsync for tenant {TenantId} by employee {EmployeeId}",
|
||||
tenantId, loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("An internal server error occurred. Please try again later.", null, 500);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves detailed information for a specific service project, including related client, status, services, and audit information.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the service project.</param>
|
||||
/// <param name="loggedInEmployee">The employee requesting the details (for audit logging).</param>
|
||||
/// <param name="tenantId">Tenant identifier ensuring multi-tenant data isolation.</param>
|
||||
/// <returns>ApiResponse containing detailed service project information or error details.</returns>
|
||||
public async Task<ApiResponse<object>> GetServiceProjectDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("GetServiceProjectDetailsAsync called with missing tenant context by employee {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Load service project with related client, status, and creator/updater roles
|
||||
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)
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(sp => sp.Id == id && sp.TenantId == tenantId);
|
||||
|
||||
if (serviceProject == null)
|
||||
{
|
||||
return ApiResponse<object>.ErrorResponse("Service Project not found", "Service Project not found", 404);
|
||||
_logger.LogWarning("Service project {ServiceProjectId} not found for tenant {TenantId}", id, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Service Project Not Found", "Service project not found for the specified tenant.", 404);
|
||||
}
|
||||
|
||||
// Retrieve related services for the project
|
||||
var services = await _context.ServiceProjectServiceMapping
|
||||
.Include(sps => sps.Service)
|
||||
.Where(sps => sps.ProjectId == serviceProject.Id &&
|
||||
@ -124,27 +175,66 @@ namespace Marco.Pms.Services.Service
|
||||
sps.TenantId == tenantId)
|
||||
.Select(sps => sps.Service!)
|
||||
.ToListAsync();
|
||||
//var numberOfJobs = await _context.JobTickets.Where(jt => jt.ProjectId == serviceProject.Id && jt.TenantId == tenantId).CountAsync();
|
||||
|
||||
// Optional: Count number of job tickets associated with the project (commented out)
|
||||
// var numberOfJobs = await _context.JobTickets.CountAsync(jt => jt.ProjectId == serviceProject.Id && jt.TenantId == tenantId);
|
||||
|
||||
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);
|
||||
response.NumberOfJobs = await _context.JobTickets.CountAsync(jt => jt.ProjectId == id && jt.IsActive && jt.TenantId == tenantId);
|
||||
|
||||
_logger.LogInfo("Fetched details for service project {ServiceProjectId} for tenant {TenantId} requested by employee {EmployeeId}",
|
||||
id, tenantId, loggedInEmployee.Id);
|
||||
|
||||
return ApiResponse<object>.SuccessResponse(response, "Service Project details fetched successfully.", 200);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to fetch service project details for project {ServiceProjectId} by employee {EmployeeId} in tenant {TenantId}",
|
||||
id, loggedInEmployee.Id, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Internal Server Error", "Unable to fetch service project details. Please try again later.", 500);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new service project along with its associated active services, and returns the created project details.
|
||||
/// </summary>
|
||||
/// <param name="model">DTO containing service project information and list of active services.</param>
|
||||
/// <param name="loggedInEmployee">Employee creating the service project (for auditing).</param>
|
||||
/// <param name="tenantId">Tenant context for multi-tenancy.</param>
|
||||
/// <returns>ApiResponse containing created service project details or error info.</returns>
|
||||
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();
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("CreateServiceProjectAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||
}
|
||||
|
||||
if (model == null)
|
||||
{
|
||||
_logger.LogInfo("CreateServiceProjectAsync called with null model by employee {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Bad Request", "Input model cannot be null.", 400);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Extract active service IDs for validation and association
|
||||
var serviceIds = model.Services?.Where(s => s.IsActive).Select(s => s.ServiceId).ToList() ?? new List<Guid>();
|
||||
|
||||
// Concurrently load dependent entities: client, services, status
|
||||
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();
|
||||
@ -157,15 +247,19 @@ namespace Marco.Pms.Services.Service
|
||||
var services = serviceTask.Result;
|
||||
var status = statusTask.Result;
|
||||
|
||||
// Validate existence of critical foreign entities
|
||||
if (client == null)
|
||||
{
|
||||
return ApiResponse<object>.ErrorResponse("Client not found", "Client not found", 404);
|
||||
_logger.LogWarning("Client with ID {ClientId} not found or inactive in tenant {TenantId}", model.ClientId, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Client Not Found", "Client not found or inactive.", 404);
|
||||
}
|
||||
if (status == null)
|
||||
{
|
||||
return ApiResponse<object>.ErrorResponse("Project Status not found", "Project Status not found", 404);
|
||||
_logger.LogWarning("Status with ID {StatusId} not found in tenant {TenantId}", model.StatusId, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Project Status Not Found", "Project status not found.", 404);
|
||||
}
|
||||
|
||||
// Map DTO to entity and enhance with auditing and tenant info
|
||||
var serviceProject = _mapper.Map<ServiceProject>(model);
|
||||
serviceProject.Id = Guid.NewGuid();
|
||||
serviceProject.CreatedById = loggedInEmployee.Id;
|
||||
@ -173,56 +267,81 @@ namespace Marco.Pms.Services.Service
|
||||
serviceProject.IsActive = true;
|
||||
serviceProject.TenantId = tenantId;
|
||||
|
||||
var projectServiceMapping = model.Services.Where(sdto => services.Any(s => s.Id == sdto.ServiceId)).Select(sdto => new ServiceProjectServiceMapping
|
||||
// Create mappings only for active services validated by database
|
||||
var projectServiceMappings = model.Services?
|
||||
.Where(sdto => services.Any(s => s.Id == sdto.ServiceId) && sdto.IsActive)
|
||||
.Select(sdto => new ServiceProjectServiceMapping
|
||||
{
|
||||
ServiceId = sdto.ServiceId,
|
||||
ProjectId = serviceProject.Id,
|
||||
TenantId = tenantId
|
||||
}).ToList();
|
||||
try
|
||||
{
|
||||
_context.ServiceProjects.Add(serviceProject);
|
||||
_context.ServiceProjectServiceMapping.AddRange(projectServiceMapping);
|
||||
}).ToList() ?? new List<ServiceProjectServiceMapping>();
|
||||
|
||||
// Add new project and its service mappings to context
|
||||
_context.ServiceProjects.Add(serviceProject);
|
||||
_context.ServiceProjectServiceMapping.AddRange(projectServiceMappings);
|
||||
|
||||
// Persist data
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInfo("Service Project {ProjectId} created successfully for TenantId={TenantId}, by Employee {EmployeeId}.",
|
||||
serviceProject.Id, tenantId, loggedInEmployee);
|
||||
_logger.LogInfo("Service Project {ProjectId} created successfully for Tenant {TenantId} by Employee {EmployeeId}", serviceProject.Id, tenantId, loggedInEmployee.Id);
|
||||
|
||||
// Prepare view model to return, mapping related created entities
|
||||
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.Services = services.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);
|
||||
|
||||
return ApiResponse<object>.SuccessResponse(serviceProjectVM, "Service project created successfully.", 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);
|
||||
_logger.LogError(ex, "Failed to create service project for tenant {TenantId} by employee {EmployeeId}", tenantId, loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while creating the service project. Please try again later.", 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// Updates an existing active service project including its related services, and returns updated project details.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the service project to update.</param>
|
||||
/// <param name="model">DTO containing updated service project data and service list.</param>
|
||||
/// <param name="loggedInEmployee">Employee performing the update (for audit/logging).</param>
|
||||
/// <param name="tenantId">Tenant identifier for multi-tenant isolation.</param>
|
||||
/// <returns>ApiResponse containing updated service project details or error info.</returns>
|
||||
public async Task<ApiResponse<object>> UpdateServiceProjectAsync(Guid id, ServiceProjectDto model, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("UpdateServiceProjectAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||
}
|
||||
|
||||
if (model == null)
|
||||
{
|
||||
_logger.LogInfo("UpdateServiceProjectAsync called with null model by employee {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Bad Request", "Input model cannot be null.", 400);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var serviceIds = model.Services.Select(s => s.ServiceId).ToList();
|
||||
// Extract all service IDs from input for validation
|
||||
var serviceIds = model.Services?.Select(s => s.ServiceId).ToList() ?? new List<Guid>();
|
||||
|
||||
// Concurrently fetch related entities for validation
|
||||
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();
|
||||
@ -235,44 +354,52 @@ namespace Marco.Pms.Services.Service
|
||||
var services = serviceTask.Result;
|
||||
var status = statusTask.Result;
|
||||
|
||||
// Validate client and status existence
|
||||
if (client == null)
|
||||
{
|
||||
return ApiResponse<object>.ErrorResponse("Client not found", "Client not found", 404);
|
||||
_logger.LogWarning("Client with ID {ClientId} not found or inactive in tenant {TenantId}", model.ClientId, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Client Not Found", "Client not found or inactive.", 404);
|
||||
}
|
||||
if (status == null)
|
||||
{
|
||||
return ApiResponse<object>.ErrorResponse("Project Status not found", "Project Status not found", 404);
|
||||
_logger.LogWarning("Project status with ID {StatusId} not found in tenant {TenantId}", model.StatusId, tenantId);
|
||||
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();
|
||||
// Fetch existing service project for update, ensuring active and tenant scope
|
||||
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);
|
||||
_logger.LogWarning("Attempt to update non-existent service project with ID {ProjectId} by employee {EmployeeId}", id, loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Project Not Found", $"No active project found with ID {id}.", 404);
|
||||
}
|
||||
|
||||
// Map incoming DTO to the tracked entity
|
||||
_mapper.Map(model, serviceProject);
|
||||
|
||||
serviceProject.UpdatedAt = DateTime.UtcNow;
|
||||
serviceProject.UpdatedById = loggedInEmployee.Id;
|
||||
|
||||
var serviceProjectServiceMappings = await _context.ServiceProjectServiceMapping
|
||||
// Get existing service mappings for the project (no tracking as we perform add/remove explicitly)
|
||||
var existingMappings = await _context.ServiceProjectServiceMapping
|
||||
.AsNoTracking()
|
||||
.Where(sps => sps.ProjectId == serviceProject.Id && sps.TenantId == tenantId)
|
||||
.ToListAsync();
|
||||
|
||||
var newMapping = new List<ServiceProjectServiceMapping>();
|
||||
var removedMapping = new List<ServiceProjectServiceMapping>();
|
||||
|
||||
// Determine service mappings to add or remove based on input DTO state
|
||||
var newMappings = new List<ServiceProjectServiceMapping>();
|
||||
var removedMappings = new List<ServiceProjectServiceMapping>();
|
||||
if (model.Services != null)
|
||||
{
|
||||
foreach (var dto in model.Services)
|
||||
{
|
||||
var serviceProjectServiceMapping = serviceProjectServiceMappings
|
||||
.FirstOrDefault(sps => sps.ServiceId == dto.ServiceId);
|
||||
var existingMapping = existingMappings.FirstOrDefault(sps => sps.ServiceId == dto.ServiceId);
|
||||
|
||||
if (dto.IsActive && serviceProjectServiceMapping == null)
|
||||
if (dto.IsActive && existingMapping == null)
|
||||
{
|
||||
newMapping.Add(new ServiceProjectServiceMapping
|
||||
newMappings.Add(new ServiceProjectServiceMapping
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ServiceId = dto.ServiceId,
|
||||
@ -280,17 +407,21 @@ namespace Marco.Pms.Services.Service
|
||||
TenantId = tenantId,
|
||||
});
|
||||
}
|
||||
else if (!dto.IsActive && serviceProjectServiceMapping != null)
|
||||
else if (!dto.IsActive && existingMapping != null)
|
||||
{
|
||||
removedMapping.Add(serviceProjectServiceMapping);
|
||||
removedMappings.Add(existingMapping);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_context.ServiceProjectServiceMapping.AddRange(newMapping);
|
||||
_context.ServiceProjectServiceMapping.RemoveRange(removedMapping);
|
||||
// Apply added and removed mappings
|
||||
if (newMappings.Any()) _context.ServiceProjectServiceMapping.AddRange(newMappings);
|
||||
if (removedMappings.Any()) _context.ServiceProjectServiceMapping.RemoveRange(removedMappings);
|
||||
|
||||
// Persist all changes within a single transaction flow
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Reload updated project and related entities for comprehensive response
|
||||
var serviceProjectTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
@ -298,9 +429,9 @@ namespace Marco.Pms.Services.Service
|
||||
.Include(sp => sp.Client)
|
||||
.Include(sp => sp.Status)
|
||||
.Include(sp => sp.CreatedBy).ThenInclude(e => e!.JobRole)
|
||||
.Where(sp => sp.TenantId == tenantId && sp.IsActive).FirstOrDefaultAsync();
|
||||
.Where(sp => sp.Id == id && sp.TenantId == tenantId && sp.IsActive)
|
||||
.FirstOrDefaultAsync();
|
||||
});
|
||||
|
||||
var servicesTask = Task.Run(async () =>
|
||||
{
|
||||
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||
@ -313,50 +444,67 @@ namespace Marco.Pms.Services.Service
|
||||
|
||||
await Task.WhenAll(serviceProjectTask, servicesTask);
|
||||
|
||||
|
||||
serviceProject = serviceProjectTask.Result;
|
||||
services = servicesTask.Result;
|
||||
|
||||
ServiceProjectVM serviceProjectVm = _mapper.Map<ServiceProjectVM>(serviceProject);
|
||||
serviceProjectVm.Services = _mapper.Map<List<ServiceMasterVM>>(services);
|
||||
var projectVm = _mapper.Map<ServiceProjectVM>(serviceProject);
|
||||
projectVm.Services = _mapper.Map<List<ServiceMasterVM>>(services);
|
||||
|
||||
return ApiResponse<object>.SuccessResponse(serviceProjectVm, "Service Project updated successfully.", 200);
|
||||
_logger.LogInfo("Service project {ProjectId} updated successfully by employee {EmployeeId}", id, loggedInEmployee.Id);
|
||||
|
||||
return ApiResponse<object>.SuccessResponse(projectVm, "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);
|
||||
_logger.LogError(ex, "Unexpected error updating service project {ProjectId} for tenant {TenantId} by employee {EmployeeId}", id, tenantId, loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while updating the service project. Please try again later.", 500);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// Activates or deactivates a service project based on the specified 'isActive' flag, scoped by tenant.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the service project to update.</param>
|
||||
/// <param name="isActive">True to activate, false to deactivate the service project.</param>
|
||||
/// <param name="loggedInEmployee">The employee performing the operation (for audit/logging).</param>
|
||||
/// <param name="tenantId">Tenant identifier to ensure multi-tenant data isolation.</param>
|
||||
/// <returns>ApiResponse indicating success or detail of failure.</returns>
|
||||
public async Task<ApiResponse<object>> DeActivateServiceProjectAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId)
|
||||
{
|
||||
if (tenantId == Guid.Empty)
|
||||
{
|
||||
_logger.LogWarning("DeActivateServiceProjectAsync called with invalid tenant context by employee {EmployeeId}", loggedInEmployee.Id);
|
||||
return ApiResponse<object>.ErrorResponse("Access Denied", "Invalid tenant context.", 403);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
// Load the service project scoped by tenant and ID
|
||||
var serviceProject = await _context.ServiceProjects
|
||||
.Where(sp => sp.Id == id && sp.TenantId == tenantId).FirstOrDefaultAsync();
|
||||
.FirstOrDefaultAsync(sp => sp.Id == id && sp.TenantId == tenantId);
|
||||
|
||||
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);
|
||||
_logger.LogWarning("Attempt to {(Action)} non-existent service project {ProjectId} by employee {EmployeeId} in tenant {TenantId}",
|
||||
isActive ? "activate" : "deactivate", id, loggedInEmployee.Id, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Project Not Found", $"No project found with ID {id}.", 404);
|
||||
}
|
||||
|
||||
// Update active status as requested by the client
|
||||
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);
|
||||
_logger.LogInfo("Service project {ProjectId} {(Action)}d successfully by employee {EmployeeId} in tenant {TenantId}",
|
||||
id, isActive ? "activate" : "deactivate", loggedInEmployee.Id, tenantId);
|
||||
|
||||
return ApiResponse<object>.SuccessResponse(new { }, $"Project {(isActive ? "activated" : "deactivated")} 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);
|
||||
_logger.LogError(ex, "Error occurred while {(Action)} service project {ProjectId} by employee {EmployeeId} in tenant {TenantId}",
|
||||
isActive ? "activating" : "deactivating", id, loggedInEmployee.Id, tenantId);
|
||||
return ApiResponse<object>.ErrorResponse("Internal Server Error", "An internal error occurred, please try again later.", 500);
|
||||
}
|
||||
}
|
||||
|
||||
@ -430,7 +578,6 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Manages service project allocations by adding new active allocations and removing inactive ones.
|
||||
/// Validates projects, employees, and team roles exist before applying changes.
|
||||
@ -1367,6 +1514,7 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region =================================================================== Job Comments Functions ===================================================================
|
||||
|
||||
/// <summary>
|
||||
@ -1615,7 +1763,8 @@ namespace Marco.Pms.Services.Service
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region =================================================================== Pubic Helper Functions ===================================================================
|
||||
|
||||
#region =================================================================== Helper Functions ===================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a job ticket by its unique identifier and associated tenant ID.
|
||||
@ -1652,7 +1801,6 @@ namespace Marco.Pms.Services.Service
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<JobStatusMapping?> GetJobStatusMappingAsync(Guid statusId, Guid nextStatusId, Guid projectId, Guid loggedInEmployeeId, Guid tenantId)
|
||||
{
|
||||
// Find transition mappings for the current status and desired status, considering team role if allocation exists
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user