Enhanced the Create and update project API #138
@ -1,7 +1,9 @@
|
|||||||
using Marco.Pms.DataAccess.Data;
|
using Marco.Pms.DataAccess.Data;
|
||||||
using Marco.Pms.Model.Master;
|
using Marco.Pms.Model.Master;
|
||||||
|
using Marco.Pms.Model.MongoDBModels;
|
||||||
using Marco.Pms.Model.MongoDBModels.Masters;
|
using Marco.Pms.Model.MongoDBModels.Masters;
|
||||||
using Marco.Pms.Model.MongoDBModels.Project;
|
using Marco.Pms.Model.MongoDBModels.Project;
|
||||||
|
using Marco.Pms.Model.OrganizationModel;
|
||||||
using Marco.Pms.Model.Projects;
|
using Marco.Pms.Model.Projects;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
@ -55,7 +57,7 @@ namespace Marco.Pms.Helpers
|
|||||||
var indexModel = new CreateIndexModel<ProjectMongoDB>(indexKeys, indexOptions);
|
var indexModel = new CreateIndexModel<ProjectMongoDB>(indexKeys, indexOptions);
|
||||||
await _projectCollection.Indexes.CreateOneAsync(indexModel);
|
await _projectCollection.Indexes.CreateOneAsync(indexModel);
|
||||||
}
|
}
|
||||||
public async Task<bool> UpdateProjectDetailsOnlyToCache(Project project, StatusMaster projectStatus)
|
public async Task<bool> UpdateProjectDetailsOnlyToCache(Project project, StatusMaster projectStatus, Organization promotor, Organization pmc)
|
||||||
{
|
{
|
||||||
// Build the update definition
|
// Build the update definition
|
||||||
var updates = Builders<ProjectMongoDB>.Update.Combine(
|
var updates = Builders<ProjectMongoDB>.Update.Combine(
|
||||||
@ -67,6 +69,20 @@ namespace Marco.Pms.Helpers
|
|||||||
Id = projectStatus.Id.ToString(),
|
Id = projectStatus.Id.ToString(),
|
||||||
Status = projectStatus.Status
|
Status = projectStatus.Status
|
||||||
}),
|
}),
|
||||||
|
Builders<ProjectMongoDB>.Update.Set(r => r.Promoter, new OrganizationMongoDB
|
||||||
|
{
|
||||||
|
Id = promotor.Id.ToString(),
|
||||||
|
Name = promotor.Name,
|
||||||
|
ContactPerson = promotor.ContactPerson,
|
||||||
|
Email = promotor.Email
|
||||||
|
}),
|
||||||
|
Builders<ProjectMongoDB>.Update.Set(r => r.PMC, new OrganizationMongoDB
|
||||||
|
{
|
||||||
|
Id = pmc.Id.ToString(),
|
||||||
|
Name = pmc.Name,
|
||||||
|
ContactPerson = pmc.ContactPerson,
|
||||||
|
Email = pmc.Email
|
||||||
|
}),
|
||||||
Builders<ProjectMongoDB>.Update.Set(r => r.StartDate, project.StartDate),
|
Builders<ProjectMongoDB>.Update.Set(r => r.StartDate, project.StartDate),
|
||||||
Builders<ProjectMongoDB>.Update.Set(r => r.EndDate, project.EndDate),
|
Builders<ProjectMongoDB>.Update.Set(r => r.EndDate, project.EndDate),
|
||||||
Builders<ProjectMongoDB>.Update.Set(r => r.ContactPerson, project.ContactPerson)
|
Builders<ProjectMongoDB>.Update.Set(r => r.ContactPerson, project.ContactPerson)
|
||||||
|
@ -7,17 +7,18 @@ namespace Marco.Pms.Model.Dtos.Project
|
|||||||
{
|
{
|
||||||
[Required(ErrorMessage = "Project Name is required!")]
|
[Required(ErrorMessage = "Project Name is required!")]
|
||||||
[DisplayName("Project Name")]
|
[DisplayName("Project Name")]
|
||||||
public string? Name { get; set; }
|
public required string Name { get; set; }
|
||||||
|
|
||||||
[DisplayName("Short Name")]
|
[DisplayName("Short Name")]
|
||||||
public string? ShortName { get; set; }
|
public string? ShortName { get; set; }
|
||||||
|
|
||||||
[DisplayName("Project Address")]
|
[DisplayName("Project Address")]
|
||||||
[Required(ErrorMessage = "Project Address is required!")]
|
[Required(ErrorMessage = "Project Address is required!")]
|
||||||
public string? ProjectAddress { get; set; }
|
public required string ProjectAddress { get; set; }
|
||||||
|
|
||||||
|
|
||||||
[DisplayName("Contact Person")]
|
[DisplayName("Contact Person")]
|
||||||
public string? ContactPerson { get; set; }
|
public required string ContactPerson { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public DateTime? StartDate { get; set; }
|
public DateTime? StartDate { get; set; }
|
||||||
@ -25,6 +26,8 @@ namespace Marco.Pms.Model.Dtos.Project
|
|||||||
|
|
||||||
[DisplayName("Project Status")]
|
[DisplayName("Project Status")]
|
||||||
[Required(ErrorMessage = "Project Status is required!")]
|
[Required(ErrorMessage = "Project Status is required!")]
|
||||||
public Guid ProjectStatusId { get; set; }
|
public required Guid ProjectStatusId { get; set; }
|
||||||
|
public required Guid PromoterId { get; set; }
|
||||||
|
public required Guid PMCId { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,20 +5,20 @@ namespace Marco.Pms.Model.Dtos.Project
|
|||||||
{
|
{
|
||||||
public class UpdateProjectDto
|
public class UpdateProjectDto
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public required Guid Id { get; set; }
|
||||||
[Required(ErrorMessage = "Project Name is required!")]
|
[Required(ErrorMessage = "Project Name is required!")]
|
||||||
[DisplayName("Project Name")]
|
[DisplayName("Project Name")]
|
||||||
public string? Name { get; set; }
|
public required string Name { get; set; }
|
||||||
[DisplayName("Short Name")]
|
[DisplayName("Short Name")]
|
||||||
public string? ShortName { get; set; }
|
public string? ShortName { get; set; }
|
||||||
|
|
||||||
[DisplayName("Project Address")]
|
[DisplayName("Project Address")]
|
||||||
[Required(ErrorMessage = "Project Address is required!")]
|
[Required(ErrorMessage = "Project Address is required!")]
|
||||||
public string? ProjectAddress { get; set; }
|
public required string ProjectAddress { get; set; }
|
||||||
|
|
||||||
|
|
||||||
[DisplayName("Contact Person")]
|
[DisplayName("Contact Person")]
|
||||||
public string? ContactPerson { get; set; }
|
public required string ContactPerson { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public DateTime? StartDate { get; set; }
|
public DateTime? StartDate { get; set; }
|
||||||
@ -26,6 +26,8 @@ namespace Marco.Pms.Model.Dtos.Project
|
|||||||
|
|
||||||
[DisplayName("Project Status")]
|
[DisplayName("Project Status")]
|
||||||
[Required(ErrorMessage = "Project Status is required!")]
|
[Required(ErrorMessage = "Project Status is required!")]
|
||||||
public Guid ProjectStatusId { get; set; }
|
public required Guid ProjectStatusId { get; set; }
|
||||||
|
public required Guid PromoterId { get; set; }
|
||||||
|
public required Guid PMCId { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
Marco.Pms.Model/MongoDBModels/OrganizationMongoDB.cs
Normal file
10
Marco.Pms.Model/MongoDBModels/OrganizationMongoDB.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace Marco.Pms.Model.MongoDBModels
|
||||||
|
{
|
||||||
|
public class OrganizationMongoDB
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
public string ContactPerson { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,8 @@ namespace Marco.Pms.Model.MongoDBModels.Project
|
|||||||
public DateTime? StartDate { get; set; }
|
public DateTime? StartDate { get; set; }
|
||||||
public DateTime? EndDate { get; set; }
|
public DateTime? EndDate { get; set; }
|
||||||
public StatusMasterMongoDB? ProjectStatus { get; set; }
|
public StatusMasterMongoDB? ProjectStatus { get; set; }
|
||||||
|
public OrganizationMongoDB? Promoter { get; set; }
|
||||||
|
public OrganizationMongoDB? PMC { get; set; }
|
||||||
public int TeamSize { get; set; }
|
public int TeamSize { get; set; }
|
||||||
public double CompletedWork { get; set; }
|
public double CompletedWork { get; set; }
|
||||||
public double PlannedWork { get; set; }
|
public double PlannedWork { get; set; }
|
||||||
|
@ -12,6 +12,7 @@ namespace Marco.Pms.Model.ViewModels.Tenant
|
|||||||
public string ContactNumber { get; set; } = string.Empty;
|
public string ContactNumber { get; set; } = string.Empty;
|
||||||
public string? logoImage { get; set; } // Base64
|
public string? logoImage { get; set; } // Base64
|
||||||
public string? OrganizationSize { get; set; }
|
public string? OrganizationSize { get; set; }
|
||||||
|
public Guid OrganizationId { get; set; }
|
||||||
public Industry? Industry { get; set; }
|
public Industry? Industry { get; set; }
|
||||||
public TenantStatus? TenantStatus { get; set; }
|
public TenantStatus? TenantStatus { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -1294,33 +1294,37 @@ namespace MarcoBMS.Services.Controllers
|
|||||||
_logger.LogInfo("Fetching TenantOrgMappings for OrganizationId: {OrganizationId}", organizationId);
|
_logger.LogInfo("Fetching TenantOrgMappings for OrganizationId: {OrganizationId}", organizationId);
|
||||||
|
|
||||||
// Retrieve all TenantOrgMappings that match the organizationId and have a related Tenant
|
// Retrieve all TenantOrgMappings that match the organizationId and have a related Tenant
|
||||||
var tenantOrganizationMapping = await _context.TenantOrgMappings
|
var tenantOrgMappingTask = Task.Run(async () =>
|
||||||
.Include(to => to.Tenant)
|
{
|
||||||
.ThenInclude(t => t!.TenantStatus)
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
.Include(to => to.Tenant)
|
return await context.TenantOrgMappings.Where(to => to.OrganizationId == organizationId && to.Tenant != null).Select(to => to.TenantId).ToListAsync();
|
||||||
.ThenInclude(t => t!.Industry)
|
});
|
||||||
.Where(to => to.OrganizationId == organizationId && to.Tenant != null)
|
var projectTask = Task.Run(async () =>
|
||||||
.ToListAsync();
|
{
|
||||||
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await context.Projects.Where(to => to.PromoterId == organizationId || to.PMCId == organizationId).Select(to => to.TenantId).ToListAsync();
|
||||||
|
});
|
||||||
|
|
||||||
var tenantList = tenantOrganizationMapping.Select(to => to.Tenant!).ToList();
|
await Task.WhenAll(tenantOrgMappingTask, projectTask);
|
||||||
|
|
||||||
|
var tenantIds = tenantOrgMappingTask.Result;
|
||||||
|
|
||||||
|
tenantIds.AddRange(projectTask.Result);
|
||||||
|
|
||||||
|
tenantIds = tenantIds.Distinct().ToList();
|
||||||
|
|
||||||
// Additionally fetch the Tenant record associated directly with this OrganizationId if any
|
// Additionally fetch the Tenant record associated directly with this OrganizationId if any
|
||||||
var tenant = await _context.Tenants
|
var tenants = await _context.Tenants
|
||||||
.Include(t => t.Industry)
|
.Include(t => t.Industry)
|
||||||
.Include(t => t.TenantStatus)
|
.Include(t => t.TenantStatus)
|
||||||
.FirstOrDefaultAsync(t => t.OrganizationId == organizationId);
|
.Where(t => t.OrganizationId == organizationId || tenantIds.Contains(t.Id)).ToListAsync();
|
||||||
if (tenant != null)
|
|
||||||
{
|
|
||||||
tenantList.Add(tenant);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
tenants = tenants.Distinct().ToList();
|
||||||
tenantList = tenantList.Distinct().ToList();
|
|
||||||
|
|
||||||
// Map the tenant entities to TenantListVM view models
|
// Map the tenant entities to TenantListVM view models
|
||||||
var response = _mapper.Map<List<TenantListVM>>(tenantList);
|
var response = _mapper.Map<List<TenantListVM>>(tenants);
|
||||||
|
|
||||||
_logger.LogInfo("Fetched {Count} tenants for OrganizationId: {OrganizationId}", tenantList.Count, organizationId);
|
_logger.LogInfo("Fetched {Count} tenants for OrganizationId: {OrganizationId}", tenants.Count, organizationId);
|
||||||
_logger.LogDebug("GetTenantAsync method completed successfully.");
|
_logger.LogDebug("GetTenantAsync method completed successfully.");
|
||||||
|
|
||||||
return Ok(ApiResponse<object>.SuccessResponse(response, "Successfully fetched the list of tenant", 200));
|
return Ok(ApiResponse<object>.SuccessResponse(response, "Successfully fetched the list of tenant", 200));
|
||||||
|
@ -10,6 +10,7 @@ using Marco.Pms.Model.MongoDBModels.Expenses;
|
|||||||
using Marco.Pms.Model.MongoDBModels.Masters;
|
using Marco.Pms.Model.MongoDBModels.Masters;
|
||||||
using Marco.Pms.Model.MongoDBModels.Project;
|
using Marco.Pms.Model.MongoDBModels.Project;
|
||||||
using Marco.Pms.Model.MongoDBModels.Utility;
|
using Marco.Pms.Model.MongoDBModels.Utility;
|
||||||
|
using Marco.Pms.Model.OrganizationModel;
|
||||||
using Marco.Pms.Model.Projects;
|
using Marco.Pms.Model.Projects;
|
||||||
using Marco.Pms.Model.Utilities;
|
using Marco.Pms.Model.Utilities;
|
||||||
using MarcoBMS.Services.Service;
|
using MarcoBMS.Services.Service;
|
||||||
@ -69,6 +70,39 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
.Where(s => s.Id == project.ProjectStatusId)
|
.Where(s => s.Id == project.ProjectStatusId)
|
||||||
.Select(s => new { s.Id, s.Status }) // Projection
|
.Select(s => new { s.Id, s.Status }) // Projection
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
var promotorTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
using var context = _dbContextFactory.CreateDbContext();
|
||||||
|
return await context.Organizations
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(o => o.Id == project.PromoterId)
|
||||||
|
.Select(o => new OrganizationMongoDB
|
||||||
|
{
|
||||||
|
Id = o.Id.ToString(),
|
||||||
|
Name = o.Name,
|
||||||
|
ContactPerson = o.ContactPerson,
|
||||||
|
Email = o.Email
|
||||||
|
}) // Projection
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
});
|
||||||
|
|
||||||
|
var pmcTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
using var context = _dbContextFactory.CreateDbContext();
|
||||||
|
return await context.Organizations
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(o => o.Id == project.PMCId)
|
||||||
|
.Select(o => new OrganizationMongoDB
|
||||||
|
{
|
||||||
|
Id = o.Id.ToString(),
|
||||||
|
Name = o.Name,
|
||||||
|
ContactPerson = o.ContactPerson,
|
||||||
|
Email = o.Email
|
||||||
|
}) // Projection
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
});
|
});
|
||||||
|
|
||||||
var teamSizeTask = Task.Run(async () =>
|
var teamSizeTask = Task.Run(async () =>
|
||||||
@ -120,12 +154,15 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Wait for all parallel database operations to complete.
|
// Wait for all parallel database operations to complete.
|
||||||
await Task.WhenAll(statusTask, teamSizeTask, infrastructureTask);
|
await Task.WhenAll(statusTask, teamSizeTask, infrastructureTask, promotorTask, pmcTask);
|
||||||
|
|
||||||
// Get the results from the completed tasks.
|
// Get the results from the completed tasks.
|
||||||
var status = await statusTask;
|
|
||||||
var teamSize = await teamSizeTask;
|
var status = statusTask.Result;
|
||||||
var (allBuildings, allFloors, allWorkAreas, workSummariesByWorkAreaId) = await infrastructureTask;
|
var teamSize = teamSizeTask.Result;
|
||||||
|
var (allBuildings, allFloors, allWorkAreas, workSummariesByWorkAreaId) = infrastructureTask.Result;
|
||||||
|
var promotor = promotorTask.Result;
|
||||||
|
var pmc = pmcTask.Result;
|
||||||
|
|
||||||
// --- Step 2: Process the fetched data and build the MongoDB model ---
|
// --- Step 2: Process the fetched data and build the MongoDB model ---
|
||||||
|
|
||||||
@ -216,6 +253,8 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
projectDetails.Buildings = buildingMongoList;
|
projectDetails.Buildings = buildingMongoList;
|
||||||
projectDetails.PlannedWork = totalPlannedWork;
|
projectDetails.PlannedWork = totalPlannedWork;
|
||||||
projectDetails.CompletedWork = totalCompletedWork;
|
projectDetails.CompletedWork = totalCompletedWork;
|
||||||
|
projectDetails.Promoter = promotor;
|
||||||
|
projectDetails.PMC = pmc;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -234,6 +273,8 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
return; // Nothing to do
|
return; // Nothing to do
|
||||||
}
|
}
|
||||||
var projectStatusIds = projects.Select(p => p.ProjectStatusId).Distinct().ToList();
|
var projectStatusIds = projects.Select(p => p.ProjectStatusId).Distinct().ToList();
|
||||||
|
var promotorIds = projects.Select(p => p.PromoterId).Distinct().ToList();
|
||||||
|
var pmcsIds = projects.Select(p => p.PMCId).Distinct().ToList();
|
||||||
|
|
||||||
// --- Step 1: Fetch all required data in maximum parallel ---
|
// --- Step 1: Fetch all required data in maximum parallel ---
|
||||||
// Each task uses its own DbContext and selects only the required columns (projection).
|
// Each task uses its own DbContext and selects only the required columns (projection).
|
||||||
@ -248,6 +289,22 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
.ToDictionaryAsync(s => s.Id);
|
.ToDictionaryAsync(s => s.Id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var organizationTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
using var context = _dbContextFactory.CreateDbContext();
|
||||||
|
return await context.Organizations
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(o => promotorIds.Contains(o.Id) || pmcsIds.Contains(o.Id))
|
||||||
|
.Select(o => new OrganizationMongoDB
|
||||||
|
{
|
||||||
|
Id = o.Id.ToString(),
|
||||||
|
Name = o.Name,
|
||||||
|
ContactPerson = o.ContactPerson,
|
||||||
|
Email = o.Email
|
||||||
|
}) // Projection
|
||||||
|
.ToListAsync();
|
||||||
|
});
|
||||||
|
|
||||||
var teamSizeTask = Task.Run(async () =>
|
var teamSizeTask = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
using var context = _dbContextFactory.CreateDbContext();
|
using var context = _dbContextFactory.CreateDbContext();
|
||||||
@ -322,20 +379,22 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Await the remaining parallel tasks.
|
// Await the remaining parallel tasks.
|
||||||
await Task.WhenAll(statusTask, teamSizeTask, workAreasTask, workSummaryTask);
|
await Task.WhenAll(statusTask, teamSizeTask, workAreasTask, workSummaryTask, organizationTask);
|
||||||
|
|
||||||
// --- Step 2: Process the fetched data and build the MongoDB models ---
|
// --- Step 2: Process the fetched data and build the MongoDB models ---
|
||||||
|
|
||||||
var allStatuses = await statusTask;
|
var allStatuses = statusTask.Result;
|
||||||
var teamSizesByProjectId = await teamSizeTask;
|
var teamSizesByProjectId = teamSizeTask.Result;
|
||||||
var allWorkAreas = await workAreasTask;
|
var allWorkAreas = workAreasTask.Result;
|
||||||
var workSummariesByWorkAreaId = await workSummaryTask;
|
var workSummariesByWorkAreaId = workSummaryTask.Result;
|
||||||
|
var organizations = organizationTask.Result;
|
||||||
|
|
||||||
// Create fast in-memory lookups for hierarchical data
|
// Create fast in-memory lookups for hierarchical data
|
||||||
var buildingsByProjectId = allBuildings.ToLookup(b => b.ProjectId);
|
var buildingsByProjectId = allBuildings.ToLookup(b => b.ProjectId);
|
||||||
var floorsByBuildingId = allFloors.ToLookup(f => f.BuildingId);
|
var floorsByBuildingId = allFloors.ToLookup(f => f.BuildingId);
|
||||||
var workAreasByFloorId = allWorkAreas.ToLookup(wa => wa.FloorId);
|
var workAreasByFloorId = allWorkAreas.ToLookup(wa => wa.FloorId);
|
||||||
|
|
||||||
|
|
||||||
var projectDetailsList = new List<ProjectMongoDB>(projects.Count);
|
var projectDetailsList = new List<ProjectMongoDB>(projects.Count);
|
||||||
foreach (var project in projects)
|
foreach (var project in projects)
|
||||||
{
|
{
|
||||||
@ -427,6 +486,8 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
projectDetails.Buildings = buildingMongoList;
|
projectDetails.Buildings = buildingMongoList;
|
||||||
projectDetails.PlannedWork = totalPlannedWork;
|
projectDetails.PlannedWork = totalPlannedWork;
|
||||||
projectDetails.CompletedWork = totalCompletedWork;
|
projectDetails.CompletedWork = totalCompletedWork;
|
||||||
|
projectDetails.Promoter = organizations.FirstOrDefault(o => o.Id == project.PromoterId.ToString());
|
||||||
|
projectDetails.PMC = organizations.FirstOrDefault(o => o.Id == project.PMCId.ToString());
|
||||||
|
|
||||||
projectDetailsList.Add(projectDetails);
|
projectDetailsList.Add(projectDetails);
|
||||||
}
|
}
|
||||||
@ -443,11 +504,32 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
}
|
}
|
||||||
public async Task<bool> UpdateProjectDetailsOnly(Project project)
|
public async Task<bool> UpdateProjectDetailsOnly(Project project)
|
||||||
{
|
{
|
||||||
StatusMaster projectStatus = await _context.StatusMasters
|
var projectStatusTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await context.StatusMasters
|
||||||
.FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId) ?? new StatusMaster();
|
.FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId) ?? new StatusMaster();
|
||||||
|
});
|
||||||
|
var promotorTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == project.PromoterId) ?? new Organization();
|
||||||
|
});
|
||||||
|
var pmcTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await context.Organizations.FirstOrDefaultAsync(o => o.Id == project.PMCId) ?? new Organization();
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(projectStatusTask, promotorTask, pmcTask);
|
||||||
|
|
||||||
|
var projectStatus = projectStatusTask.Result;
|
||||||
|
var promotor = promotorTask.Result;
|
||||||
|
var pmc = pmcTask.Result;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project, projectStatus);
|
bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project, projectStatus, promotor, pmc);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -351,63 +351,110 @@ namespace Marco.Pms.Services.Service
|
|||||||
|
|
||||||
#region =================================================================== Project Manage APIs ===================================================================
|
#region =================================================================== Project Manage APIs ===================================================================
|
||||||
|
|
||||||
public async Task<ApiResponse<object>> CreateProjectAsync(CreateProjectDto projectDto, Guid tenantId, Employee loggedInEmployee)
|
public async Task<ApiResponse<object>> CreateProjectAsync(CreateProjectDto model, Guid tenantId, Employee loggedInEmployee)
|
||||||
{
|
{
|
||||||
|
// Begin a new scope for service resolution.
|
||||||
using var scope = _serviceScopeFactory.CreateScope();
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
|
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
|
||||||
|
|
||||||
// 1. Prepare data without I/O
|
// Step 1: Validate tenant access for the current employee.
|
||||||
|
var tenant = await _context.Tenants
|
||||||
|
.FirstOrDefaultAsync(t => t.Id == tenantId && t.OrganizationId == loggedInEmployee.OrganizationId);
|
||||||
|
|
||||||
|
if (tenant == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Access DENIED (OrgId:{OrgId}) by Employee {EmployeeId} for tenantId={TenantId}.",
|
||||||
|
loggedInEmployee.OrganizationId, loggedInEmployee.Id, tenantId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to create a project for this tenant.", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Concurrent validation for Promoter and PMC organization existence.
|
||||||
|
// Run database queries in parallel for better performance.
|
||||||
|
var promoterExistsTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await context.Organizations.AnyAsync(o => o.Id == model.PromoterId);
|
||||||
|
});
|
||||||
|
var pmcExistsTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await context.Organizations.AnyAsync(o => o.Id == model.PMCId);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(promoterExistsTask, pmcExistsTask);
|
||||||
|
|
||||||
|
bool promoterExists = promoterExistsTask.Result;
|
||||||
|
bool pmcExists = pmcExistsTask.Result;
|
||||||
|
|
||||||
|
if (!promoterExists)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Promoter check failed. PromoterId={PromoterId} not found.", model.PromoterId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Promoter not found", "Promoter not found in database.", 404);
|
||||||
|
}
|
||||||
|
if (!pmcExists)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("PMC check failed. PMCId={PMCId} not found.", model.PMCId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("PMC not found", "PMC not found in database.", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Prepare the project entity.
|
||||||
var loggedInUserId = loggedInEmployee.Id;
|
var loggedInUserId = loggedInEmployee.Id;
|
||||||
var project = _mapper.Map<Project>(projectDto);
|
var project = _mapper.Map<Project>(model);
|
||||||
project.TenantId = tenantId;
|
project.TenantId = tenantId;
|
||||||
|
|
||||||
// 2. Store it to database
|
// Step 4: Save the new project to the database.
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_context.Projects.Add(project);
|
_context.Projects.Add(project);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
_logger.LogInfo("Project {ProjectId} created successfully for TenantId={TenantId}, by Employee {EmployeeId}.",
|
||||||
|
project.Id, tenantId, loggedInUserId);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Log the detailed exception
|
_logger.LogError(ex, "DB Failure: Project creation failed for TenantId={TenantId}. Rolling back.", tenantId);
|
||||||
_logger.LogError(ex, "Failed to create project in database. Rolling back transaction.");
|
|
||||||
// Return a server error as the primary operation failed
|
|
||||||
return ApiResponse<object>.ErrorResponse("An error occurred while saving the project.", ex.Message, 500);
|
return ApiResponse<object>.ErrorResponse("An error occurred while saving the project.", ex.Message, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Perform non-critical side-effects (caching, notifications) concurrently
|
// Step 5: Perform non-critical post-save side effects (e.g., caching) in parallel.
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// These operations do not depend on each other, so they can run in parallel.
|
var cacheAddDetailsTask = _cache.AddProjectDetails(project);
|
||||||
Task cacheAddDetailsTask = _cache.AddProjectDetails(project);
|
var cacheClearListTask = _cache.ClearAllProjectIdsByPermissionId(PermissionsMaster.ManageProject, tenantId);
|
||||||
Task cacheClearListTask = _cache.ClearAllProjectIdsByPermissionId(PermissionsMaster.ManageProject, tenantId);
|
|
||||||
|
|
||||||
// Await all side-effect tasks to complete in parallel
|
|
||||||
await Task.WhenAll(cacheAddDetailsTask, cacheClearListTask);
|
await Task.WhenAll(cacheAddDetailsTask, cacheClearListTask);
|
||||||
|
|
||||||
|
_logger.LogInfo("Cache updated for ProjectId={ProjectId} after creation.", project.Id);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// The project was created successfully, but a side-effect failed.
|
// Log the issue but do not interrupt the user flow.
|
||||||
// Log this as a warning, as the primary operation succeeded. Don't return an error to the user.
|
_logger.LogError(ex, "Post-save cache operation failed for ProjectId={ProjectId}.", project.Id);
|
||||||
_logger.LogError(ex, "Project {ProjectId} was created, but a post-creation side-effect (caching/notification) failed. ", project.Id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Step 6: Fire-and-forget push notification for post-creation event. Designed to be non-blocking.
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
// --- Push Notification Section ---
|
try
|
||||||
// This section attempts to send a test push notification to the user's device.
|
{
|
||||||
// It's designed to fail gracefully and handle invalid Firebase Cloud Messaging (FCM) tokens.
|
var name = $"{loggedInEmployee.FirstName} {loggedInEmployee.LastName}";
|
||||||
|
await _firebase.SendModifyProjectMessageAsync(project, name, false, tenantId);
|
||||||
var name = $"{loggedInEmployee.FirstName} {loggedInEmployee.LastName}";
|
_logger.LogInfo("Push notification sent for ProjectId={ProjectId} ({Name}).", project.Id, name);
|
||||||
|
}
|
||||||
await _firebase.SendModifyProjectMessageAsync(project, name, false, tenantId);
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Push notification sending failed for ProjectId={ProjectId}.", project.Id);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. Return a success response to the user as soon as the critical data is saved.
|
// Step 7: Return success response as soon as critical operation is complete.
|
||||||
return ApiResponse<object>.SuccessResponse(_mapper.Map<ProjectDto>(project), "Project created successfully.", 200);
|
var resultDto = _mapper.Map<ProjectDto>(project);
|
||||||
|
_logger.LogInfo("Returning success response for ProjectId={ProjectId}.", project.Id);
|
||||||
|
|
||||||
|
return ApiResponse<object>.SuccessResponse(resultDto, "Project created successfully.", 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates an existing project's details.
|
/// Updates an existing project's details.
|
||||||
/// This endpoint is secure, handles concurrency, and performs non-essential tasks in the background.
|
/// This endpoint is secure, handles concurrency, and performs non-essential tasks in the background.
|
||||||
@ -415,7 +462,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
/// <param name="id">The ID of the project to update.</param>
|
/// <param name="id">The ID of the project to update.</param>
|
||||||
/// <param name="updateProjectDto">The data to update the project with.</param>
|
/// <param name="updateProjectDto">The data to update the project with.</param>
|
||||||
/// <returns>An ApiResponse confirming the update or an appropriate error.</returns>
|
/// <returns>An ApiResponse confirming the update or an appropriate error.</returns>
|
||||||
public async Task<ApiResponse<object>> UpdateProjectAsync(Guid id, UpdateProjectDto updateProjectDto, Guid tenantId, Employee loggedInEmployee)
|
public async Task<ApiResponse<object>> UpdateProjectAsync(Guid id, UpdateProjectDto model, Guid tenantId, Employee loggedInEmployee)
|
||||||
{
|
{
|
||||||
using var scope = _serviceScopeFactory.CreateScope();
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
|
var _firebase = scope.ServiceProvider.GetRequiredService<IFirebaseService>();
|
||||||
@ -436,7 +483,46 @@ namespace Marco.Pms.Services.Service
|
|||||||
return ApiResponse<object>.ErrorResponse("Project not found.", $"No project found with ID {id}.", 404);
|
return ApiResponse<object>.ErrorResponse("Project not found.", $"No project found with ID {id}.", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1b. Security Check
|
var tenant = await _context.Tenants
|
||||||
|
.FirstOrDefaultAsync(t => t.Id == tenantId && t.OrganizationId == loggedInEmployee.OrganizationId);
|
||||||
|
|
||||||
|
if (tenant == null && existingProject.PMCId == loggedInEmployee.OrganizationId)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Access DENIED (OrgId:{OrgId}) by Employee {EmployeeId} for tenantId={TenantId}.",
|
||||||
|
loggedInEmployee.OrganizationId, loggedInEmployee.Id, tenantId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to update a project for this tenant.", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1bb. Concurrent validation for Promoter and PMC organization existence.
|
||||||
|
// Run database queries in parallel for better performance.
|
||||||
|
var promoterExistsTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await context.Organizations.AnyAsync(o => o.Id == model.PromoterId);
|
||||||
|
});
|
||||||
|
var pmcExistsTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await using var context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
return await context.Organizations.AnyAsync(o => o.Id == model.PMCId);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Task.WhenAll(promoterExistsTask, pmcExistsTask);
|
||||||
|
|
||||||
|
bool promoterExists = promoterExistsTask.Result;
|
||||||
|
bool pmcExists = pmcExistsTask.Result;
|
||||||
|
|
||||||
|
if (!promoterExists)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Promoter check failed. PromoterId={PromoterId} not found.", model.PromoterId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("Promoter not found", "Promoter not found in database.", 404);
|
||||||
|
}
|
||||||
|
if (!pmcExists)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("PMC check failed. PMCId={PMCId} not found.", model.PMCId);
|
||||||
|
return ApiResponse<object>.ErrorResponse("PMC not found", "PMC not found in database.", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1c. Security Check
|
||||||
var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, id);
|
var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, id);
|
||||||
if (!hasPermission)
|
if (!hasPermission)
|
||||||
{
|
{
|
||||||
@ -447,7 +533,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
// --- Step 2: Apply Changes and Save ---
|
// --- Step 2: Apply Changes and Save ---
|
||||||
// Map the changes from the DTO onto the entity we just fetched from the database.
|
// Map the changes from the DTO onto the entity we just fetched from the database.
|
||||||
// This only modifies the properties defined in the mapping, preventing data loss.
|
// This only modifies the properties defined in the mapping, preventing data loss.
|
||||||
_mapper.Map(updateProjectDto, existingProject);
|
_mapper.Map(model, existingProject);
|
||||||
|
|
||||||
// Mark the entity as modified (if your mapping doesn't do it automatically).
|
// Mark the entity as modified (if your mapping doesn't do it automatically).
|
||||||
_context.Entry(existingProject).State = EntityState.Modified;
|
_context.Entry(existingProject).State = EntityState.Modified;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user