Directory_Refactor #128

Merged
ashutosh.nehete merged 22 commits from Directory_Refactor into Document_Manager 2025-09-09 11:03:46 +00:00
5 changed files with 221 additions and 36 deletions
Showing only changes of commit 6688b76145 - Show all commits

View File

@ -25,8 +25,30 @@ namespace Marco.Pms.Helpers.Utility
#region =================================================================== Update Log Helper Functions ===================================================================
public async Task PushToUpdateLogsAsync(UpdateLogsObject oldObject, string collectionName)
{
var collection = _mongoDatabase.GetCollection<UpdateLogsObject>(collectionName);
await collection.InsertOneAsync(oldObject);
try
{
var collection = _mongoDatabase.GetCollection<UpdateLogsObject>(collectionName);
await collection.InsertOneAsync(oldObject);
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occured while saving object of update logs in collection: {Collection}", collectionName);
}
}
public async Task PushListToUpdateLogsAsync(List<UpdateLogsObject> oldObjects, string collectionName)
{
try
{
var collection = _mongoDatabase.GetCollection<UpdateLogsObject>(collectionName);
if (oldObjects.Any())
{
await collection.InsertManyAsync(oldObjects);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occured while saving list of update logs in collection: {Collection}", collectionName);
}
}
public async Task<List<UpdateLogsObject>> GetFromUpdateLogsByEntityIdAsync(Guid entityId, string collectionName)

View File

@ -80,6 +80,13 @@ namespace Marco.Pms.Services.Controllers
var response = await _directoryService.GetOrganizationListAsync(tenantId, loggedInEmployee);
return Ok(response);
}
[HttpGet("designations")]
public async Task<IActionResult> GetDesignationList()
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _directoryService.GetDesignationListAsync(tenantId, loggedInEmployee);
return Ok(response);
}
#endregion

View File

@ -251,9 +251,11 @@ namespace Marco.Pms.Services.MappingProfiles
CreateMap<ContactPhone, ContactPhoneVM>();
CreateMap<CreateContactPhoneDto, ContactPhone>();
CreateMap<UpdateContactPhoneDto, ContactPhone>();
CreateMap<ContactEmail, ContactEmailVM>();
CreateMap<CreateContactEmailDto, ContactEmail>();
CreateMap<UpdateContactEmailDto, ContactEmail>();
CreateMap<ContactCategoryMaster, ContactCategoryVM>();

View File

@ -1,10 +1,12 @@
using AutoMapper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Helpers.Utility;
using Marco.Pms.Model.Directory;
using Marco.Pms.Model.Dtos.Directory;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Mapper;
using Marco.Pms.Model.MongoDBModels.Utility;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Directory;
using Marco.Pms.Model.ViewModels.Master;
@ -26,7 +28,13 @@ namespace Marco.Pms.Services.Service
private readonly ILoggingService _logger;
private readonly UserHelper _userHelper;
private readonly IMapper _mapper;
private readonly PermissionServices _permissionServices;
private readonly UtilityMongoDBHelper _updateLogsHelper;
private static readonly string contactCollection = "ContactModificationLog";
private static readonly string contactPhoneCollection = "ContactPhoneModificationLog";
private static readonly string contactEmailCollection = "ContactEmailModificationLog";
public DirectoryService(
IDbContextFactory<ApplicationDbContext> dbContextFactory,
@ -34,8 +42,7 @@ namespace Marco.Pms.Services.Service
ILoggingService logger,
IServiceScopeFactory serviceScopeFactory,
UserHelper userHelper,
IMapper mapper,
PermissionServices permissionServices)
IMapper mapper)
{
_dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory));
_context = context ?? throw new ArgumentNullException(nameof(context));
@ -43,7 +50,8 @@ namespace Marco.Pms.Services.Service
_serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory));
_userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
_permissionServices = permissionServices ?? throw new ArgumentNullException(nameof(permissionServices));
using var scope = serviceScopeFactory.CreateScope();
_updateLogsHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
}
#region =================================================================== Contact APIs ===================================================================
@ -747,6 +755,66 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("An internal server error occurred.", ExceptionMapper(ex), 500);
}
}
public async Task<ApiResponse<object>> GetDesignationListAsync(Guid tenantId, Employee loggedInEmployee)
{
// --- Parameter Validation ---
// Fail fast if essential parameters are not provided.
ArgumentNullException.ThrowIfNull(loggedInEmployee);
var employeeId = loggedInEmployee.Id;
try
{
// --- 1. Permission Check ---
// Verify that the employee has at least one of the required permissions to view this data.
// This prevents unauthorized data access early in the process.
var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(employeeId);
if (!hasAdminPermission && !hasManagerPermission && !hasUserPermission)
{
// Log the specific denial reason for security auditing.
_logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get designations list for tenant {TenantId} due to lack of permissions.", employeeId, tenantId);
// Return a strongly-typed error response.
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to perform this action.", 403);
}
// --- 2. Database Query ---
// Build and execute the database query efficiently.
_logger.LogDebug("Fetching designations list for Tenant {TenantId} by Employee {EmployeeId}", tenantId, employeeId);
var designationsList = await _context.Contacts
// Filter contacts by the specified tenant to ensure data isolation and Filter out contacts that do not have an designations name to ensure data quality.
.Where(c => c.TenantId == tenantId && !string.IsNullOrEmpty(c.Designation))
// Project only the 'Designation' column. This is a major performance optimization
// as it avoids loading entire 'Contact' entities into memory.
.Select(c => c.Designation)
// Let the database perform the distinct operation, which is highly efficient.
.Distinct()
// Execute the query asynchronously and materialize the results into a list.
.ToListAsync();
// --- 3. Success Response ---
// Log the successful operation with key details.
_logger.LogInfo("Successfully fetched {DesignationsCount} distinct designations for Tenant {TenantId} for employee {EmployeeId}", designationsList.Count, tenantId, employeeId);
// Return a strongly-typed success response with the data and a descriptive message.
return ApiResponse<object>.SuccessResponse(
designationsList,
$"{designationsList.Count} unique designation(s) found.",
200
);
}
catch (Exception ex)
{
// --- 4. Exception Handling ---
// Log the full exception details for effective debugging, including context.
_logger.LogError(ex, "An unexpected error occurred while fetching designation list for Tenant {TenantId} by Employee {EmployeeId}", tenantId, employeeId);
// Return a generic, strongly-typed error response to the client to avoid leaking implementation details.
return ApiResponse<object>.ErrorResponse("An internal server error occurred.", ExceptionMapper(ex), 500);
}
}
#endregion
@ -968,6 +1036,19 @@ namespace Marco.Pms.Services.Service
var allTags = await context.ContactTagMasters.Where(t => t.TenantId == tenantId).ToListAsync();
var tagNameLookup = allTags.ToDictionary(t => t.Name.ToLowerInvariant(), t => t);
var contactObject = _updateLogsHelper.EntityToBsonDocument(contact);
var contactUpdateLog = new UpdateLogsObject
{
EntityId = contact.Id.ToString(),
UpdatedById = loggedInEmployee.Id.ToString(),
OldObject = contactObject,
UpdatedAt = DateTime.UtcNow
};
List<UpdateLogsObject> phoneUpdateLogs = new List<UpdateLogsObject>();
List<UpdateLogsObject> emailUpdateLogs = new List<UpdateLogsObject>();
// ---------------------- Update Phones -----------------------
if (updateContact.ContactPhones != null)
{
@ -975,11 +1056,26 @@ namespace Marco.Pms.Services.Service
foreach (var phoneDto in updateContact.ContactPhones)
{
var phoneEntity = phoneDto.ToContactPhoneFromUpdateContactPhoneDto(tenantId, contact.Id);
if (phoneDto.Id != null && phoneDto.Id != Guid.Empty && phoneIds.Contains(phoneEntity.Id))
var phoneEntity = _mapper.Map<ContactPhone>(phoneDto);
phoneEntity.TenantId = tenantId;
phoneEntity.ContactId = contact.Id;
var existingPhone = phones.FirstOrDefault(p => p.Id == phoneEntity.Id);
if (phoneDto.Id != null && phoneDto.Id != Guid.Empty && existingPhone != null)
{
var phoneObject = _updateLogsHelper.EntityToBsonDocument(existingPhone);
phoneUpdateLogs.Add(new UpdateLogsObject
{
EntityId = existingPhone.Id.ToString(),
UpdatedById = loggedInEmployee.Id.ToString(),
OldObject = phoneObject,
UpdatedAt = DateTime.UtcNow
});
context.ContactsPhones.Update(phoneEntity);
}
else
{
context.ContactsPhones.Add(phoneEntity);
}
}
// Remove phones not updated in payload
@ -987,6 +1083,14 @@ namespace Marco.Pms.Services.Service
{
if (!updatedPhoneIds.Contains(phone.Id))
{
var phoneObject = _updateLogsHelper.EntityToBsonDocument(phone);
phoneUpdateLogs.Add(new UpdateLogsObject
{
EntityId = phone.Id.ToString(),
UpdatedById = loggedInEmployee.Id.ToString(),
OldObject = phoneObject,
UpdatedAt = DateTime.UtcNow
});
context.ContactsPhones.Remove(phone);
}
}
@ -1003,11 +1107,29 @@ namespace Marco.Pms.Services.Service
foreach (var emailDto in updateContact.ContactEmails)
{
var emailEntity = emailDto.ToContactEmailFromUpdateContactEmailDto(tenantId, contact.Id);
if (emailDto.Id != null && emailDto.Id != Guid.Empty && emailIds.Contains(emailEntity.Id))
var emailEntity = _mapper.Map<ContactEmail>(emailDto);
emailEntity.TenantId = tenantId;
emailEntity.ContactId = contact.Id;
var existingEmail = emails.FirstOrDefault(e => e.Id == emailEntity.Id);
if (emailDto.Id != null && emailDto.Id != Guid.Empty && existingEmail != null)
{
var emailObject = _updateLogsHelper.EntityToBsonDocument(existingEmail);
emailUpdateLogs.Add(new UpdateLogsObject
{
EntityId = existingEmail.Id.ToString(),
UpdatedById = loggedInEmployee.Id.ToString(),
OldObject = emailObject,
UpdatedAt = DateTime.UtcNow
});
context.ContactsEmails.Update(emailEntity);
}
else
{
context.ContactsEmails.Add(emailEntity);
}
}
// Remove emails not updated in payload
@ -1015,6 +1137,14 @@ namespace Marco.Pms.Services.Service
{
if (!updatedEmailIds.Contains(email.Id))
{
var emailObject = _updateLogsHelper.EntityToBsonDocument(email);
phoneUpdateLogs.Add(new UpdateLogsObject
{
EntityId = email.Id.ToString(),
UpdatedById = loggedInEmployee.Id.ToString(),
OldObject = emailObject,
UpdatedAt = DateTime.UtcNow
});
context.ContactsEmails.Remove(email);
}
}
@ -1047,6 +1177,7 @@ namespace Marco.Pms.Services.Service
{
if (!incomingBucketIds.Contains(contactBucket.BucketId))
{
context.ContactBucketMappings.Remove(contactBucket);
}
}
@ -1163,34 +1294,56 @@ namespace Marco.Pms.Services.Service
}
// Reload updated contact and related data for response, using a fresh context
using var responseContext = _dbContextFactory.CreateDbContext();
var reloadedContact = await responseContext.Contacts
.Include(c => c.ContactCategory)
.FirstOrDefaultAsync(c => c.Id == id && c.IsActive && c.TenantId == tenantId) ?? new Contact();
var responsePhones = await responseContext.ContactsPhones.AsNoTracking().Where(p => p.ContactId == reloadedContact.Id).ToListAsync();
var responseEmails = await responseContext.ContactsEmails.AsNoTracking().Where(e => e.ContactId == reloadedContact.Id).ToListAsync();
var responseContactTags = await responseContext.ContactTagMappings.AsNoTracking().Where(t => t.ContactId == reloadedContact.Id).ToListAsync();
var responseContactBuckets = await responseContext.ContactBucketMappings.AsNoTracking().Where(cb => cb.ContactId == reloadedContact.Id).ToListAsync();
var responseContactProjects = await responseContext.ContactProjectMappings.AsNoTracking().Where(cp => cp.ContactId == reloadedContact.Id).ToListAsync();
var tagIdsForResponse = responseContactTags.Select(t => t.ContactTagId).Distinct().ToList();
var tagsForResponse = await responseContext.ContactTagMasters.Where(t => tagIdsForResponse.Contains(t.Id)).ToListAsync();
// Map entities to view models
var contactVM = reloadedContact.ToContactVMFromContact();
contactVM.ContactPhones = responsePhones.Select(p => p.ToContactPhoneVMFromContactPhone()).ToList();
contactVM.ContactEmails = responseEmails.Select(e => e.ToContactEmailVMFromContactEmail()).ToList();
contactVM.Tags = responseContactTags.Select(ctm =>
var responseTask = Task.Run(async () =>
{
var tag = tagsForResponse.Find(t => t.Id == ctm.ContactTagId);
return tag != null ? tag.ToContactTagVMFromContactTagMaster() : new ContactTagVM();
}).ToList();
using var responseContext = _dbContextFactory.CreateDbContext();
contactVM.BucketIds = responseContactBuckets.Select(cb => cb.BucketId).ToList();
contactVM.ProjectIds = responseContactProjects.Select(cp => cp.ProjectId).ToList();
var reloadedContact = await responseContext.Contacts
.Include(c => c.ContactCategory)
.FirstOrDefaultAsync(c => c.Id == id && c.IsActive && c.TenantId == tenantId) ?? new Contact();
var responsePhones = await responseContext.ContactsPhones.AsNoTracking().Where(p => p.ContactId == reloadedContact.Id).ToListAsync();
var responseEmails = await responseContext.ContactsEmails.AsNoTracking().Where(e => e.ContactId == reloadedContact.Id).ToListAsync();
var responseContactTags = await responseContext.ContactTagMappings.AsNoTracking().Where(t => t.ContactId == reloadedContact.Id).ToListAsync();
var responseContactBuckets = await responseContext.ContactBucketMappings.AsNoTracking().Where(cb => cb.ContactId == reloadedContact.Id).ToListAsync();
var responseContactProjects = await responseContext.ContactProjectMappings.AsNoTracking().Where(cp => cp.ContactId == reloadedContact.Id).ToListAsync();
var tagIdsForResponse = responseContactTags.Select(t => t.ContactTagId).Distinct().ToList();
var tagsForResponse = await responseContext.ContactTagMasters.Where(t => tagIdsForResponse.Contains(t.Id)).ToListAsync();
// Map entities to view models
var response = reloadedContact.ToContactVMFromContact();
response.ContactPhones = responsePhones.Select(p => p.ToContactPhoneVMFromContactPhone()).ToList();
response.ContactEmails = responseEmails.Select(e => e.ToContactEmailVMFromContactEmail()).ToList();
response.Tags = responseContactTags.Select(ctm =>
{
var tag = tagsForResponse.Find(t => t.Id == ctm.ContactTagId);
return tag != null ? tag.ToContactTagVMFromContactTagMaster() : new ContactTagVM();
}).ToList();
response.BucketIds = responseContactBuckets.Select(cb => cb.BucketId).ToList();
response.ProjectIds = responseContactProjects.Select(cp => cp.ProjectId).ToList();
return response;
});
var contactUpdateLogTask = Task.Run(async () =>
{
await _updateLogsHelper.PushToUpdateLogsAsync(contactUpdateLog, contactCollection);
});
var phoneUpdateLogTask = Task.Run(async () =>
{
await _updateLogsHelper.PushListToUpdateLogsAsync(phoneUpdateLogs, contactPhoneCollection);
});
var emailUpdateLogTask = Task.Run(async () =>
{
await _updateLogsHelper.PushListToUpdateLogsAsync(emailUpdateLogs, contactEmailCollection);
});
await Task.WhenAll(responseTask, contactUpdateLogTask, phoneUpdateLogTask, emailUpdateLogTask);
var contactVM = responseTask.Result;
_logger.LogInfo("Contact {ContactId} successfully updated by employee {EmployeeId}.", contact.Id, loggedInEmployee.Id);

View File

@ -11,6 +11,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
Task<ApiResponse<object>> GetContactsListByBucketIdAsync(Guid bucketId, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetContactProfileAsync(Guid id, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetOrganizationListAsync(Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetDesignationListAsync(Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> CreateContactAsync(CreateContactDto createContact, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> UpdateContactAsync(Guid id, UpdateContactDto updateContact, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> DeleteContactAsync(Guid id, bool active, Guid tenantId, Employee loggedInEmployee);