diff --git a/Marco.Pms.Helpers/Utility/UtilityMongoDBHelper.cs b/Marco.Pms.Helpers/Utility/UtilityMongoDBHelper.cs index 7159850..c6e93f3 100644 --- a/Marco.Pms.Helpers/Utility/UtilityMongoDBHelper.cs +++ b/Marco.Pms.Helpers/Utility/UtilityMongoDBHelper.cs @@ -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(collectionName); - await collection.InsertOneAsync(oldObject); + try + { + var collection = _mongoDatabase.GetCollection(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 oldObjects, string collectionName) + { + try + { + var collection = _mongoDatabase.GetCollection(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> GetFromUpdateLogsByEntityIdAsync(Guid entityId, string collectionName) diff --git a/Marco.Pms.Model/Directory/ContactEmail.cs b/Marco.Pms.Model/Directory/ContactEmail.cs index 1eb1b34..a572be6 100644 --- a/Marco.Pms.Model/Directory/ContactEmail.cs +++ b/Marco.Pms.Model/Directory/ContactEmail.cs @@ -1,6 +1,6 @@ -using System.ComponentModel; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using System.ComponentModel; using System.ComponentModel.DataAnnotations.Schema; -using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; namespace Marco.Pms.Model.Directory { diff --git a/Marco.Pms.Model/Directory/ContactPhone.cs b/Marco.Pms.Model/Directory/ContactPhone.cs index d10439b..f030bc7 100644 --- a/Marco.Pms.Model/Directory/ContactPhone.cs +++ b/Marco.Pms.Model/Directory/ContactPhone.cs @@ -1,6 +1,6 @@ -using System.ComponentModel; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using System.ComponentModel; using System.ComponentModel.DataAnnotations.Schema; -using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; namespace Marco.Pms.Model.Directory { diff --git a/Marco.Pms.Model/Dtos/Directory/CreateContactEmailDto.cs b/Marco.Pms.Model/Dtos/Directory/CreateContactEmailDto.cs index 654890a..0102c39 100644 --- a/Marco.Pms.Model/Dtos/Directory/CreateContactEmailDto.cs +++ b/Marco.Pms.Model/Dtos/Directory/CreateContactEmailDto.cs @@ -4,6 +4,7 @@ { public string? Label { get; set; } public string? EmailAddress { get; set; } + public bool IsPrimary { get; set; } = false; } } diff --git a/Marco.Pms.Model/Dtos/Directory/CreateContactPhoneDto.cs b/Marco.Pms.Model/Dtos/Directory/CreateContactPhoneDto.cs index a72ac3d..84c10c5 100644 --- a/Marco.Pms.Model/Dtos/Directory/CreateContactPhoneDto.cs +++ b/Marco.Pms.Model/Dtos/Directory/CreateContactPhoneDto.cs @@ -4,5 +4,6 @@ { public string? Label { get; set; } public string? PhoneNumber { get; set; } + public bool IsPrimary { get; set; } = false; } } diff --git a/Marco.Pms.Model/Utilities/ContactNoteFilter.cs b/Marco.Pms.Model/Utilities/ContactNoteFilter.cs new file mode 100644 index 0000000..7db8115 --- /dev/null +++ b/Marco.Pms.Model/Utilities/ContactNoteFilter.cs @@ -0,0 +1,8 @@ +namespace Marco.Pms.Model.Utilities +{ + public class ContactNoteFilter + { + public List? CreatedByIds { get; set; } + public List? Organizations { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/DirectoryController.cs b/Marco.Pms.Services/Controllers/DirectoryController.cs index 69cd9d0..b4b6cfa 100644 --- a/Marco.Pms.Services/Controllers/DirectoryController.cs +++ b/Marco.Pms.Services/Controllers/DirectoryController.cs @@ -1,6 +1,6 @@ using Marco.Pms.Model.Dtos.Directory; -using Marco.Pms.Model.Utilities; -using Marco.Pms.Services.Helpers; +using Marco.Pms.Services.Service.ServiceInterfaces; +using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -14,14 +14,33 @@ namespace Marco.Pms.Services.Controllers public class DirectoryController : ControllerBase { - private readonly DirectoryHelper _directoryHelper; + private readonly IDirectoryService _directoryService; + private readonly UserHelper _userHelper; private readonly ILoggingService _logger; + private readonly ISignalRService _signalR; + private readonly Guid tenantId; - - public DirectoryController(DirectoryHelper directoryHelper, ILoggingService logger) + public DirectoryController(IDirectoryService directoryHelper, UserHelper userHelper, ILoggingService logger, ISignalRService signalR) { - _directoryHelper = directoryHelper; + _directoryService = directoryHelper; + _userHelper = userHelper; _logger = logger; + tenantId = userHelper.GetTenantId(); + _signalR = signalR; + } + #region =================================================================== Contact APIs =================================================================== + + #region =================================================================== Contact Get APIs =================================================================== + + [HttpGet("list")] + public async Task GetContactList([FromQuery] string? search, [FromQuery] string? filter, [FromQuery] Guid? projectId, [FromQuery] bool active = true, + [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20) + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.GetListOfContactsAsync(search: search, filter: filter, projectId: projectId, active: active, pageSize: pageSize, pageNumber: pageNumber, tenantId, loggedInEmployee); + + return StatusCode(response.StatusCode, response); + } [HttpGet] @@ -32,266 +51,224 @@ namespace Marco.Pms.Services.Controllers BucketIds = bucketIds, CategoryIds = categoryIds }; - var response = await _directoryHelper.GetListOfContacts(search, active, filterDto, projectId); - return StatusCode(response.StatusCode, response); + var response = await _directoryService.GetListOfContactsOld(search, active, filterDto, projectId); + + return StatusCode(response.StatusCode, response); } [HttpGet("contact-bucket/{bucketId}")] public async Task GetContactsListByBucketId(Guid bucketId) { - var response = await _directoryHelper.GetContactsListByBucketId(bucketId); - return StatusCode(response.StatusCode, response); - } - - [HttpPost] - public async Task CreateContact([FromBody] CreateContactDto createContact) - { - if (!ModelState.IsValid) - { - var errors = ModelState.Values - .SelectMany(v => v.Errors) - .Select(e => e.ErrorMessage) - .ToList(); - _logger.LogWarning("User sent Invalid Date while marking attendance"); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - } - var response = await _directoryHelper.CreateContact(createContact); - return StatusCode(response.StatusCode, response); - } - - [HttpPut("{id}")] - public async Task UpdateContact(Guid id, [FromBody] UpdateContactDto updateContact) - { - var response = await _directoryHelper.UpdateContact(id, updateContact); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.GetContactsListByBucketIdAsync(bucketId, tenantId, loggedInEmployee); return StatusCode(response.StatusCode, response); } [HttpGet("profile/{id}")] public async Task GetContactProfile(Guid id) { - var response = await _directoryHelper.GetContactProfile(id); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.GetContactProfileAsync(id, tenantId, loggedInEmployee); return StatusCode(response.StatusCode, response); } [HttpGet("organization")] public async Task GetOrganizationList() { - var response = await _directoryHelper.GetOrganizationList(); - return StatusCode(response.StatusCode, response); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.GetOrganizationListAsync(tenantId, loggedInEmployee); + return Ok(response); } + [HttpGet("designations")] public async Task GetDesignationList() { - var response = await _directoryHelper.GetDesignationList(); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.GetDesignationListAsync(tenantId, loggedInEmployee); + return Ok(response); + } + + [HttpGet("contact/filter")] + public async Task GetContactFilterObject() + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.GetContactFilterObjectAsync(tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); + } + + #endregion + + [HttpPost] + public async Task CreateContact([FromBody] CreateContactDto createContact) + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.CreateContactAsync(createContact, tenantId, loggedInEmployee); + if (response.Success) + { + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Directory", Response = response.Data }; + await _signalR.SendNotificationAsync(notification); + } + return StatusCode(response.StatusCode, response); + } + + [HttpPut("{id}")] + public async Task UpdateContact(Guid id, [FromBody] UpdateContactDto updateContact) + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.UpdateContactAsync(id, updateContact, tenantId, loggedInEmployee); + if (response.Success) + { + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Directory", Response = response.Data }; + await _signalR.SendNotificationAsync(notification); + } return StatusCode(response.StatusCode, response); } [HttpDelete("{id}")] - public async Task DeleteContact(Guid id, [FromQuery] bool? active) + public async Task DeleteContact(Guid id, [FromQuery] bool active = false) { - var response = await _directoryHelper.DeleteContact(id, active ?? false); - if (response.StatusCode == 200) + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.DeleteContactAsync(id, active, tenantId, loggedInEmployee); + if (response.Success) { - return Ok(response); - } - else if (response.StatusCode == 404) - { - return NotFound(response); - } - else - { - return BadRequest(response); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Directory", Response = id }; + await _signalR.SendNotificationAsync(notification); } + return StatusCode(response.StatusCode, response); } + #endregion - // -------------------------------- Contact Notes -------------------------------- + #region =================================================================== Contact Notes APIs =================================================================== [HttpGet("notes")] - public async Task GetListOFAllNotes([FromQuery] Guid? projectId, [FromQuery] int? pageSize, [FromQuery] int pageNumber) + public async Task GetListOFAllNotes([FromQuery] Guid? projectId, [FromQuery] string? searchString, [FromQuery] string? filter, [FromQuery] int pageSize = 20, [FromQuery] int pageNumber = 1) { - var response = await _directoryHelper.GetListOFAllNotes(projectId, pageSize ?? 25, pageNumber); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.GetListOFAllNotesAsync(projectId, searchString, filter, pageSize, pageNumber, tenantId, loggedInEmployee); return StatusCode(response.StatusCode, response); } [HttpPost("note")] public async Task CreateContactNote([FromBody] CreateContactNoteDto noteDto) { - - var response = await _directoryHelper.CreateContactNote(noteDto); - if (response.StatusCode == 200) + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.CreateContactNoteAsync(noteDto, tenantId, loggedInEmployee); ; + if (response.Success) { - return Ok(response); - } - else if (response.StatusCode == 404) - { - return NotFound(response); - } - else - { - return BadRequest(response); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Directory_Notes", Response = response.Data }; + await _signalR.SendNotificationAsync(notification); } + return StatusCode(response.StatusCode, response); } [HttpGet("notes/{ContactId}")] public async Task GetNoteListByContactId(Guid contactId, [FromQuery] bool active = true) { - var response = await _directoryHelper.GetNoteListByContactId(contactId, active); - if (response.StatusCode == 200) - { - return Ok(response); - } - else if (response.StatusCode == 404) - { - return NotFound(response); - } - else - { - return BadRequest(response); - } + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.GetNoteListByContactIdAsync(contactId, active, tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); + } + + [HttpGet("notes/filter")] + public async Task GetContactNotesFilterObject() + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.GetContactNotesFilterObjectAsync(tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } [HttpPut("note/{id}")] public async Task UpdateContactNote(Guid id, [FromBody] UpdateContactNoteDto noteDto) { - var response = await _directoryHelper.UpdateContactNote(id, noteDto); - if (response.StatusCode == 200) + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.UpdateContactNoteAsync(id, noteDto, tenantId, loggedInEmployee); + if (response.Success) { - return Ok(response); - } - else if (response.StatusCode == 404) - { - return NotFound(response); - } - else - { - return BadRequest(response); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Directory_Notes", Response = response.Data }; + await _signalR.SendNotificationAsync(notification); } + return StatusCode(response.StatusCode, response); } [HttpDelete("note/{id}")] - public async Task DeleteContactNote(Guid id, [FromQuery] bool? active) + public async Task DeleteContactNote(Guid id, [FromQuery] bool active = false) { - var response = await _directoryHelper.DeleteContactNote(id, active ?? false); - return Ok(response); + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.DeleteContactNoteAsync(id, active, tenantId, loggedInEmployee); + if (response.Success) + { + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Directory_Notes", Response = response.Data }; + await _signalR.SendNotificationAsync(notification); + } + return StatusCode(response.StatusCode, response); } - // -------------------------------- Bucket -------------------------------- + #endregion + + #region =================================================================== Bucket APIs =================================================================== [HttpGet("buckets")] public async Task GetBucketList() { - var response = await _directoryHelper.GetBucketList(); - if (response.StatusCode == 200) - { - return Ok(response); - } - else if (response.StatusCode == 401) - { - return Unauthorized(response); - } - else - { - return BadRequest(response); - } + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.GetBucketListAsync(tenantId, loggedInEmployee); + return StatusCode(response.StatusCode, response); } [HttpPost("bucket")] - public async Task CreateBucket(CreateBucketDto bucketDto) + public async Task CreateBucket([FromBody] CreateBucketDto bucketDto) { - if (!ModelState.IsValid) + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.CreateBucketAsync(bucketDto, tenantId, loggedInEmployee); + if (response.Success) { - var errors = ModelState.Values - .SelectMany(v => v.Errors) - .Select(e => e.ErrorMessage) - .ToList(); - _logger.LogWarning("User sent Invalid Date while marking attendance"); - return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); - } - var response = await _directoryHelper.CreateBucket(bucketDto); - if (response.StatusCode == 200) - { - return Ok(response); - } - else if (response.StatusCode == 409) - { - return Conflict(response); - } - else if (response.StatusCode == 401) - { - return Unauthorized(response); - } - else - { - return BadRequest(response); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Directory_Buckets", Response = response.Data }; + await _signalR.SendNotificationAsync(notification); } + return StatusCode(response.StatusCode, response); } [HttpPut("bucket/{id}")] public async Task UpdateBucket(Guid id, [FromBody] UpdateBucketDto bucketDto) { - var response = await _directoryHelper.UpdateBucket(id, bucketDto); - if (response.StatusCode == 200) + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.UpdateBucketAsync(id, bucketDto, tenantId, loggedInEmployee); + if (response.Success) { - return Ok(response); - } - else if (response.StatusCode == 404) - { - return NotFound(response); - } - else if (response.StatusCode == 401) - { - return Unauthorized(response); - } - else - { - return BadRequest(response); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Directory_Buckets", Response = response.Data }; + await _signalR.SendNotificationAsync(notification); } + return StatusCode(response.StatusCode, response); } [HttpPost("assign-bucket/{bucketId}")] public async Task AssignBucket(Guid bucketId, [FromBody] List assignBuckets) { - var response = await _directoryHelper.AssignBucket(bucketId, assignBuckets); - if (response.StatusCode == 200) + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.AssignBucketAsync(bucketId, assignBuckets, tenantId, loggedInEmployee); + if (response.Success) { - return Ok(response); - } - else if (response.StatusCode == 404) - { - return NotFound(response); - } - else if (response.StatusCode == 401) - { - return Unauthorized(response); - } - else - { - return BadRequest(response); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Directory_Buckets", Response = response.Data }; + await _signalR.SendNotificationAsync(notification); } + return StatusCode(response.StatusCode, response); } [HttpDelete("bucket/{id}")] public async Task DeleteBucket(Guid id) { - var response = await _directoryHelper.DeleteBucket(id); - if (response.StatusCode == 200) + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _directoryService.DeleteBucketAsync(id, tenantId, loggedInEmployee); + if (response.Success) { - return Ok(response); - } - else if (response.StatusCode == 404) - { - return NotFound(response); - } - else if (response.StatusCode == 401) - { - return Unauthorized(response); - } - else - { - return BadRequest(response); + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Directory_Buckets", Response = id }; + await _signalR.SendNotificationAsync(notification); } + return StatusCode(response.StatusCode, response); } + + #endregion } } \ No newline at end of file diff --git a/Marco.Pms.Services/Helpers/DirectoryHelper.cs b/Marco.Pms.Services/Helpers/DirectoryHelper.cs deleted file mode 100644 index cb169a1..0000000 --- a/Marco.Pms.Services/Helpers/DirectoryHelper.cs +++ /dev/null @@ -1,1556 +0,0 @@ -using Marco.Pms.DataAccess.Data; -using Marco.Pms.Model.Directory; -using Marco.Pms.Model.Dtos.Directory; -using Marco.Pms.Model.Entitlements; -using Marco.Pms.Model.Mapper; -using Marco.Pms.Model.Projects; -using Marco.Pms.Model.Utilities; -using Marco.Pms.Model.ViewModels.Directory; -using Marco.Pms.Model.ViewModels.Master; -using Marco.Pms.Model.ViewModels.Projects; -using Marco.Pms.Services.Service; -using MarcoBMS.Services.Helpers; -using MarcoBMS.Services.Service; -using Microsoft.EntityFrameworkCore; - -namespace Marco.Pms.Services.Helpers -{ - public class DirectoryHelper - { - private readonly ApplicationDbContext _context; - private readonly ILoggingService _logger; - private readonly UserHelper _userHelper; - private readonly PermissionServices _permissionServices; - - public DirectoryHelper(ApplicationDbContext context, ILoggingService logger, UserHelper userHelper, PermissionServices permissionServices) - { - _context = context; - _logger = logger; - _userHelper = userHelper; - _permissionServices = permissionServices; - } - - public async Task> GetListOfContacts(string? search, bool active, ContactFilterDto? filterDto, Guid? projectId) - { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var assignedRoleIds = await _context.EmployeeRoleMappings.Where(r => r.EmployeeId == LoggedInEmployee.Id).Select(r => r.RoleId).ToListAsync(); - var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); - List? employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).ToListAsync(); - List bucketIds = employeeBuckets.Select(c => c.BucketId).ToList(); - if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) - { - var buckets = await _context.Buckets.Where(b => b.TenantId == tenantId).ToListAsync(); - bucketIds = buckets.Select(b => b.Id).ToList(); - } - else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) || permissionIds.Contains(PermissionsMaster.DirectoryUser)) - { - var buckets = await _context.Buckets.Where(b => b.CreatedByID == LoggedInEmployee.Id).ToListAsync(); - var createdBucketIds = buckets.Select(b => b.Id).ToList(); - bucketIds.AddRange(createdBucketIds); - bucketIds = bucketIds.Distinct().ToList(); - } - else - { - _logger.LogWarning("Employee {EmployeeId} attemped to access a contacts, but do not have permission", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("You don't have permission", "You don't have permission", 401); - } - - List filterbucketIds = bucketIds; - if (filterDto != null && filterDto.BucketIds != null && filterDto.BucketIds.Count > 0) - { - filterbucketIds = filterDto.BucketIds; - } - List? contactBuckets = await _context.ContactBucketMappings.Where(cb => bucketIds.Contains(cb.BucketId)).ToListAsync(); - List contactIds = contactBuckets.Where(b => filterbucketIds.Contains(b.BucketId)).Select(cb => cb.ContactId).ToList(); - List contacts = new List(); - - var contactProjects = await _context.ContactProjectMappings.Where(p => contactIds.Contains(p.ContactId)).ToListAsync(); - - if (projectId != null && projectId != Guid.Empty) - { - contactProjects = contactProjects.Where(p => p.ProjectId == projectId).ToList(); - contactIds = contactProjects.Select(p => p.ContactId).Distinct().ToList(); - } - - if (filterDto != null && filterDto.CategoryIds != null && filterDto.CategoryIds.Count > 0) - { - var categoryIds = filterDto.CategoryIds; - contacts = await _context.Contacts.Include(c => c.ContactCategory).Where(c => contactIds.Contains(c.Id) && categoryIds.Contains(c.ContactCategoryId ?? Guid.Empty) && c.TenantId == tenantId && c.IsActive == active).ToListAsync(); - } - else - { - contacts = await _context.Contacts.Include(c => c.ContactCategory).Where(c => contactIds.Contains(c.Id) && c.TenantId == tenantId && c.IsActive == active).ToListAsync(); - } - - var phoneNo = await _context.ContactsPhones.Where(p => contactIds.Contains(p.ContactId)).ToListAsync(); - var Emails = await _context.ContactsEmails.Where(E => contactIds.Contains(E.ContactId)).ToListAsync(); - var Tags = await _context.ContactTagMappings.Where(t => contactIds.Contains(t.ContactId)).ToListAsync(); - - List TagIds = Tags.Select(t => t.ContactTagId).ToList(); - - var TagList = await _context.ContactTagMasters.Where(t => TagIds.Contains(t.Id)).ToListAsync(); - - if (search != null && search != string.Empty) - { - List filteredContactIds = new List(); - phoneNo = phoneNo.Where(p => Compare(p.PhoneNumber, search)).ToList(); - filteredContactIds = phoneNo.Select(p => p.ContactId).ToList(); - - Emails = Emails.Where(e => Compare(e.EmailAddress, search)).ToList(); - filteredContactIds.AddRange(Emails.Select(e => e.ContactId).ToList()); - filteredContactIds = filteredContactIds.Distinct().ToList(); - - contacts = contacts.Where(c => Compare(c.Name, search) || Compare(c.Organization, search) || filteredContactIds.Contains(c.Id)).ToList(); - } - - List list = new List(); - - foreach (var contact in contacts) - { - - ContactVM contactVM = new ContactVM(); - List contactEmailVms = new List(); - List contactPhoneVms = new List(); - - List conatctTagVms = new List(); - var phones = phoneNo.Where(p => p.ContactId == contact.Id).ToList(); - var emails = Emails.Where(e => e.ContactId == contact.Id).ToList(); - var tagMappingss = Tags.Where(t => t.ContactId == contact.Id).ToList(); - var projectMapping = contactProjects.Where(p => p.ContactId == contact.Id).ToList(); - var bucketMapping = contactBuckets.Where(b => b.ContactId == contact.Id).ToList(); - - - if (emails != null && emails.Count > 0) - { - foreach (var email in emails) - { - ContactEmailVM emailVM = new ContactEmailVM(); - emailVM = email.ToContactEmailVMFromContactEmail(); - contactEmailVms.Add(emailVM); - } - } - - if (phones != null && phones.Count > 0) - { - foreach (var phone in phones) - { - ContactPhoneVM phoneVM = new ContactPhoneVM(); - phoneVM = phone.ToContactPhoneVMFromContactPhone(); - contactPhoneVms.Add(phoneVM); - } - - } - if (tagMappingss != null && tagMappingss.Count > 0) - { - foreach (var tagMapping in tagMappingss) - { - ContactTagVM tagVM = new ContactTagVM(); ; - var tag = TagList.Find(t => t.Id == tagMapping.ContactTagId); - - tagVM = tag != null ? tag.ToContactTagVMFromContactTagMaster() : new ContactTagVM(); - conatctTagVms.Add(tagVM); - - - } - } - contactVM = contact.ToContactVMFromContact(); - - if (projectMapping != null && projectMapping.Count > 0) - { - contactVM.ProjectIds = projectMapping.Select(p => p.ProjectId).ToList(); - } - if (bucketMapping != null && bucketMapping.Count > 0) - { - contactVM.BucketIds = bucketMapping.Select(p => p.BucketId).ToList(); - } - - contactVM.ContactEmails = contactEmailVms; - contactVM.ContactPhones = contactPhoneVms; - contactVM.Tags = conatctTagVms; - - list.Add(contactVM); - } - _logger.LogInfo("{count} contacts are fetched by Employee with ID {LoggedInEmployeeId}", list.Count, LoggedInEmployee.Id); - return ApiResponse.SuccessResponse(list, System.String.Format("{0} contacts fetched successfully", list.Count), 200); - - } - public async Task> GetContactsListByBucketId(Guid id) - { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - if (id != Guid.Empty) - { - Bucket? bucket = await _context.Buckets.FirstOrDefaultAsync(b => b.Id == id && b.TenantId == tenantId); - if (bucket == null) - { - _logger.LogInfo("Employee ID {EmployeeId} attempted access to bucket ID {BucketId}, but not found in database", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("Bucket not found", "Bucket not found", 404); - } - List? employeeBuckets = await _context.EmployeeBucketMappings.Where(em => em.BucketId == id).ToListAsync(); - var assignedRoleIds = await _context.EmployeeRoleMappings.Where(r => r.EmployeeId == LoggedInEmployee.Id).Select(r => r.RoleId).ToListAsync(); - var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); - - EmployeeBucketMapping? employeeBucket = null; - if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) - { - employeeBucket = employeeBuckets.FirstOrDefault(); - } - else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) || permissionIds.Contains(PermissionsMaster.DirectoryUser)) - { - employeeBucket = employeeBuckets.FirstOrDefault(eb => eb.EmployeeId == LoggedInEmployee.Id); - } - else - { - _logger.LogWarning("Employee {EmployeeId} attemped to access a contacts with in bucket {BucketId}, but do not have permission", LoggedInEmployee.Id, id); - return ApiResponse.ErrorResponse("You don't have permission", "You don't have permission", 401); - } - - if (employeeBucket == null) - { - _logger.LogInfo("Employee ID {EmployeeId} does not have access to bucket ID {BucketId}", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("You do not have access to this bucket.", "You do not have access to this bucket.", 401); - } - - List contactBucket = await _context.ContactBucketMappings.Where(cb => cb.BucketId == id).ToListAsync() ?? new List(); - List contactVMs = new List(); - if (contactBucket.Count > 0) - { - var contactIds = contactBucket.Select(cb => cb.ContactId).ToList(); - List contacts = await _context.Contacts.Include(c => c.ContactCategory).Where(c => contactIds.Contains(c.Id) && c.IsActive).ToListAsync(); - List phones = await _context.ContactsPhones.Where(p => contactIds.Contains(p.ContactId)).ToListAsync(); - List emails = await _context.ContactsEmails.Where(e => contactIds.Contains(e.ContactId)).ToListAsync(); - - List? tags = await _context.ContactTagMappings.Where(ct => contactIds.Contains(ct.ContactId)).ToListAsync(); - List? contactProjects = await _context.ContactProjectMappings.Where(cp => contactIds.Contains(cp.ContactId)).ToListAsync(); - List? contactBuckets = await _context.ContactBucketMappings.Where(cp => contactIds.Contains(cp.ContactId)).ToListAsync(); - - List tagIds = new List(); - List tagMasters = new List(); - if (tags.Count > 0) - { - tagIds = tags.Select(ct => ct.ContactTagId).ToList(); - tagMasters = await _context.ContactTagMasters.Where(t => tagIds.Contains(t.Id)).ToListAsync(); - } - - if (contacts.Count > 0) - { - - - foreach (var contact in contacts) - { - List? emailVMs = new List(); - List? phoneVMs = new List(); - List? tagVMs = new List(); - - List contactPhones = phones.Where(p => p.ContactId == contact.Id).ToList(); - List contactEmails = emails.Where(e => e.ContactId == contact.Id).ToList(); - - List? contactTags = tags.Where(t => t.ContactId == contact.Id).ToList(); - List? projectMappings = contactProjects.Where(cp => cp.ContactId == contact.Id).ToList(); - List? bucketMappings = contactBuckets.Where(cb => cb.ContactId == contact.Id).ToList(); - - if (contactPhones.Count > 0) - { - foreach (var phone in contactPhones) - { - ContactPhoneVM phoneVM = phone.ToContactPhoneVMFromContactPhone(); - phoneVMs.Add(phoneVM); - } - } - if (contactEmails.Count > 0) - { - foreach (var email in contactEmails) - { - ContactEmailVM emailVM = email.ToContactEmailVMFromContactEmail(); - emailVMs.Add(emailVM); - } - } - if (contactTags.Count > 0) - { - foreach (var contactTag in contactTags) - { - ContactTagMaster? tagMaster = tagMasters.Find(t => t.Id == contactTag.ContactTagId); - if (tagMaster != null) - { - ContactTagVM tagVM = tagMaster.ToContactTagVMFromContactTagMaster(); - tagVMs.Add(tagVM); - } - } - } - ContactVM contactVM = contact.ToContactVMFromContact(); - contactVM.ContactEmails = emailVMs; - contactVM.ContactPhones = phoneVMs; - contactVM.Tags = tagVMs; - contactVM.ProjectIds = projectMappings.Select(cp => cp.ProjectId).ToList(); - contactVM.BucketIds = bucketMappings.Select(cb => cb.BucketId).ToList(); - contactVMs.Add(contactVM); - } - } - - } - _logger.LogInfo("{count} contact from Bucket {BucketId} fetched by Employee {EmployeeId}", contactVMs.Count, id, LoggedInEmployee.Id); - return ApiResponse.SuccessResponse(contactVMs, $"{contactVMs.Count} contacts fetched successfully", 200); - } - _logger.LogInfo("Employee ID {EmployeeId} sent an empty Bucket id", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("Bucket ID is empty", "Bucket ID is empty", 400); - } - public async Task> CreateContact(CreateContactDto createContact) - { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - if (createContact != null) - { - List phones = new List(); - List emails = new List(); - List contactBucketMappings = new List(); - List contactTagMappings = new List(); - - Contact? contact = createContact.ToContactFromCreateContactDto(tenantId, LoggedInEmployee.Id); - - _context.Contacts.Add(contact); - await _context.SaveChangesAsync(); - _logger.LogInfo("Contact with ID {ContactId} created by Employee with ID {LoggedInEmployeeId}", contact.Id, LoggedInEmployee.Id); - - var tags = await _context.ContactTagMasters.Where(t => t.TenantId == tenantId).ToListAsync(); - var tagNames = tags.Select(t => t.Name.ToLower()).ToList(); - var buckets = await _context.Buckets.Where(b => b.TenantId == tenantId).Select(b => b.Id).ToListAsync(); - var projects = await _context.Projects.Where(p => p.TenantId == tenantId).Select(p => p.Id).ToListAsync(); - - if (createContact.ContactPhones != null) - { - - foreach (var contactPhone in createContact.ContactPhones) - { - ContactPhone phone = contactPhone.ToContactPhoneFromCreateContactPhoneDto(tenantId, contact.Id); - phones.Add(phone); - } - _context.ContactsPhones.AddRange(phones); - _logger.LogInfo("{count} phone number are saved in contact with ID {ContactId} by employee with ID {LoggedEmployeeId}", phones.Count, contact.Id, LoggedInEmployee.Id); - } - if (createContact.ContactEmails != null) - { - - foreach (var contactEmail in createContact.ContactEmails) - { - ContactEmail email = contactEmail.ToContactEmailFromCreateContactEmailDto(tenantId, contact.Id); - emails.Add(email); - } - _context.ContactsEmails.AddRange(emails); - _logger.LogInfo("{count} email addresses are saved in contact with ID {ContactId} by employee with ID {LoggedEmployeeId}", emails.Count, contact.Id, LoggedInEmployee.Id); - } - - if (createContact.BucketIds != null) - { - foreach (var bucket in createContact.BucketIds) - { - if (buckets.Contains(bucket)) - { - ContactBucketMapping bucketMapping = new ContactBucketMapping - { - BucketId = bucket, - ContactId = contact.Id - }; - contactBucketMappings.Add(bucketMapping); - } - } - _context.ContactBucketMappings.AddRange(contactBucketMappings); - _logger.LogInfo("Contact with ID {ContactId} added to {count} number of buckets by employee with ID {LoggedEmployeeId}", contact.Id, contactBucketMappings.Count, LoggedInEmployee.Id); - } - - if (createContact.ProjectIds != null) - { - List projectMappings = new List(); - foreach (var projectId in createContact.ProjectIds) - { - if (projects.Contains(projectId)) - { - ContactProjectMapping projectMapping = new ContactProjectMapping - { - ProjectId = projectId, - ContactId = contact.Id, - TenantId = tenantId - }; - projectMappings.Add(projectMapping); - } - } - _context.ContactProjectMappings.AddRange(projectMappings); - _logger.LogInfo("Contact with ID {ContactId} added to {count} number of project by employee with ID {LoggedEmployeeId}", contact.Id, projectMappings.Count, LoggedInEmployee.Id); - } - - if (createContact.Tags != null) - { - foreach (var tag in createContact.Tags) - { - if (tagNames.Contains(tag.Name.ToLower())) - { - ContactTagMaster existingTag = tags.Find(t => t.Name == tag.Name) ?? new ContactTagMaster(); - _context.ContactTagMappings.Add(new ContactTagMapping - { - ContactId = contact.Id, - ContactTagId = tag.Id ?? existingTag.Id - }); - } - else if (tag.Id == null || tags.Where(t => t.Name == tag.Name) == null) - { - var newtag = new ContactTagMaster - { - Name = tag.Name, - TenantId = tenantId - }; - _context.ContactTagMasters.Add(newtag); - ContactTagMapping tagMapping = new ContactTagMapping - { - ContactTagId = newtag.Id, - ContactId = contact.Id - }; - contactTagMappings.Add(tagMapping); - } - } - - _context.ContactTagMappings.AddRange(contactTagMappings); - _logger.LogInfo("{count} number of tags added to Contact with ID {ContactId} by employee with ID {LoggedEmployeeId}", contactTagMappings.Count, contact.Id, LoggedInEmployee.Id); - } - await _context.SaveChangesAsync(); - - ContactVM contactVM = new ContactVM(); - List phoneVMs = new List(); - - contact = await _context.Contacts.Include(c => c.ContactCategory).FirstOrDefaultAsync(c => c.Id == contact.Id) ?? new Contact(); - var tagIds = contactTagMappings.Select(t => t.ContactTagId).ToList(); - tags = await _context.ContactTagMasters.Where(t => t.TenantId == tenantId && tagIds.Contains(t.Id)).ToListAsync(); - List contactProjects = await _context.ContactProjectMappings.Where(cp => cp.ContactId == contact.Id).ToListAsync(); - List bucketMappings = await _context.ContactBucketMappings.Where(cb => cb.ContactId == contact.Id).ToListAsync(); - foreach (var phone in phones) - { - ContactPhoneVM phoneVM = phone.ToContactPhoneVMFromContactPhone(); - phoneVMs.Add(phoneVM); - } - List emailVMs = new List(); - foreach (var email in emails) - { - ContactEmailVM emailVM = email.ToContactEmailVMFromContactEmail(); - emailVMs.Add(emailVM); - } - List tagVMs = new List(); - foreach (var contactTagMapping in contactTagMappings) - { - ContactTagVM tagVM = new ContactTagVM(); - var tag = tags.Find(t => t.Id == contactTagMapping.ContactTagId); - tagVM = tag != null ? tag.ToContactTagVMFromContactTagMaster() : new ContactTagVM(); - tagVMs.Add(tagVM); - } - - - contactVM = contact.ToContactVMFromContact(); - contactVM.ContactPhones = phoneVMs; - contactVM.ContactEmails = emailVMs; - contactVM.Tags = tagVMs; - contactVM.ProjectIds = contactProjects.Select(cp => cp.ProjectId).ToList(); - contactVM.BucketIds = bucketMappings.Select(cb => cb.BucketId).ToList(); - - return ApiResponse.SuccessResponse(contactVM, "Contact Created Successfully", 200); - } - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} sended empty payload", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("User Send empty Payload", "User Send empty Payload", 400); - } - public async Task> UpdateContact(Guid id, UpdateContactDto updateContact) - { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - if (updateContact != null) - { - if (updateContact.Id != id) - { - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} sended different ID in payload and path parameter", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("Invalid data", "Invalid data", 400); - } - Contact? contact = await _context.Contacts.AsNoTracking().FirstOrDefaultAsync(c => c.Id == id && c.IsActive && c.TenantId == tenantId); - if (contact == null) - { - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} tries to update contact with ID {ContactId} is not found in database", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("Contact not found", "Contact not found", 404); - } - - var assignedRoleIds = await _context.EmployeeRoleMappings.Where(r => r.EmployeeId == LoggedInEmployee.Id).Select(r => r.RoleId).ToListAsync(); - var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); - List? employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).ToListAsync(); - List bucketIds = employeeBuckets.Select(c => c.BucketId).ToList(); - if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) - { - var buckets = await _context.Buckets.Where(b => b.TenantId == tenantId).ToListAsync(); - bucketIds = buckets.Select(b => b.Id).ToList(); - } - else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) || permissionIds.Contains(PermissionsMaster.DirectoryUser)) - { - var buckets = await _context.Buckets.Where(b => b.CreatedByID == LoggedInEmployee.Id).ToListAsync(); - var createdBucketIds = buckets.Select(b => b.Id).ToList(); - bucketIds.AddRange(createdBucketIds); - bucketIds = bucketIds.Distinct().ToList(); - } - else - { - _logger.LogWarning("Employee {EmployeeId} attemped to update a contact, but do not have permission", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("You don't have permission", "You don't have permission", 401); - } - - List contactBuckets = await _context.ContactBucketMappings.AsNoTracking().Where(m => m.ContactId == contact.Id && bucketIds.Contains(m.BucketId)).ToListAsync(); - bucketIds = contactBuckets.Select(b => b.BucketId).Distinct().ToList(); - - - - var newContact = updateContact.ToContactFromUpdateContactDto(tenantId, contact); - newContact.UpdatedById = LoggedInEmployee.Id; - newContact.UpdatedAt = DateTime.UtcNow; - _context.Contacts.Update(newContact); - await _context.SaveChangesAsync(); - - List phones = await _context.ContactsPhones.AsNoTracking().Where(p => p.ContactId == contact.Id).ToListAsync(); - var phoneIds = phones.Select(p => p.Id).ToList(); - List emails = await _context.ContactsEmails.AsNoTracking().Where(p => p.ContactId == contact.Id).ToListAsync(); - var emailIds = emails.Select(p => p.Id).ToList(); - - - - List contactTags = await _context.ContactTagMappings.AsNoTracking().Where(m => m.ContactId == contact.Id).ToListAsync(); - var tagIds = contactTags.Select(t => t.ContactTagId).Distinct().ToList(); - - - List contactProjects = await _context.ContactProjectMappings.AsNoTracking().Where(m => m.ContactId == contact.Id).ToListAsync(); - var projectIds = contactProjects.Select(t => t.ProjectId).Distinct().ToList(); - - List tags = await _context.ContactTagMasters.Where(t => tagIds.Contains(t.Id)).ToListAsync(); - List allTags = await _context.ContactTagMasters.Where(t => t.TenantId == tenantId).ToListAsync(); - var tagNames = allTags.Select(t => t.Name.ToLower()).ToList(); - - if (updateContact.ContactPhones != null) - { - var updatedPhoneIds = updateContact.ContactPhones.Select(p => p.Id).Distinct().ToList(); - foreach (var phoneDto in updateContact.ContactPhones) - { - var phone = phoneDto.ToContactPhoneFromUpdateContactPhoneDto(tenantId, contact.Id); - if (phoneDto.Id != null && phoneDto.Id != Guid.Empty && phoneIds.Contains(phone.Id)) - { - _context.ContactsPhones.Update(phone); - } - else - { - _context.ContactsPhones.Add(phone); - } - } - foreach (var phone in phones) - { - - if (!updatedPhoneIds.Contains(phone.Id)) - { - _context.ContactsPhones.Remove(phone); - } - } - } - else if (phones != null) - { - _context.ContactsPhones.RemoveRange(phones); - } - - if (updateContact.ContactEmails != null) - { - var updateEmailIds = updateContact.ContactEmails.Select(p => p.Id).Distinct().ToList(); - - foreach (var emailDto in updateContact.ContactEmails) - { - var email = emailDto.ToContactEmailFromUpdateContactEmailDto(tenantId, contact.Id); - if (emailDto.Id != null && emailDto.Id != Guid.Empty && emailIds.Contains(email.Id)) - { - _context.ContactsEmails.Update(email); - } - else - { - _context.ContactsEmails.Add(email); - } - } - foreach (var email in emails) - { - - if (!updateEmailIds.Contains(email.Id)) - { - _context.ContactsEmails.Remove(email); - } - } - } - else if (emails != null) - { - _context.ContactsEmails.RemoveRange(emails); - } - - if (updateContact.BucketIds != null) - { - foreach (var bucketId in updateContact.BucketIds) - { - if (!bucketIds.Contains(bucketId)) - { - _context.ContactBucketMappings.Add(new ContactBucketMapping - { - BucketId = bucketId, - ContactId = contact.Id - }); - } - } - foreach (var bucketMapping in contactBuckets) - { - if (!updateContact.BucketIds.Contains(bucketMapping.BucketId)) - { - _context.ContactBucketMappings.Remove(bucketMapping); - } - } - } - else if (contactBuckets != null) - { - _context.ContactBucketMappings.RemoveRange(contactBuckets); - } - - if (updateContact.ProjectIds != null) - { - foreach (var ProjectId in updateContact.ProjectIds) - { - if (!projectIds.Contains(ProjectId)) - { - _context.ContactProjectMappings.Add(new ContactProjectMapping - { - ProjectId = ProjectId, - ContactId = contact.Id, - TenantId = tenantId - }); - } - } - - foreach (var projectMapping in contactProjects) - { - if (!updateContact.ProjectIds.Contains(projectMapping.ProjectId)) - { - _context.ContactProjectMappings.Remove(projectMapping); - } - } - } - else if (contactProjects != null) - { - _context.ContactProjectMappings.RemoveRange(contactProjects); - } - - if (updateContact.Tags != null) - { - var updatedTagIds = updateContact.Tags.Select(t => t.Id).Distinct().ToList(); - foreach (var tag in updateContact.Tags) - { - var namecheck = tagNames.Contains(tag.Name.ToLower()); - var idCheck = (!tagIds.Contains(tag.Id ?? Guid.Empty)); - var test = namecheck && idCheck; - if (test) - { - ContactTagMaster existingTag = tags.Find(t => t.Name == tag.Name) ?? new ContactTagMaster(); - _context.ContactTagMappings.Add(new ContactTagMapping - { - ContactId = contact.Id, - ContactTagId = tag.Id ?? existingTag.Id - }); - } - else if (tag.Id == null || tag.Id == Guid.Empty) - { - ContactTagMaster contactTag = new ContactTagMaster - { - Name = tag.Name, - Description = "", - TenantId = tenantId - }; - _context.ContactTagMasters.Add(contactTag); - - _context.ContactTagMappings.Add(new ContactTagMapping - { - ContactId = contact.Id, - ContactTagId = contactTag.Id - }); - } - } - foreach (var contactTag in contactTags) - { - if (!updatedTagIds.Contains(contactTag.ContactTagId)) - { - _context.ContactTagMappings.Remove(contactTag); - } - } - } - else if (contactTags != null) - { - _context.ContactTagMappings.RemoveRange(contactTags); - } - _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog - { - RefereanceId = contact.Id, - UpdatedById = LoggedInEmployee.Id, - UpdateAt = DateTime.UtcNow - }); - - await _context.SaveChangesAsync(); - - contact = await _context.Contacts.Include(c => c.ContactCategory).FirstOrDefaultAsync(c => c.Id == id && c.IsActive && c.TenantId == tenantId) ?? new Contact(); - phones = await _context.ContactsPhones.AsNoTracking().Where(p => p.ContactId == contact.Id).ToListAsync(); - emails = await _context.ContactsEmails.AsNoTracking().Where(p => p.ContactId == contact.Id).ToListAsync(); - contactTags = await _context.ContactTagMappings.AsNoTracking().Where(m => m.ContactId == contact.Id).ToListAsync(); - contactBuckets = await _context.ContactBucketMappings.AsNoTracking().Where(cb => cb.ContactId == contact.Id).ToListAsync(); - contactProjects = await _context.ContactProjectMappings.AsNoTracking().Where(cp => cp.ContactId == contact.Id).ToListAsync(); - tagIds = contactTags.Select(t => t.ContactTagId).Distinct().ToList(); - tags = await _context.ContactTagMasters.Where(t => tagIds.Contains(t.Id)).ToListAsync(); - - ContactVM contactVM = new ContactVM(); - List phoneVMs = new List(); - foreach (var phone in phones) - { - ContactPhoneVM phoneVM = phone.ToContactPhoneVMFromContactPhone(); - phoneVMs.Add(phoneVM); - } - List emailVMs = new List(); - foreach (var email in emails) - { - ContactEmailVM emailVM = email.ToContactEmailVMFromContactEmail(); - emailVMs.Add(emailVM); - } - List tagVMs = new List(); - foreach (var contactTagMapping in contactTags) - { - ContactTagVM tagVM = new ContactTagVM(); - var tag = tags.Find(t => t.Id == contactTagMapping.ContactTagId); - tagVM = tag != null ? tag.ToContactTagVMFromContactTagMaster() : new ContactTagVM(); - tagVMs.Add(tagVM); - } - - - contactVM = contact.ToContactVMFromContact(); - contactVM.ContactPhones = phoneVMs; - contactVM.ContactEmails = emailVMs; - contactVM.Tags = tagVMs; - contactVM.BucketIds = contactBuckets.Select(cb => cb.BucketId).ToList(); - contactVM.ProjectIds = contactProjects.Select(cp => cp.ProjectId).ToList(); - - _logger.LogInfo("Conatct {ContactId} has been updated by employee {EmployeeId}", contact.Id, LoggedInEmployee.Id); - return ApiResponse.SuccessResponse(contactVM, "Contact Updated Successfully", 200); - } - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} sended empty payload", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("User Send empty Payload", "User Send empty Payload", 400); - } - public async Task> GetContactProfile(Guid id) - { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var hasAdminPermission = await _permissionServices.HasPermission(PermissionsMaster.DirectoryAdmin, LoggedInEmployee.Id); - if (id != Guid.Empty) - { - Contact? contact = await _context.Contacts.Include(c => c.ContactCategory).Include(c => c.CreatedBy).FirstOrDefaultAsync(c => c.Id == id && c.IsActive); - if (contact == null) - { - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} tries to update contact with ID {ContactId} is not found in database", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("Contact not found", "Contact not found", 404); - } - ContactProfileVM contactVM = contact.ToContactProfileVMFromContact(); - DirectoryUpdateLog? updateLog = await _context.DirectoryUpdateLogs.Include(l => l.Employee).Where(l => l.RefereanceId == contact.Id).OrderByDescending(l => l.UpdateAt).FirstOrDefaultAsync(); - if (updateLog != null) - { - contactVM.UpdatedAt = updateLog.UpdateAt; - contactVM.UpdatedBy = updateLog.Employee != null ? updateLog.Employee.ToBasicEmployeeVMFromEmployee() : null; - } - - List? phones = await _context.ContactsPhones.Where(p => p.ContactId == contact.Id).ToListAsync(); - if (phones.Any()) - { - List? phoneVMs = new List(); - foreach (var phone in phones) - { - ContactPhoneVM phoneVM = phone.ToContactPhoneVMFromContactPhone(); - phoneVMs.Add(phoneVM); - } - contactVM.ContactPhones = phoneVMs; - } - - List? emails = await _context.ContactsEmails.Where(e => e.ContactId == contact.Id).ToListAsync(); - if (emails.Any()) - { - List? emailVMs = new List(); - foreach (var email in emails) - { - ContactEmailVM emailVM = email.ToContactEmailVMFromContactEmail(); - emailVMs.Add(emailVM); - } - contactVM.ContactEmails = emailVMs; - } - - List? contactProjects = await _context.ContactProjectMappings.Where(cp => cp.ContactId == contact.Id).ToListAsync(); - if (contactProjects.Any()) - { - List projectIds = contactProjects.Select(cp => cp.ProjectId).ToList(); - List? projects = await _context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync(); - List? projectVMs = new List(); - foreach (var project in projects) - { - BasicProjectVM projectVM = new BasicProjectVM - { - Id = project.Id, - Name = project.Name - }; - projectVMs.Add(projectVM); - } - contactVM.Projects = projectVMs; - } - List? contactBuckets = await _context.ContactBucketMappings.Where(cb => cb.ContactId == contact.Id).ToListAsync(); - List? employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).ToListAsync(); - if (contactBuckets.Any() && (employeeBuckets.Any() || hasAdminPermission)) - { - List contactBucketIds = contactBuckets.Select(cb => cb.BucketId).ToList(); - List employeeBucketIds = employeeBuckets.Select(eb => eb.BucketId).ToList(); - List? buckets = null; - if (hasAdminPermission) - { - buckets = await _context.Buckets.Where(b => contactBucketIds.Contains(b.Id)).ToListAsync(); - } - else - { - buckets = await _context.Buckets.Where(b => contactBucketIds.Contains(b.Id) && employeeBucketIds.Contains(b.Id)).ToListAsync(); - } - List? bucketVMs = new List(); - foreach (var bucket in buckets) - { - BucketVM bucketVM = bucket.ToBucketVMFromBucket(); - bucketVMs.Add(bucketVM); - } - contactVM.Buckets = bucketVMs; - } - List? contactTags = await _context.ContactTagMappings.Where(ct => ct.ContactId == contact.Id).ToListAsync(); - if (contactTags.Any()) - { - List tagIds = contactTags.Select(ct => ct.ContactTagId).ToList(); - List tagMasters = await _context.ContactTagMasters.Where(t => tagIds.Contains(t.Id)).ToListAsync(); - List tagVMs = new List(); - foreach (var tagMaster in tagMasters) - { - ContactTagVM tagVM = tagMaster.ToContactTagVMFromContactTagMaster(); - tagVMs.Add(tagVM); - } - contactVM.Tags = tagVMs; - } - List? notes = await _context.ContactNotes.Where(n => n.ContactId == contact.Id && n.IsActive).ToListAsync(); - if (notes.Any()) - { - List? noteIds = notes.Select(n => n.Id).ToList(); - List? noteUpdateLogs = await _context.DirectoryUpdateLogs.Include(l => l.Employee).Where(l => noteIds.Contains(l.RefereanceId)).OrderByDescending(l => l.UpdateAt).ToListAsync(); - List? noteVMs = new List(); - foreach (var note in notes) - { - DirectoryUpdateLog? noteUpdateLog = noteUpdateLogs.Where(n => n.RefereanceId == note.Id).OrderByDescending(l => l.UpdateAt).FirstOrDefault(); - ContactNoteVM noteVM = note.ToContactNoteVMFromContactNote(); - if (noteUpdateLog != null) - { - noteVM.UpdatedAt = noteUpdateLog.UpdateAt; - noteVM.UpdatedBy = noteUpdateLog.Employee != null ? noteUpdateLog.Employee.ToBasicEmployeeVMFromEmployee() : null; - } - noteVMs.Add(noteVM); - } - contactVM.Notes = noteVMs; - } - _logger.LogInfo("Employee ID {EmployeeId} fetched profile of contact {COntactId}", LoggedInEmployee.Id, contact.Id); - return ApiResponse.SuccessResponse(contactVM, "Contact profile fetched successfully"); - - } - _logger.LogInfo("Employee ID {EmployeeId} sent an empty contact id", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("Contact ID is empty", "Contact ID is empty", 400); - } - public async Task> GetOrganizationList() - { - // Step 1: Retrieve tenant and employee context - Guid tenantId = _userHelper.GetTenantId(); - var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - _logger.LogInfo("GetOrganizationList called by EmployeeId: {EmployeeId} for TenantId: {TenantId}", - loggedInEmployee.Id, tenantId); - - // Step 2: Fetch distinct, non-empty organization names - var organizationList = await _context.Contacts - .Where(c => c.TenantId == tenantId && !string.IsNullOrWhiteSpace(c.Organization)) - .Select(c => c.Organization.Trim()) - .Distinct() - .ToListAsync(); - - _logger.LogInfo("EmployeeId: {EmployeeId} fetched {Count} organization names from TenantId: {TenantId}", - loggedInEmployee.Id, organizationList.Count, tenantId); - - // Step 3: Return success response - return ApiResponse.SuccessResponse( - organizationList, - $"{organizationList.Count} records of organization names fetched from contacts", - 200 - ); - } - public async Task> GetDesignationList() - { - // Step 1: Get tenant and logged-in employee details - Guid tenantId = _userHelper.GetTenantId(); - var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - _logger.LogInfo("GetDesignationList called by EmployeeId: {EmployeeId} in TenantId: {TenantId}", - loggedInEmployee.Id, tenantId); - - // Step 2: Fetch distinct, non-null designations from contacts - var designationList = await _context.Contacts - .Where(c => c.TenantId == tenantId && !string.IsNullOrWhiteSpace(c.Designation)) - .Select(c => c.Designation.Trim()) - .Distinct() - .ToListAsync(); - - _logger.LogInfo("EmployeeId: {EmployeeId} fetched {Count} designations from TenantId: {TenantId}", - loggedInEmployee.Id, designationList.Count, tenantId); - - // Step 3: Return result - return ApiResponse.SuccessResponse( - designationList, - $"{designationList.Count} records of designation fetched from contacts", - 200 - ); - } - public async Task> DeleteContact(Guid id, bool active) - { - // Step 1: Get tenant and logged-in employee info - Guid tenantId = _userHelper.GetTenantId(); - var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - _logger.LogInfo("DeleteContact called by EmployeeId: {EmployeeId} for ContactId: {ContactId} with Active: {IsActive}", - loggedInEmployee.Id, id, active); - - // Step 2: Validate contact ID - if (id == Guid.Empty) - { - _logger.LogWarning("Empty contact ID received from EmployeeId: {EmployeeId}", loggedInEmployee.Id); - return ApiResponse.ErrorResponse("Contact ID is empty", "Contact ID is empty", 400); - } - - // Step 3: Check if contact exists under current tenant - var contact = await _context.Contacts - .FirstOrDefaultAsync(c => c.Id == id && c.TenantId == tenantId); - - if (contact == null) - { - _logger.LogWarning("EmployeeId {EmployeeId} attempted to delete non-existent contact Id: {ContactId}", loggedInEmployee.Id, id); - return ApiResponse.ErrorResponse("Contact not found", "Contact not found", 404); - } - - // Step 4: Soft delete or restore contact - contact.IsActive = active; - - // Step 5: Log the update in DirectoryUpdateLog - _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog - { - RefereanceId = contact.Id, - UpdatedById = loggedInEmployee.Id, - UpdateAt = DateTime.UtcNow - }); - - await _context.SaveChangesAsync(); - - string status = active ? "restored" : "deleted"; - _logger.LogInfo("Contact {ContactId} successfully {Status} by EmployeeId: {EmployeeId}", - contact.Id, status, loggedInEmployee.Id); - - // Step 6: Return success response - return ApiResponse.SuccessResponse(new { }, $"Contact {status} successfully", 200); - } - - // -------------------------------- Contact Notes -------------------------------- - - /// - /// Retrieves a paginated list of contact notes based on user permissions. - /// - /// The number of items per page. - /// The current page number. - /// An ApiResponse containing the paginated notes or an error message. - public async Task> GetListOFAllNotes(Guid? projectId, int pageSize, int pageNumber) - { - _logger.LogInfo("Attempting to fetch list of all notes. PageSize: {PageSize}, PageNumber: {PageNumber}", pageSize, pageNumber); - - Guid tenantId = _userHelper.GetTenantId(); - var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - List? projectContactIds = null; - - if (loggedInEmployee == null) - { - _logger.LogWarning("GetListOFAllNotes: LoggedInEmployee is null. Cannot proceed."); - return ApiResponse.ErrorResponse("Unauthorized", "Employee not found.", 401); - } - - // --- Permission Checks --- - var hasAdminPermission = await _permissionServices.HasPermission(PermissionsMaster.DirectoryAdmin, loggedInEmployee.Id); - var hasManagerPermission = await _permissionServices.HasPermission(PermissionsMaster.DirectoryAdmin, loggedInEmployee.Id); - var hasUserPermission = await _permissionServices.HasPermission(PermissionsMaster.DirectoryUser, loggedInEmployee.Id); - - IQueryable notesQuery = _context.ContactNotes - .Include(cn => cn.UpdatedBy) - .Include(cn => cn.Createdby) // Assuming 'CreatedBy' (PascalCase) - .Include(cn => cn.Contact) - .Where(cn => cn.TenantId == tenantId) - .AsQueryable(); // Start building the query - - if (!hasAdminPermission && !(hasManagerPermission || hasUserPermission)) - { - _logger.LogWarning("GetListOFAllNotes: User {EmployeeId} does not have required permissions to access notes for TenantId: {TenantId}", loggedInEmployee.Id, tenantId); - return ApiResponse.ErrorResponse("Access Denied", "You don't have access to view notes.", 403); - } - if (projectId != null) - { - projectContactIds = await _context.ContactProjectMappings - .Where(pc => pc.ProjectId == projectId) - .Select(pc => pc.ContactId) - .ToListAsync(); - } - if (!hasAdminPermission) // If not an admin, apply additional filtering - { - _logger.LogInfo("GetListOFAllNotes: User {EmployeeId} is not an admin. Applying manager/user specific filters.", loggedInEmployee.Id); - var assignedBucketIds = await _context.EmployeeBucketMappings - .Where(eb => eb.EmployeeId == loggedInEmployee.Id) - .Select(eb => eb.BucketId) - .ToListAsync(); - - if (!assignedBucketIds.Any()) - { - _logger.LogInfo("GetListOFAllNotes: User {EmployeeId} has no assigned buckets. Returning empty list.", loggedInEmployee.Id); - return ApiResponse.SuccessResponse(new { CurrentPage = pageNumber, TotalPages = 0, Data = new List() }, "No notes found based on assigned buckets.", 200); - } - - List? contactIds = null; - - if (projectContactIds == null) - { - contactIds = await _context.ContactBucketMappings - .Where(cb => assignedBucketIds.Contains(cb.BucketId)) - .Select(cb => cb.ContactId) - .ToListAsync(); - } - else - { - contactIds = await _context.ContactBucketMappings - .Where(cb => assignedBucketIds.Contains(cb.BucketId) && projectContactIds.Contains(cb.ContactId)) - .Select(cb => cb.ContactId) - .ToListAsync(); - } - - if (!contactIds.Any()) - { - _logger.LogInfo("GetListOFAllNotes: No contacts found for assigned buckets for user {EmployeeId}. Returning empty list.", loggedInEmployee.Id); - return ApiResponse.SuccessResponse(new { CurrentPage = pageNumber, TotalPages = 0, Data = new List() }, "No notes found for associated contacts.", 200); - } - - notesQuery = notesQuery.Where(cn => contactIds.Contains(cn.ContactId)); - } - else - { - if (projectContactIds != null) - { - notesQuery = notesQuery.Where(cn => projectContactIds.Contains(cn.ContactId)); - } - } - - // --- Pagination Logic --- - // Ensure pageSize and pageNumber are valid - pageSize = pageSize < 1 ? 25 : pageSize; // Default to 25 if less than 1 - pageNumber = pageNumber < 1 ? 1 : pageNumber; // Default to 1 if less than 1 - - // Get total count BEFORE applying Skip/Take for accurate pagination metadata - int totalRecords = await notesQuery.CountAsync(); - int totalPages = (int)Math.Ceiling((double)totalRecords / pageSize); - - int skip = (pageNumber - 1) * pageSize; - - // --- Apply Ordering and Pagination in the database --- - List notes = await notesQuery - .OrderByDescending(cn => (cn.UpdatedAt != null ? cn.UpdatedAt : cn.CreatedAt)) // Order by updated date or created date - .Skip(skip) - .Take(pageSize) - .ToListAsync(); - - _logger.LogInfo("GetListOFAllNotes: Fetched {Count} notes for page {PageNumber} of {TotalPages} total pages. Total records: {TotalRecords}.", - notes.Count, pageNumber, totalPages, totalRecords); - - // --- Map to ViewModel (in-memory) --- - // This mapping is done in memory because ToBasicEmployeeVMFromEmployee() is likely a C# method - // that cannot be translated to SQL by Entity Framework. - - List noteVMS = notes - .Select(cn => cn.ToContactNoteVMFromContactNote()) - .ToList(); - - var response = new - { - CurrentPage = pageNumber, - PageSize = pageSize, // Include pageSize in response for client clarity - TotalPages = totalPages, - TotalRecords = totalRecords, // Add total records for client - Data = noteVMS - }; - - _logger.LogInfo("GetListOFAllNotes: Successfully retrieved notes and mapped to ViewModel for TenantId: {TenantId}.", tenantId); - return ApiResponse.SuccessResponse(response, $"{noteVMS.Count} notes fetched successfully.", 200); - } - public async Task> GetNoteListByContactId(Guid id, bool active) - { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - Contact? contact = await _context.Contacts.FirstOrDefaultAsync(c => c.Id == id && c.IsActive && c.TenantId == tenantId); - if (contact != null) - { - List notes = new List(); - if (active) - { - notes = await _context.ContactNotes - .Include(n => n.Createdby) - .Include(n => n.UpdatedBy) - .Where(n => n.ContactId == contact.Id && n.IsActive && n.TenantId == tenantId) - .ToListAsync(); - } - else - { - notes = await _context.ContactNotes - .Include(n => n.Createdby) - .Include(n => n.UpdatedBy) - .Where(n => n.ContactId == contact.Id && n.TenantId == tenantId) - .ToListAsync(); - } - var noteIds = notes.Select(n => n.Id).ToList(); - List? updateLogs = await _context.DirectoryUpdateLogs.Include(l => l.Employee).Where(l => noteIds.Contains(l.RefereanceId)).ToListAsync(); - //List? noteVMs = new List(); - List? noteVMs = notes.Select(n => n.ToContactNoteVMFromContactNote()).ToList(); - - _logger.LogInfo("{count} contact-notes record from contact {ContactId} fetched by Employee {EmployeeId}", noteVMs.Count, id, LoggedInEmployee.Id); - return ApiResponse.SuccessResponse(noteVMs, $"{noteVMs.Count} contact-notes record fetched successfully", 200); - } - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} attempted to fetch a list notes from contact with ID {ContactId}, but the contact was not found in the database.", LoggedInEmployee.Id, id); - return ApiResponse.ErrorResponse("Contact not found", "Contact not found", 404); - } - public async Task> CreateContactNote(CreateContactNoteDto noteDto) - { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - if (noteDto != null) - { - Contact? contact = await _context.Contacts.FirstOrDefaultAsync(c => c.Id == noteDto.ContactId && c.IsActive && c.TenantId == tenantId); - if (contact != null) - { - ContactNote note = noteDto.ToContactNoteFromCreateContactNoteDto(tenantId, LoggedInEmployee.Id); - _context.ContactNotes.Add(note); - await _context.SaveChangesAsync(); - ContactNoteVM noteVM = note.ToContactNoteVMFromContactNote(); - _logger.LogInfo("Employee {EmployeeId} Added note at contact {ContactId}", LoggedInEmployee.Id, contact.Id); - return ApiResponse.SuccessResponse(noteVM, "Note added successfully", 200); - } - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} attempted to add a note to contact with ID {ContactId}, but the contact was not found in the database.", LoggedInEmployee.Id, noteDto.ContactId); - return ApiResponse.ErrorResponse("Contact not found", "Contact not found", 404); - } - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} sended empty payload", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("User Send empty Payload", "User Send empty Payload", 400); - } - public async Task> UpdateContactNote(Guid id, UpdateContactNoteDto noteDto) - { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - if (noteDto != null && id == noteDto.Id) - { - Contact? contact = await _context.Contacts.FirstOrDefaultAsync(c => c.Id == noteDto.ContactId && c.TenantId == tenantId); - if (contact != null) - { - ContactNote? contactNote = await _context.ContactNotes.Include(cn => cn.Createdby).Include(cn => cn.Contact).FirstOrDefaultAsync(n => n.Id == noteDto.Id && n.ContactId == contact.Id && n.IsActive); - if (contactNote != null) - { - contactNote.Note = noteDto.Note; - contactNote.UpdatedById = LoggedInEmployee.Id; - contactNote.UpdatedAt = DateTime.UtcNow; - - _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog - { - RefereanceId = id, - UpdatedById = LoggedInEmployee.Id, - UpdateAt = DateTime.UtcNow - }); - - await _context.SaveChangesAsync(); - ContactNoteVM noteVM = contactNote.ToContactNoteVMFromContactNote(); - noteVM.UpdatedAt = DateTime.UtcNow; - noteVM.UpdatedBy = LoggedInEmployee.ToBasicEmployeeVMFromEmployee(); - - _logger.LogInfo("Employee {EmployeeId} updated note {NoteId} at contact {ContactId}", LoggedInEmployee.Id, noteVM.Id, contact.Id); - return ApiResponse.SuccessResponse(noteVM, "Note updated successfully", 200); - } - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} attempted to update a note {NoteId} to contact with ID {ContactId}, but the Note was not found in the database.", LoggedInEmployee.Id, noteDto.Id, noteDto.ContactId); - return ApiResponse.ErrorResponse("Note not found", "Note not found", 404); - } - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} attempted to update a note {NoteId} to contact with ID {ContactId}, but the contact was not found in the database.", LoggedInEmployee.Id, noteDto.Id, noteDto.ContactId); - return ApiResponse.ErrorResponse("Contact not found", "Contact not found", 404); - } - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} sended empty payload", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("User Send empty Payload", "User Send empty Payload", 400); - } - public async Task> DeleteContactNote(Guid id, bool active) - { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - ContactNote? note = await _context.ContactNotes.FirstOrDefaultAsync(n => n.Id == id && n.TenantId == tenantId); - if (note != null) - { - note.IsActive = active; - note.UpdatedById = LoggedInEmployee.Id; - note.UpdatedAt = DateTime.UtcNow; - - _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog - { - RefereanceId = id, - UpdatedById = LoggedInEmployee.Id, - UpdateAt = DateTime.UtcNow - }); - await _context.SaveChangesAsync(); - _logger.LogInfo("Employee {EmployeeId} deleted note {NoteId}", LoggedInEmployee.Id, id); - } - - _logger.LogWarning("Employee {EmployeeId} tries to delete contact note {NoteId} but not found in database", LoggedInEmployee.Id, id); - return ApiResponse.SuccessResponse(new { }, "Note deleted successfully", 200); - } - - // -------------------------------- Bucket -------------------------------- - - public async Task> GetBucketList() - { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - var assignedRoleIds = await _context.EmployeeRoleMappings.Where(r => r.EmployeeId == LoggedInEmployee.Id).Select(r => r.RoleId).ToListAsync(); - var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); - - List employeeBuckets = await _context.EmployeeBucketMappings.Where(b => b.EmployeeId == LoggedInEmployee.Id).ToListAsync(); - var bucketIds = employeeBuckets.Select(b => b.BucketId).ToList(); - - List bucketList = new List(); - if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) - { - bucketList = await _context.Buckets.Include(b => b.CreatedBy).Where(b => b.TenantId == tenantId).ToListAsync(); - bucketIds = bucketList.Select(b => b.Id).ToList(); - } - else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) || permissionIds.Contains(PermissionsMaster.DirectoryUser)) - { - bucketList = await _context.Buckets.Include(b => b.CreatedBy).Where(b => bucketIds.Contains(b.Id) || b.CreatedByID == LoggedInEmployee.Id).ToListAsync(); - } - else - { - _logger.LogWarning("Employee {EmployeeId} attemped to access a buckets list, but do not have permission", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("You don't have permission", "You don't have permission", 401); - } - - List employeeBucketVM = await _context.EmployeeBucketMappings.Where(b => bucketIds.Contains(b.BucketId)).ToListAsync(); - - List bucketVMs = new List(); - if (bucketList.Any()) - { - bucketIds = bucketList.Select(b => b.Id).ToList(); - List? contactBucketMappings = await _context.ContactBucketMappings.Where(cb => bucketIds.Contains(cb.BucketId)).ToListAsync(); - foreach (var bucket in bucketList) - { - List employeeBucketMappings = employeeBucketVM.Where(eb => eb.BucketId == bucket.Id).ToList(); - var emplyeeIds = employeeBucketMappings.Select(eb => eb.EmployeeId).ToList(); - List? contactBuckets = contactBucketMappings.Where(cb => cb.BucketId == bucket.Id).ToList(); - AssignBucketVM bucketVM = bucket.ToAssignBucketVMFromBucket(); - if (bucketVM.CreatedBy != null) - { - emplyeeIds.Add(bucketVM.CreatedBy.Id); - } - bucketVM.EmployeeIds = emplyeeIds.Distinct().ToList(); - bucketVM.NumberOfContacts = contactBuckets.Count; - bucketVMs.Add(bucketVM); - } - } - - _logger.LogInfo("{count} Buckets are fetched by Employee with ID {LoggedInEmployeeId}", bucketVMs.Count, LoggedInEmployee.Id); - return ApiResponse.SuccessResponse(bucketVMs, $"{bucketVMs.Count} buckets fetched successfully", 200); - } - public async Task> CreateBucket(CreateBucketDto bucketDto) - { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - if (bucketDto != null) - { - var assignedRoleIds = await _context.EmployeeRoleMappings.Where(r => r.EmployeeId == LoggedInEmployee.Id).Select(r => r.RoleId).ToListAsync(); - var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); - var demo = !permissionIds.Contains(PermissionsMaster.DirectoryUser); - if (!permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && !permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && !permissionIds.Contains(PermissionsMaster.DirectoryUser)) - { - _logger.LogWarning("Employee {EmployeeId} attemped to create a bucket, but do not have permission", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("You don't have permission", "You don't have permission", 401); - } - - var existingBucket = await _context.Buckets.FirstOrDefaultAsync(b => b.Name == bucketDto.Name); - if (existingBucket != null) - { - _logger.LogWarning("Employee ID {LoggedInEmployeeId} attempted to create an existing bucket.", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("Bucket already existed", "Bucket already existed", 409); - } - Bucket bucket = new Bucket - { - Name = bucketDto.Name, - Description = bucketDto.Description, - CreatedAt = DateTime.UtcNow, - CreatedByID = LoggedInEmployee.Id, - TenantId = tenantId - }; - _context.Buckets.Add(bucket); - - EmployeeBucketMapping employeeBucket = new EmployeeBucketMapping - { - EmployeeId = LoggedInEmployee.Id, - BucketId = bucket.Id - }; - - _context.EmployeeBucketMappings.Add(employeeBucket); - await _context.SaveChangesAsync(); - bucket = await _context.Buckets.Include(b => b.CreatedBy).FirstOrDefaultAsync(b => b.Id == bucket.Id) ?? new Bucket(); - BucketVM bucketVM = bucket.ToBucketVMFromBucket(); - _logger.LogInfo("Employee Id {LoggedInEmployeeId} creayted new bucket {BucketId}", LoggedInEmployee.Id, bucket.Id); - return ApiResponse.SuccessResponse(bucketVM, "Bucket Created SuccessFully", 200); - } - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} sended empty payload", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("User Send empty Payload", "User Send empty Payload", 400); - } - public async Task> UpdateBucket(Guid id, UpdateBucketDto bucketDto) - { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - if (bucketDto != null && id == bucketDto.Id) - { - var assignedRoleIds = await _context.EmployeeRoleMappings.Where(r => r.EmployeeId == LoggedInEmployee.Id).Select(r => r.RoleId).ToListAsync(); - var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); - var employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.BucketId == id).ToListAsync(); - var bucketIds = employeeBuckets.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).Select(eb => eb.BucketId).ToList(); - Bucket? bucket = await _context.Buckets.Include(b => b.CreatedBy).FirstOrDefaultAsync(b => b.Id == bucketDto.Id && b.TenantId == tenantId); - - if (bucket == null) - { - _logger.LogWarning("Employee ID {LoggedInEmployeeId} attempted to update a bucket but not found in database.", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("Bucket not found", "Bucket not found", 404); - } - - Bucket? accessableBucket = null; - if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) - { - accessableBucket = bucket; - } - else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && bucketIds.Contains(id)) - { - accessableBucket = bucket; - } - else if (permissionIds.Contains(PermissionsMaster.DirectoryUser)) - { - if (bucket.CreatedByID == LoggedInEmployee.Id) - { - accessableBucket = bucket; - } - } - if (accessableBucket == null) - { - _logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id); - return ApiResponse.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 401); - } - - bucket.Name = bucketDto.Name ?? ""; - bucket.Description = bucketDto.Description ?? ""; - - _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog - { - RefereanceId = bucketDto.Id, - UpdatedById = LoggedInEmployee.Id, - UpdateAt = DateTime.UtcNow - }); - - await _context.SaveChangesAsync(); - - AssignBucketVM bucketVM = bucket.ToAssignBucketVMFromBucket(); - List employeeBucketMappings = employeeBuckets.Where(eb => eb.BucketId == bucket.Id).ToList(); - List contactBuckets = await _context.ContactBucketMappings.Where(eb => eb.BucketId == bucket.Id).ToListAsync(); - var employeeIds = employeeBucketMappings.Select(eb => eb.EmployeeId).ToList(); - bucketVM.EmployeeIds = employeeIds; - bucketVM.NumberOfContacts = contactBuckets.Count; - - _logger.LogInfo("Employee Id {LoggedInEmployeeId} Updated new bucket {BucketId}", LoggedInEmployee.Id, bucket.Id); - return ApiResponse.SuccessResponse(bucketVM, "Bucket update successFully", 200); - } - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} sended empty payload", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("User Send empty Payload", "User Send empty Payload", 400); - } - public async Task> AssignBucket(Guid bucketId, List assignBuckets) - { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - if (assignBuckets != null && bucketId != Guid.Empty) - { - var assignedRoleIds = await _context.EmployeeRoleMappings.Where(r => r.EmployeeId == LoggedInEmployee.Id).Select(r => r.RoleId).ToListAsync(); - var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); - - Bucket? bucket = await _context.Buckets.Include(b => b.CreatedBy).FirstOrDefaultAsync(b => b.Id == bucketId && b.TenantId == tenantId); - - if (bucket == null) - { - _logger.LogWarning("Employee ID {LoggedInEmployeeId} attempted to update a bucket but not found in database.", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("Bucket not found", "Bucket not found", 404); - } - var employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.BucketId == bucketId).ToListAsync(); - var bucketIds = employeeBuckets.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).Select(eb => eb.BucketId).ToList(); - var employeeBucketIds = employeeBuckets.Select(eb => eb.EmployeeId).ToList(); - Bucket? accessableBucket = null; - if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) - { - accessableBucket = bucket; - } - else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && bucketIds.Contains(bucketId)) - { - accessableBucket = bucket; - } - else if (permissionIds.Contains(PermissionsMaster.DirectoryUser)) - { - if (bucket.CreatedByID == LoggedInEmployee.Id) - { - accessableBucket = bucket; - } - } - if (accessableBucket == null) - { - _logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id); - return ApiResponse.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 401); - } - var employeeIds = await _context.Employees.Where(e => e.TenantId == tenantId && e.IsActive).Select(e => e.Id).ToListAsync(); - int assignedEmployee = 0; - int removededEmployee = 0; - foreach (var assignBucket in assignBuckets) - { - if (employeeIds.Contains(assignBucket.EmployeeId)) - { - if (assignBucket.IsActive && !employeeBucketIds.Contains(assignBucket.EmployeeId)) - { - EmployeeBucketMapping employeeBucketMapping = new EmployeeBucketMapping - { - EmployeeId = assignBucket.EmployeeId, - BucketId = bucketId - }; - _context.EmployeeBucketMappings.Add(employeeBucketMapping); - assignedEmployee += 1; - } - else if (!assignBucket.IsActive) - { - EmployeeBucketMapping? employeeBucketMapping = employeeBuckets.FirstOrDefault(eb => eb.BucketId == bucketId && eb.EmployeeId == assignBucket.EmployeeId); - if (employeeBucketMapping != null) - { - _context.EmployeeBucketMappings.Remove(employeeBucketMapping); - removededEmployee += 1; - } - } - } - } - - _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog - { - RefereanceId = bucketId, - UpdatedById = LoggedInEmployee.Id, - UpdateAt = DateTime.UtcNow - }); - - await _context.SaveChangesAsync(); - - AssignBucketVM bucketVM = bucket.ToAssignBucketVMFromBucket(); - List employeeBucketMappings = await _context.EmployeeBucketMappings.Where(eb => eb.BucketId == bucket.Id).ToListAsync(); - List contactBuckets = await _context.ContactBucketMappings.Where(eb => eb.BucketId == bucket.Id).ToListAsync(); - employeeIds = employeeBucketMappings.Select(eb => eb.EmployeeId).ToList(); - bucketVM.EmployeeIds = employeeIds; - bucketVM.NumberOfContacts = contactBuckets.Count; - - if (assignedEmployee > 0) - { - _logger.LogInfo("Employee {EmployeeId} assigned bucket {BucketId} to {conut} number of employees", LoggedInEmployee.Id, bucketId, assignedEmployee); - } - if (removededEmployee > 0) - { - _logger.LogWarning("Employee {EmployeeId} removed {conut} number of employees from bucket {BucketId}", LoggedInEmployee.Id, removededEmployee, bucketId); - } - return ApiResponse.SuccessResponse(bucketVM, "Details updated successfully", 200); - } - _logger.LogWarning("Employee with ID {LoggedInEmployeeId} sended empty payload", LoggedInEmployee.Id); - return ApiResponse.ErrorResponse("User Send empty Payload", "User Send empty Payload", 400); - } - public async Task> DeleteBucket(Guid id) - { - Guid tenantId = _userHelper.GetTenantId(); - var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); - - Bucket? bucket = await _context.Buckets.FirstOrDefaultAsync(n => n.Id == id && n.TenantId == tenantId); - - if (bucket != null) - { - List? employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.BucketId == id).ToListAsync(); - List? contactBuckets = await _context.ContactBucketMappings.Where(eb => eb.BucketId == id).ToListAsync(); - - if (contactBuckets.Any()) - { - _logger.LogInfo("Employee {EmployeeId} attempted to deleted bucket {BucketId},but bucket have contacts in it.", LoggedInEmployee.Id, id); - return ApiResponse.ErrorResponse("This bucket can not be deleted", "This bucket can not be deleted", 400); - } - - var assignedRoleIds = await _context.EmployeeRoleMappings.Where(r => r.EmployeeId == LoggedInEmployee.Id).Select(r => r.RoleId).ToListAsync(); - var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); - var bucketIds = employeeBuckets.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).Select(eb => eb.BucketId).ToList(); - - Bucket? accessableBucket = null; - if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) - { - accessableBucket = bucket; - } - else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) && bucketIds.Contains(id)) - { - accessableBucket = bucket; - } - else if (permissionIds.Contains(PermissionsMaster.DirectoryUser)) - { - if (bucket.CreatedByID == LoggedInEmployee.Id) - { - accessableBucket = bucket; - } - } - if (accessableBucket == null) - { - _logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without the necessary permissions.", LoggedInEmployee.Id, bucket.Id); - return ApiResponse.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 401); - } - - _context.EmployeeBucketMappings.RemoveRange(employeeBuckets); - _context.Buckets.Remove(bucket); - _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog - { - RefereanceId = id, - UpdatedById = LoggedInEmployee.Id, - UpdateAt = DateTime.UtcNow - }); - await _context.SaveChangesAsync(); - _logger.LogInfo("Employee {EmployeeId} deleted bucket {BucketId} and related entries", LoggedInEmployee.Id, id); - return ApiResponse.SuccessResponse(new { }, "Bucket deleted successfully", 200); - } - - _logger.LogWarning("Employee {EmployeeId} tries to delete bucket {BucketId} but not found in database", LoggedInEmployee.Id, id); - return ApiResponse.SuccessResponse(new { }, "Bucket deleted successfully", 200); - } - - // -------------------------------- Helper -------------------------------- - - private bool Compare(string sentence, string search) - { - sentence = sentence.Trim().ToLower(); - search = search.Trim().ToLower(); - - // Check for exact substring - bool result = sentence.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0; - return result; - } - } -} \ No newline at end of file diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index 1c516e1..9fb3f60 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -1,4 +1,6 @@ using AutoMapper; +using Marco.Pms.Model.Directory; +using Marco.Pms.Model.Dtos.Directory; using Marco.Pms.Model.AppMenu; using Marco.Pms.Model.Dtos.AppMenu; using Marco.Pms.Model.Dtos.Expenses; @@ -18,6 +20,7 @@ using Marco.Pms.Model.Projects; using Marco.Pms.Model.TenantModels; using Marco.Pms.Model.TenantModels.MongoDBModel; using Marco.Pms.Model.ViewModels.Activities; +using Marco.Pms.Model.ViewModels.Directory; using Marco.Pms.Model.ViewModels.DocumentManager; using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Expanses; @@ -158,6 +161,7 @@ namespace Marco.Pms.Services.MappingProfiles #endregion #region ======================================================= Employee ======================================================= + CreateMap(); CreateMap() .ForMember( @@ -330,6 +334,43 @@ namespace Marco.Pms.Services.MappingProfiles dest => dest.Name, opt => opt.MapFrom(src => src.Text)); #endregion + + #region ======================================================= Directory ======================================================= + + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + + + CreateMap(); + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + CreateMap(); + + CreateMap(); + + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap() + .ForMember( + dest => dest.ContactName, + opt => opt.MapFrom(src => src.Contact != null ? src.Contact.Name : string.Empty) + ) + .ForMember( + dest => dest.OrganizationName, + opt => opt.MapFrom(src => src.Contact != null ? src.Contact.Organization : string.Empty) + ); + CreateMap(); + CreateMap(); + + #endregion } } } diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 0b06de0..71b4e28 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -178,6 +178,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); #endregion #region Helpers @@ -185,7 +186,6 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Marco.Pms.Services/Service/DirectoryService.cs b/Marco.Pms.Services/Service/DirectoryService.cs new file mode 100644 index 0000000..d522f66 --- /dev/null +++ b/Marco.Pms.Services/Service/DirectoryService.cs @@ -0,0 +1,2969 @@ +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.Activities; +using Marco.Pms.Model.ViewModels.Directory; +using Marco.Pms.Model.ViewModels.Master; +using Marco.Pms.Model.ViewModels.Projects; +using Marco.Pms.Services.Service.ServiceInterfaces; +using MarcoBMS.Services.Helpers; +using MarcoBMS.Services.Service; +using Microsoft.CodeAnalysis; +using Microsoft.EntityFrameworkCore; +using System.Text.Json; + +namespace Marco.Pms.Services.Service +{ + public class DirectoryService : IDirectoryService + { + private readonly IDbContextFactory _dbContextFactory; + private readonly ApplicationDbContext _context; + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly ILoggingService _logger; + private readonly UserHelper _userHelper; + private readonly IMapper _mapper; + private readonly UtilityMongoDBHelper _updateLogsHelper; + + + private static readonly string contactCollection = "ContactModificationLog"; + private static readonly string contactPhoneCollection = "ContactPhoneModificationLog"; + private static readonly string contactEmailCollection = "ContactEmailModificationLog"; + private static readonly string bucketCollection = "BucketModificationLog"; + private static readonly string contactNoteCollection = "ContactNoteModificationLog"; + + + public DirectoryService( + IDbContextFactory dbContextFactory, + ApplicationDbContext context, + ILoggingService logger, + IServiceScopeFactory serviceScopeFactory, + UserHelper userHelper, + IMapper mapper) + { + _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); + _context = context ?? throw new ArgumentNullException(nameof(context)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); + _userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + using var scope = serviceScopeFactory.CreateScope(); + _updateLogsHelper = scope.ServiceProvider.GetRequiredService(); + } + #region =================================================================== Contact APIs =================================================================== + + #region =================================================================== Contact Get APIs =================================================================== + + /// + /// Retrieves a paginated list of contacts based on permissions, search criteria, and filters. + /// + /// A search term to filter contacts by name, organization, email, phone, or tag. + /// A JSON string representing ContactFilterDto for advanced filtering. + /// Optional project ID to filter contacts assigned to a specific project. + /// Boolean to filter for active or inactive contacts. + /// The number of records per page. + /// The current page number. + /// The ID of the tenant to which the contacts belong. + /// The employee making the request, used for permission checks. + /// An ApiResponse containing the paginated list of contacts or an error. + public async Task> GetListOfContactsAsync(string? search, string? filter, Guid? projectId, bool active, int pageSize, int pageNumber, Guid tenantId, Employee loggedInEmployee) + { + Guid loggedInEmployeeId = loggedInEmployee.Id; + _logger.LogInfo( + "Attempting to fetch contact list for Tenant {TenantId} by Employee {EmployeeId}. Search: '{Search}', Filter: '{Filter}'", + tenantId, loggedInEmployeeId, search ?? "", filter ?? ""); + + try + { + // Use a single DbContext for the entire operation to ensure consistency. + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + + // Step 1: Perform initial permission checks in parallel. + var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployeeId); + + // Step 2: Build the core IQueryable with all filtering logic applied on the server. + // This is the most critical optimization. + var contactQuery = dbContext.Contacts.Where(c => c.TenantId == tenantId && c.IsActive == active); + + // --- Permission-based Filtering --- + if (!hasAdminPermission) + { + if (hasManagerPermission || hasUserPermission) + { + // User can see contacts in buckets they created or are assigned to. + var employeeBucketIds = await dbContext.EmployeeBucketMappings + .Where(eb => eb.EmployeeId == loggedInEmployeeId) + .Select(eb => eb.BucketId) + .ToListAsync(); + + var accessibleBucketIds = await dbContext.Buckets + .Where(b => b.TenantId == tenantId && (b.CreatedByID == loggedInEmployeeId || employeeBucketIds.Contains(b.Id))) + .Select(b => b.Id) + .ToListAsync(); + + contactQuery = contactQuery.Where(c => dbContext.ContactBucketMappings.Any(cbm => + cbm.ContactId == c.Id && accessibleBucketIds.Contains(cbm.BucketId))); + } + else + { + _logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get contact list due to lack of permissions.", loggedInEmployeeId); + return ApiResponse.SuccessResponse("Access Denied", "You do not have permission to view any contacts.", 403); + } + } + + // --- Advanced Filtering from 'filter' parameter --- + ContactFilterDto? contactFilter = TryDeserializeContactFilter(filter); + if (contactFilter?.BucketIds?.Any() ?? false) + { + // Note: Permission filtering is already applied. Here we further restrict by the user's filter choice. + contactQuery = contactQuery.Where(c => dbContext.ContactBucketMappings.Any(cbm => + cbm.ContactId == c.Id && contactFilter.BucketIds.Contains(cbm.BucketId))); + } + if (contactFilter?.CategoryIds?.Any() ?? false) + { + contactQuery = contactQuery.Where(c => c.ContactCategoryId.HasValue && contactFilter.CategoryIds.Contains(c.ContactCategoryId.Value)); + } + + // --- Standard Filtering --- + if (projectId != null) + { + contactQuery = contactQuery.Where(c => dbContext.ContactProjectMappings.Any(cpm => cpm.ContactId == c.Id && cpm.ProjectId == projectId)); + } + + // --- Search Term Filtering --- + if (!string.IsNullOrWhiteSpace(search)) + { + var searchTermLower = search.ToLower(); + contactQuery = contactQuery.Where(c => + (c.Name != null && c.Name.ToLower().Contains(searchTermLower)) || + (c.Organization != null && c.Organization.ToLower().Contains(searchTermLower)) || + dbContext.ContactsEmails.Any(e => e.ContactId == c.Id && e.EmailAddress.ToLower().Contains(searchTermLower)) || + dbContext.ContactsPhones.Any(p => p.ContactId == c.Id && p.PhoneNumber.ToLower().Contains(searchTermLower)) || + dbContext.ContactTagMappings.Any(ctm => ctm.ContactId == c.Id && ctm.ContactTag != null && ctm.ContactTag.Name.ToLower().Contains(searchTermLower)) + ); + } + + // Step 3: Get the total count for pagination before applying Skip/Take. + var totalCount = await contactQuery.CountAsync(); + if (totalCount == 0) + { + return ApiResponse.SuccessResponse(new { TotalPages = 0, CurrentPage = pageNumber, PageSize = pageSize, Data = new List() }, "No contacts found matching the criteria.", 200); + } + + // Step 4: Apply ordering and pagination to get the primary Contact entities for the current page. + var contacts = await contactQuery + .OrderBy(c => c.Name) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .Include(c => c.ContactCategory) // Include direct navigation properties if they exist + .ToListAsync(); + + var finalContactIds = contacts.Select(c => c.Id).ToList(); + + // Step 5: Batch-fetch all related data for only the contacts on the current page. + // This is highly efficient and avoids the N+1 problem. + var phonesTask = Task.Run(async () => + { + await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync(); + return await taskDbContext.ContactsPhones + .Where(p => finalContactIds.Contains(p.ContactId) && p.TenantId == tenantId) + .ToListAsync(); + }); + + var emailsTask = Task.Run(async () => + { + await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync(); + return await taskDbContext.ContactsEmails + .Where(e => finalContactIds.Contains(e.ContactId) && e.TenantId == tenantId) + .ToListAsync(); + }); + + var tagsTask = Task.Run(async () => + { + await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync(); + return await taskDbContext.ContactTagMappings + .Include(t => t.ContactTag) + .Where(t => finalContactIds.Contains(t.ContactId)) + .ToListAsync(); + }); + + var projectsTask = Task.Run(async () => + { + await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync(); + return await taskDbContext.ContactProjectMappings + .Where(p => finalContactIds.Contains(p.ContactId)) + .ToListAsync(); + }); + + var bucketsTask = Task.Run(async () => + { + await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync(); + return await taskDbContext.ContactBucketMappings + .Where(b => finalContactIds.Contains(b.ContactId)) + .ToListAsync(); + }); + + // Now, await all the independent, thread-safe tasks. + await Task.WhenAll(phonesTask, emailsTask, tagsTask, projectsTask, bucketsTask); + + // Use Lookups for efficient in-memory mapping. + var phonesLookup = (phonesTask.Result).ToLookup(p => p.ContactId); + var emailsLookup = (emailsTask.Result).ToLookup(e => e.ContactId); + var tagsLookup = (tagsTask.Result).ToLookup(t => t.ContactId); + var projectsLookup = (projectsTask.Result).ToLookup(p => p.ContactId); + var bucketsLookup = (bucketsTask.Result).ToLookup(b => b.ContactId); + + // Step 6: Map entities to ViewModels, populating with the pre-fetched related data. + var list = contacts.Select(c => + { + var contactVM = _mapper.Map(c); + contactVM.ContactPhones = _mapper.Map>(phonesLookup[c.Id]); + contactVM.ContactEmails = _mapper.Map>(emailsLookup[c.Id]); + contactVM.Tags = _mapper.Map>(tagsLookup[c.Id].Select(ctm => ctm.ContactTag)); + contactVM.ProjectIds = projectsLookup[c.Id].Select(p => p.ProjectId).ToList(); + contactVM.BucketIds = bucketsLookup[c.Id].Select(b => b.BucketId).ToList(); + return contactVM; + }).ToList(); + + // Step 7: Construct and return the final response. + var response = new + { + TotalPages = (int)Math.Ceiling((double)totalCount / pageSize), + CurrentPage = pageNumber, + PageSize = pageSize, + Data = list + }; + + _logger.LogInfo("{Count} contacts were fetched successfully for Tenant {TenantId} by Employee {EmployeeId}", list.Count, tenantId, loggedInEmployeeId); + return ApiResponse.SuccessResponse(response, $"{list.Count} contacts fetched successfully.", 200); + } + catch (Exception ex) + { + _logger.LogError(ex, "An unexpected error occurred while fetching contact list for Tenant {TenantId} by Employee {EmployeeId}", tenantId, loggedInEmployeeId); + return ApiResponse.ErrorResponse("An internal error occurred.", ExceptionMapper(ex), 500); + } + } + public async Task> GetListOfContactsOld(string? search, bool active, ContactFilterDto? filterDto, Guid? projectId) + { + Guid tenantId = _userHelper.GetTenantId(); + var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var assignedRoleIds = await _context.EmployeeRoleMappings.Where(r => r.EmployeeId == LoggedInEmployee.Id).Select(r => r.RoleId).ToListAsync(); + var permissionIds = await _context.RolePermissionMappings.Where(rp => assignedRoleIds.Contains(rp.ApplicationRoleId)).Select(rp => rp.FeaturePermissionId).Distinct().ToListAsync(); + List? employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).ToListAsync(); + List bucketIds = employeeBuckets.Select(c => c.BucketId).ToList(); + if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin)) + { + var buckets = await _context.Buckets.Where(b => b.TenantId == tenantId).ToListAsync(); + bucketIds = buckets.Select(b => b.Id).ToList(); + } + else if (permissionIds.Contains(PermissionsMaster.DirectoryAdmin) || permissionIds.Contains(PermissionsMaster.DirectoryUser)) + { + var buckets = await _context.Buckets.Where(b => b.CreatedByID == LoggedInEmployee.Id).ToListAsync(); + var createdBucketIds = buckets.Select(b => b.Id).ToList(); + bucketIds.AddRange(createdBucketIds); + bucketIds = bucketIds.Distinct().ToList(); + } + else + { + _logger.LogWarning("Employee {EmployeeId} attemped to access a contacts, but do not have permission", LoggedInEmployee.Id); + return ApiResponse.ErrorResponse("You don't have permission", "You don't have permission", 403); + } + + List filterbucketIds = bucketIds; + if (filterDto != null && filterDto.BucketIds != null && filterDto.BucketIds.Count > 0) + { + filterbucketIds = filterDto.BucketIds; + } + List? contactBuckets = await _context.ContactBucketMappings.Where(cb => bucketIds.Contains(cb.BucketId)).ToListAsync(); + List contactIds = contactBuckets.Where(b => filterbucketIds.Contains(b.BucketId)).Select(cb => cb.ContactId).ToList(); + List contacts = new List(); + + var contactProjects = await _context.ContactProjectMappings.Where(p => contactIds.Contains(p.ContactId)).ToListAsync(); + + if (projectId != null && projectId != Guid.Empty) + { + contactProjects = contactProjects.Where(p => p.ProjectId == projectId).ToList(); + contactIds = contactProjects.Select(p => p.ContactId).Distinct().ToList(); + } + + if (filterDto != null && filterDto.CategoryIds != null && filterDto.CategoryIds.Count > 0) + { + var categoryIds = filterDto.CategoryIds; + contacts = await _context.Contacts.Include(c => c.ContactCategory).Where(c => contactIds.Contains(c.Id) && categoryIds.Contains(c.ContactCategoryId ?? Guid.Empty) && c.TenantId == tenantId && c.IsActive == active).ToListAsync(); + } + else + { + contacts = await _context.Contacts.Include(c => c.ContactCategory).Where(c => contactIds.Contains(c.Id) && c.TenantId == tenantId && c.IsActive == active).ToListAsync(); + } + + var phoneNo = await _context.ContactsPhones.Where(p => contactIds.Contains(p.ContactId)).ToListAsync(); + var Emails = await _context.ContactsEmails.Where(E => contactIds.Contains(E.ContactId)).ToListAsync(); + var Tags = await _context.ContactTagMappings.Where(t => contactIds.Contains(t.ContactId)).ToListAsync(); + + List TagIds = Tags.Select(t => t.ContactTagId).ToList(); + + var TagList = await _context.ContactTagMasters.Where(t => TagIds.Contains(t.Id)).ToListAsync(); + + if (search != null && search != string.Empty) + { + List filteredContactIds = new List(); + phoneNo = phoneNo.Where(p => Compare(p.PhoneNumber, search)).ToList(); + filteredContactIds = phoneNo.Select(p => p.ContactId).ToList(); + + Emails = Emails.Where(e => Compare(e.EmailAddress, search)).ToList(); + filteredContactIds.AddRange(Emails.Select(e => e.ContactId).ToList()); + filteredContactIds = filteredContactIds.Distinct().ToList(); + + contacts = contacts.Where(c => Compare(c.Name, search) || Compare(c.Organization, search) || filteredContactIds.Contains(c.Id)).ToList(); + } + + List list = new List(); + + foreach (var contact in contacts) + { + + ContactVM contactVM = new ContactVM(); + List contactEmailVms = new List(); + List contactPhoneVms = new List(); + + List conatctTagVms = new List(); + var phones = phoneNo.Where(p => p.ContactId == contact.Id).ToList(); + var emails = Emails.Where(e => e.ContactId == contact.Id).ToList(); + var tagMappingss = Tags.Where(t => t.ContactId == contact.Id).ToList(); + var projectMapping = contactProjects.Where(p => p.ContactId == contact.Id).ToList(); + var bucketMapping = contactBuckets.Where(b => b.ContactId == contact.Id).ToList(); + + + if (emails != null && emails.Count > 0) + { + foreach (var email in emails) + { + ContactEmailVM emailVM = new ContactEmailVM(); + emailVM = email.ToContactEmailVMFromContactEmail(); + contactEmailVms.Add(emailVM); + } + } + + if (phones != null && phones.Count > 0) + { + foreach (var phone in phones) + { + ContactPhoneVM phoneVM = new ContactPhoneVM(); + phoneVM = phone.ToContactPhoneVMFromContactPhone(); + contactPhoneVms.Add(phoneVM); + } + + } + if (tagMappingss != null && tagMappingss.Count > 0) + { + foreach (var tagMapping in tagMappingss) + { + ContactTagVM tagVM = new ContactTagVM(); ; + var tag = TagList.Find(t => t.Id == tagMapping.ContactTagId); + + tagVM = tag != null ? tag.ToContactTagVMFromContactTagMaster() : new ContactTagVM(); + conatctTagVms.Add(tagVM); + + + } + } + contactVM = contact.ToContactVMFromContact(); + + if (projectMapping != null && projectMapping.Count > 0) + { + contactVM.ProjectIds = projectMapping.Select(p => p.ProjectId).ToList(); + } + if (bucketMapping != null && bucketMapping.Count > 0) + { + contactVM.BucketIds = bucketMapping.Select(p => p.BucketId).ToList(); + } + + contactVM.ContactEmails = contactEmailVms; + contactVM.ContactPhones = contactPhoneVms; + contactVM.Tags = conatctTagVms; + + list.Add(contactVM); + } + _logger.LogInfo("{count} contacts are fetched by Employee with ID {LoggedInEmployeeId}", list.Count, LoggedInEmployee.Id); + return ApiResponse.SuccessResponse(list, System.String.Format("{0} contacts fetched successfully", list.Count), 200); + + } + public async Task> GetContactsListByBucketIdAsync(Guid bucketId, Guid tenantId, Employee loggedInEmployee) + { + // Validate incoming bucket ID + if (bucketId == Guid.Empty) + { + _logger.LogInfo("Employee ID {EmployeeId} sent an empty Bucket ID.", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Bucket ID is empty", "Bucket ID is empty", 400); + } + + // Check permissions of logged-in employee + var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id); + if (!hasAdminPermission && !hasManagerPermission && !hasUserPermission) + { + _logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get contacts list for tenant {TenantId} due to insufficient permissions.", + loggedInEmployee.Id, tenantId); + return ApiResponse.ErrorResponse("Access Denied", "You do not have permission to perform this action.", 403); + } + + // Use dbContextFactory to create new DbContext instances for parallel calls + using var context = _dbContextFactory.CreateDbContext(); + + // Confirm that the bucket exists and belongs to tenant + var bucket = await context.Buckets.FirstOrDefaultAsync(b => b.Id == bucketId && b.TenantId == tenantId); + if (bucket == null) + { + _logger.LogInfo("Employee ID {EmployeeId} attempted to access non-existent bucket ID {BucketId}.", loggedInEmployee.Id, bucketId); + return ApiResponse.ErrorResponse("Bucket not found", "Bucket not found", 404); + } + + // Load employee-bucket mappings for this bucket to check access + var employeeBuckets = await context.EmployeeBucketMappings.Where(em => em.BucketId == bucketId).ToListAsync(); + + EmployeeBucketMapping? employeeBucket = hasAdminPermission + ? employeeBuckets.FirstOrDefault() + : employeeBuckets.FirstOrDefault(eb => eb.EmployeeId == loggedInEmployee.Id); + + if (employeeBucket == null) + { + _logger.LogInfo("Employee ID {EmployeeId} does not have access to bucket ID {BucketId}.", loggedInEmployee.Id, bucketId); + return ApiResponse.ErrorResponse("You do not have access to this bucket.", "Unauthorized access to bucket.", 403); + } + + // Fetch all contact-bucket mappings for this bucket + var contactBucketMappings = await context.ContactBucketMappings.Where(cb => cb.BucketId == bucketId).ToListAsync(); + + if (contactBucketMappings.Count == 0) + { + _logger.LogInfo("No contacts found in bucket ID {BucketId} for employee {EmployeeId}.", bucketId, loggedInEmployee.Id); + return ApiResponse.SuccessResponse(new List(), "No contacts available in this bucket.", 200); + } + + var contactIds = contactBucketMappings.Select(cb => cb.ContactId).Distinct().ToList(); + + // Parallel fetching of related data via independent contexts to improve performance + var contactTask = Task.Run(async () => + { + await using var _dbContext = await _dbContextFactory.CreateDbContextAsync(); + + return await _dbContext.Contacts.Include(c => c.ContactCategory) + .Where(c => contactIds.Contains(c.Id) && c.IsActive).ToListAsync(); + }); + var phoneTask = Task.Run(async () => + { + await using var _dbContext = await _dbContextFactory.CreateDbContextAsync(); + + return await _dbContext.ContactsPhones.Where(p => contactIds.Contains(p.ContactId)).ToListAsync(); + }); + var emailTask = Task.Run(async () => + { + await using var _dbContext = await _dbContextFactory.CreateDbContextAsync(); + + return await _dbContext.ContactsEmails.Where(e => contactIds.Contains(e.ContactId)).ToListAsync(); + }); + var tagTask = Task.Run(async () => + { + await using var _dbContext = await _dbContextFactory.CreateDbContextAsync(); + + return await _dbContext.ContactTagMappings.Where(ct => contactIds.Contains(ct.ContactId)).ToListAsync(); + }); + var contactProjectTask = Task.Run(async () => + { + await using var _dbContext = await _dbContextFactory.CreateDbContextAsync(); + + return await _dbContext.ContactProjectMappings.Where(cp => contactIds.Contains(cp.ContactId)).ToListAsync(); + }); + var contactBucketTask = Task.Run(async () => + { + await using var _dbContext = await _dbContextFactory.CreateDbContextAsync(); + + return await _dbContext.ContactBucketMappings.Where(cb => contactIds.Contains(cb.ContactId)).ToListAsync(); + }); + + await Task.WhenAll(contactTask); + + var contacts = contactTask.Result; + var phones = phoneTask.Result; + var emails = emailTask.Result; + var tags = tagTask.Result; + var contactProjects = contactProjectTask.Result; + var contactBuckets = contactBucketTask.Result; + + // Load tag metadata if tags found + List tagMasters = new List(); + if (tags.Count > 0) + { + var tagIds = tags.Select(t => t.ContactTagId).Distinct().ToList(); + tagMasters = await context.ContactTagMasters.Where(tm => tagIds.Contains(tm.Id)).ToListAsync(); + } + + var contactVMs = new List(); + + // Build contact view models from data + foreach (var contact in contacts) + { + // Transform phones + var phoneVMs = phones.Where(p => p.ContactId == contact.Id).Select(p => _mapper.Map(p)).ToList(); + + // Transform emails + var emailVMs = emails.Where(e => e.ContactId == contact.Id).Select(e => _mapper.Map(e)).ToList(); + + // Transform tags + var contactTagMappings = tags.Where(t => t.ContactId == contact.Id); + var tagVMs = new List(); + foreach (var ct in contactTagMappings) + { + var tagMaster = tagMasters.Find(tm => tm.Id == ct.ContactTagId); + if (tagMaster != null) + { + tagVMs.Add(_mapper.Map(tagMaster)); + } + } + + // Get project and bucket mappings for the contact + var projectIds = contactProjects.Where(cp => cp.ContactId == contact.Id).Select(cp => cp.ProjectId).ToList(); + var bucketIds = contactBuckets.Where(cb => cb.ContactId == contact.Id).Select(cb => cb.BucketId).ToList(); + + // Create the contact VM + var contactVM = _mapper.Map(contact); + contactVM.ContactPhones = phoneVMs; + contactVM.ContactEmails = emailVMs; + contactVM.Tags = tagVMs; + contactVM.ProjectIds = projectIds; + contactVM.BucketIds = bucketIds; + + contactVMs.Add(contactVM); + } + + _logger.LogInfo("{Count} contacts from Bucket {BucketId} fetched successfully by Employee {EmployeeId}.", contactVMs.Count, bucketId, loggedInEmployee.Id); + + return ApiResponse.SuccessResponse(contactVMs, $"{contactVMs.Count} contacts fetched successfully.", 200); + } + public async Task> GetContactProfileAsync(Guid id, Guid tenantId, Employee loggedInEmployee) + { + Guid loggedInEmployeeId = loggedInEmployee.Id; + if (id == Guid.Empty) + { + _logger.LogInfo("Employee ID {EmployeeId} sent an empty contact id", loggedInEmployeeId); + return ApiResponse.ErrorResponse("Contact ID is empty", "Contact ID is empty", 400); + } + try + { + // Use a single DbContext for the entire operation to ensure consistency. + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + + // Step 1: Perform initial permission checks in parallel. + var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployeeId); + + if (!hasAdminPermission && !hasManagerPermission && !hasUserPermission) + { + _logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get contact profile due to lack of permissions.", loggedInEmployeeId); + return ApiResponse.ErrorResponse("Access Denied", "You do not have permission to view contact.", 403); + } + + Contact? contact = await dbContext.Contacts + .AsNoTracking() // Use AsNoTracking for read-only operations to improve performance. + .Include(c => c.ContactCategory) + .Include(c => c.CreatedBy) + .ThenInclude(e => e!.JobRole) + .Include(c => c.UpdatedBy) + .ThenInclude(e => e!.JobRole) + .FirstOrDefaultAsync(c => c.Id == id && c.IsActive && c.TenantId == tenantId); + if (contact == null) + { + _logger.LogWarning("Employee with ID {LoggedInEmployeeId} tries to update contact with ID {ContactId} is not found in database", loggedInEmployeeId); + return ApiResponse.ErrorResponse("Contact not found", "Contact not found", 404); + } + ContactProfileVM contactVM = _mapper.Map(contact); + + var phonesTask = Task.Run(async () => + { + await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync(); + return await taskDbContext.ContactsPhones + .AsNoTracking() + .Where(p => p.ContactId == contact.Id && p.TenantId == tenantId) + .Select(p => _mapper.Map(p)) + .ToListAsync(); + }); + + var emailsTask = Task.Run(async () => + { + await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync(); + return await taskDbContext.ContactsEmails + .AsNoTracking() + .Where(e => e.ContactId == contact.Id && e.TenantId == tenantId) + .Select(e => _mapper.Map(e)) + .ToListAsync(); + }); + + var contactProjectsTask = Task.Run(async () => + { + await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync(); + return await taskDbContext.ContactProjectMappings + .AsNoTracking() + .Include(cp => cp.Project) + .Where(cp => cp.ContactId == contact.Id && cp.Project != null && cp.Project.TenantId == tenantId) + .Select(cp => new BasicProjectVM + { + Id = cp.Project!.Id, + Name = cp.Project.Name + }) + .ToListAsync(); + }); + + var contactBucketsTask = Task.Run(async () => + { + await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync(); + var bucketQuery = taskDbContext.ContactBucketMappings + .AsNoTracking() + .Include(cb => cb.Bucket) + .ThenInclude(b => b!.CreatedBy) + .ThenInclude(e => e!.JobRole) + .Where(cb => cb.ContactId == contact.Id && cb.Bucket != null && cb.Bucket.TenantId == tenantId); + + if (hasAdminPermission) + { + return await bucketQuery + .Select(cb => _mapper.Map(cb.Bucket)) + .ToListAsync(); + } + List employeeBucketIds = await taskDbContext.EmployeeBucketMappings + .AsNoTracking() + .Where(eb => eb.EmployeeId == loggedInEmployeeId) + .Select(eb => eb.BucketId) + .ToListAsync(); + + return await bucketQuery + .Where(cb => employeeBucketIds.Contains(cb.BucketId)) + .Select(cb => _mapper.Map(cb.Bucket)) + .ToListAsync(); + + }); + + var contactTagsTask = Task.Run(async () => + { + await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync(); + return await taskDbContext.ContactTagMappings + .AsNoTracking() + .Include(ct => ct.ContactTag) + .Where(ct => ct.ContactId == contact.Id && ct.ContactTag != null && ct.ContactTag.TenantId == tenantId) + .Select(ct => _mapper.Map(ct.ContactTag)) + .ToListAsync(); + }); + + var contactNotesTask = Task.Run(async () => + { + await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync(); + return await taskDbContext.ContactNotes + .AsNoTracking() + .Include(cn => cn.Createdby) + .ThenInclude(e => e!.JobRole) + .Include(cn => cn.UpdatedBy) + .ThenInclude(e => e!.JobRole) + .Include(cn => cn.Contact) + .Where(cn => cn.ContactId == contact.Id && cn.Createdby != null && cn.Createdby.TenantId == tenantId) + .Select(cn => _mapper.Map(cn)) + .ToListAsync(); + }); + + await Task.WhenAll(phonesTask, emailsTask, contactProjectsTask, contactBucketsTask, contactTagsTask, contactNotesTask); + contactVM.ContactPhones = phonesTask.Result; + contactVM.ContactEmails = emailsTask.Result; + contactVM.Tags = contactTagsTask.Result; + contactVM.Buckets = contactBucketsTask.Result; + contactVM.Projects = contactProjectsTask.Result; + contactVM.Notes = contactNotesTask.Result; + _logger.LogInfo("Employee ID {EmployeeId} fetched profile of contact {ContactId}", loggedInEmployeeId, contact.Id); + return ApiResponse.SuccessResponse(contactVM, "Contact profile fetched successfully"); + } + catch (Exception ex) + { + _logger.LogError(ex, "An unexpected error occurred while fetching contact profile for Tenant {TenantId} by Employee {EmployeeId}", tenantId, loggedInEmployeeId); + return ApiResponse.ErrorResponse("An internal error occurred.", ExceptionMapper(ex), 500); + } + } + + /// + /// Asynchronously retrieves a distinct list of organization names for a given tenant. + /// + /// The unique identifier of the tenant. + /// The employee making the request, used for permission checks. + /// + /// An ApiResponse containing the list of organization names on success, + /// or an error response with appropriate status codes (403 for Forbidden, 500 for internal errors). + /// + public async Task> GetOrganizationListAsync(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 organization list for tenant {TenantId} due to lack of permissions.", employeeId, tenantId); + // Return a strongly-typed error response. + return ApiResponse.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 organization list for Tenant {TenantId} by Employee {EmployeeId}", tenantId, employeeId); + + var organizationList = await _context.Contacts + // Filter contacts by the specified tenant to ensure data isolation and Filter out contacts that do not have an organization name to ensure data quality. + .Where(c => c.TenantId == tenantId && !string.IsNullOrEmpty(c.Organization)) + + // Project only the 'Organization' column. This is a major performance optimization + // as it avoids loading entire 'Contact' entities into memory. + .Select(c => c.Organization) + // 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 {OrganizationCount} distinct organizations for Tenant {TenantId} for employee {EmployeeId}", organizationList.Count, tenantId, employeeId); + + // Return a strongly-typed success response with the data and a descriptive message. + return ApiResponse.SuccessResponse( + organizationList, + $"{organizationList.Count} unique organization(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 organization 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.ErrorResponse("An internal server error occurred.", ExceptionMapper(ex), 500); + } + } + public async Task> 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.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.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.ErrorResponse("An internal server error occurred.", ExceptionMapper(ex), 500); + } + } + + /// + /// Fetches filter options (Buckets and Contact Categories) based on user permissions + /// for a given tenant. + /// + /// The tenant ID. + /// The employee making the request. + /// ApiResponse with Buckets and Contact Categories. + public async Task> GetContactFilterObjectAsync(Guid tenantId, Employee loggedInEmployee) + { + try + { + _logger.LogInfo("Started fetching contact filters for TenantId: {TenantId}, EmployeeId: {EmployeeId}", + tenantId, loggedInEmployee.Id); + + // Step 1: Determine accessible bucket IDs based on permissions + var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id); + List? accessibleBucketIds = null; + + if (hasAdminPermission) + { + // Admin → Has access to all buckets in the tenant + accessibleBucketIds = await _context.Buckets + .Where(b => b.TenantId == tenantId) + .Select(b => b.Id) + .ToListAsync(); + + _logger.LogDebug("Admin access granted. Fetched {Count} tenant buckets.", accessibleBucketIds.Count); + } + else if (hasManagerPermission || hasUserPermission) + { + // Manager/User → Only mapped buckets + accessibleBucketIds = await _context.EmployeeBucketMappings + .Where(eb => eb.EmployeeId == loggedInEmployee.Id) + .Select(eb => eb.BucketId) + .ToListAsync(); + + _logger.LogDebug("Manager/User access granted. Fetched {Count} mapped buckets for EmployeeId: {EmployeeId}", + accessibleBucketIds.Count, loggedInEmployee.Id); + } + else + { + _logger.LogWarning("No permissions found for EmployeeId: {EmployeeId}. Returning empty bucket list.", loggedInEmployee.Id); + } + + // Step 2: Fetch available buckets (accessible OR personally created by the employee) + var buckets = await _context.Buckets + .Where(b => + (accessibleBucketIds != null && accessibleBucketIds.Contains(b.Id) && b.TenantId == tenantId) + || b.CreatedByID == loggedInEmployee.Id) + .Select(b => new + { + Id = b.Id, + Name = b.Name + }) + .OrderBy(b => b.Name) + .Distinct() // Ensures no duplicates (LINQ to Entities distinct works with anonymous types) + .ToListAsync(); + + accessibleBucketIds = buckets.Select(b => b.Id).ToList(); + + _logger.LogInfo("Fetched {Count} buckets for EmployeeId: {EmployeeId}", buckets.Count, loggedInEmployee.Id); + + // Step 3: Fetch contact categories mapped to the retrieved buckets + var contactCategories = await _context.ContactBucketMappings + .AsNoTracking() // Optimized since we are reading only + .Include(cb => cb.Contact) + .ThenInclude(c => c!.ContactCategory) + .Where(cb => + accessibleBucketIds.Contains(cb.BucketId) && + cb.Contact != null && + cb.Contact!.ContactCategory != null) + .Select(cb => new + { + Id = cb.Contact!.ContactCategory!.Id, + Name = cb.Contact.ContactCategory.Name + }) + .OrderBy(cc => cc.Name) + .Distinct() + .ToListAsync(); + + _logger.LogInfo("{Count} contact categories fetched for EmployeeId: {EmployeeId}", contactCategories.Count, loggedInEmployee.Id); + + // Step 4: Prepare response payload + var response = new + { + Buckets = buckets, + ContactCategories = contactCategories + }; + + _logger.LogInfo("Successfully returning filters for TenantId: {TenantId}, EmployeeId: {EmployeeId}", tenantId, loggedInEmployee.Id); + return ApiResponse.SuccessResponse(response, "Filters for contact fetched successfully", 200); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while fetching contact filters for TenantId: {TenantId}, EmployeeId: {EmployeeId}", tenantId, loggedInEmployee.Id); + return ApiResponse.ErrorResponse("An error occurred while fetching filters", 500); + } + } + + #endregion + + #region =================================================================== Contact Post APIs =================================================================== + + /// + /// Creates a new contact along with its associated details such as phone numbers, emails, tags, and project/bucket mappings. + /// This operation is performed within a single database transaction to ensure data integrity. + /// + /// The DTO containing the details for the new contact. + /// The ID of the tenant to which the contact belongs. + /// The employee performing the action. + /// An ApiResponse containing the newly created contact's view model or an error. + public async Task> CreateContactAsync(CreateContactDto createContact, Guid tenantId, Employee loggedInEmployee) + { + Guid loggedInEmployeeId = loggedInEmployee.Id; + + if (string.IsNullOrWhiteSpace(createContact.Name) || + string.IsNullOrWhiteSpace(createContact.Organization) || + !(createContact.BucketIds?.Any() ?? false)) + { + _logger.LogWarning("Validation failed for CreateContactAsync. Payload missing required fields. Triggered by Employee: {LoggedInEmployeeId}", loggedInEmployeeId); + return ApiResponse.ErrorResponse("Payload is missing required fields: Name, Organization, and at least one BucketId are required.", "Invalid Payload", 400); + } + + using var transaction = await _context.Database.BeginTransactionAsync(); + _logger.LogInfo("Starting transaction to create contact for Tenant {TenantId} by Employee {EmployeeId}", tenantId, loggedInEmployeeId); + + try + { + var contact = _mapper.Map(createContact); + contact.CreatedAt = DateTime.UtcNow; + contact.CreatedById = loggedInEmployeeId; + contact.TenantId = tenantId; + _context.Contacts.Add(contact); + + // --- Process Phones --- + var existingPhoneList = await _context.ContactsPhones + .Where(p => p.TenantId == tenantId && p.IsPrimary) + .Select(p => p.PhoneNumber) + .ToListAsync(); + var existingPhones = new HashSet(existingPhoneList); + var phoneVMs = ProcessContactPhones(createContact, contact, existingPhones); + + // --- Process Emails --- + var existingEmailList = await _context.ContactsEmails + .Where(e => e.TenantId == tenantId && e.IsPrimary) + .Select(e => e.EmailAddress) + .ToListAsync(); + var existingEmails = new HashSet(existingEmailList, StringComparer.OrdinalIgnoreCase); + var emailVMs = ProcessContactEmails(createContact, contact, existingEmails); + + // --- Process Tags --- + var tenantTags = await _context.ContactTagMasters + .Where(t => t.TenantId == tenantId) + .ToDictionaryAsync(t => t.Name.ToLowerInvariant(), t => t); + var tagVMs = ProcessTags(createContact, contact, tenantTags); + + // --- Process Mappings --- + var contactBucketMappings = await ProcessBucketMappingsAsync(createContact, contact, tenantId); + var projectMappings = await ProcessProjectMappingsAsync(createContact, contact, tenantId); + + // --- Final Save and Commit --- + try + { + var changesCount = await _context.SaveChangesAsync(); + await transaction.CommitAsync(); + + _logger.LogInfo( + "Successfully created Contact {ContactId} with {Count} related records for Tenant {TenantId} by Employee {EmployeeId}. Transaction committed.", + contact.Id, changesCount - 1, tenantId, loggedInEmployeeId); + } + catch (DbUpdateException dbEx) + { + await transaction.RollbackAsync(); + _logger.LogError(dbEx, "Database exception during contact creation for Tenant {TenantId}. Transaction rolled back.", tenantId); + return ApiResponse.ErrorResponse("An internal database error occurred.", ExceptionMapper(dbEx), 500); + } + + // --- Construct and Return Response --- + var contactVM = _mapper.Map(contact); + contactVM.ContactPhones = phoneVMs; + contactVM.ContactEmails = emailVMs; + contactVM.Tags = tagVMs; + contactVM.BucketIds = contactBucketMappings.Select(cb => cb.BucketId).ToList(); + contactVM.ProjectIds = projectMappings.Select(cp => cp.ProjectId).ToList(); + + return ApiResponse.SuccessResponse(contactVM, "Contact created successfully.", 201); + } + catch (Exception ex) + { + _logger.LogError(ex, "An unexpected exception occurred during contact creation for Tenant {TenantId}. Transaction rolled back.", tenantId); + return ApiResponse.ErrorResponse("An unexpected internal error occurred.", ExceptionMapper(ex), 500); + } + } + + #endregion + + #region =================================================================== Contact Put APIs =================================================================== + + public async Task> UpdateContactAsync(Guid id, UpdateContactDto updateContact, Guid tenantId, Employee loggedInEmployee) + { + if (updateContact == null) + { + _logger.LogWarning("Employee {EmployeeId} sent empty payload for updating contact.", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Empty payload", "User sent empty payload", 400); + } + + // Ensure payload ID matches path ID + if (updateContact.Id != id) + { + _logger.LogWarning("Employee {EmployeeId} sent mismatched contact IDs. Path: {PathId}, Payload: {PayloadId}", + loggedInEmployee.Id, id, updateContact.Id); + return ApiResponse.ErrorResponse("Invalid data", "Payload ID does not match path parameter", 400); + } + + using var context = _dbContextFactory.CreateDbContext(); + + // Retrieve the contact with tracking disabled for initial existence and permission check + var contact = await context.Contacts + .AsNoTracking() + .FirstOrDefaultAsync(c => c.Id == id && c.IsActive && c.TenantId == tenantId); + + if (contact == null) + { + _logger.LogWarning("Employee {EmployeeId} attempted to update non-existing contact {ContactId}", + loggedInEmployee.Id, id); + return ApiResponse.ErrorResponse("Contact not found", "Contact not found", 404); + } + + // Validate permissions + var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id); + + // Determine accessible bucket IDs for this employee + List bucketIds; + if (hasAdminPermission) + { + bucketIds = await context.Buckets + .Where(b => b.TenantId == tenantId) + .Select(b => b.Id) + .ToListAsync(); + } + else if (hasManagerPermission || hasUserPermission) + { + var employeeBucketIds = await context.EmployeeBucketMappings + .Where(eb => eb.EmployeeId == loggedInEmployee.Id) + .Select(eb => eb.BucketId) + .ToListAsync(); + + var createdBucketIds = await context.Buckets + .Where(b => b.CreatedByID == loggedInEmployee.Id) + .Select(b => b.Id) + .ToListAsync(); + + bucketIds = employeeBucketIds.Concat(createdBucketIds).Distinct().ToList(); + } + else + { + _logger.LogWarning("Employee {EmployeeId} does not have permission to update contact {ContactId}", + loggedInEmployee.Id, id); + return ApiResponse.ErrorResponse("Unauthorized", "You do not have permission", 403); + } + + // Fetch contact bucket mappings accessible by the user + var contactBuckets = await context.ContactBucketMappings + .Where(cb => cb.ContactId == contact.Id && bucketIds.Contains(cb.BucketId)) + .ToListAsync(); + + // Refresh bucket IDs to only those relevant to this contact & permissions + var accessibleBucketIds = contactBuckets.Select(cb => cb.BucketId).Distinct().ToHashSet(); + + // Update the main contact object from DTO + var updatedContact = _mapper.Map(updateContact); + updatedContact.TenantId = tenantId; + updatedContact.CreatedAt = contact.CreatedAt; + updatedContact.CreatedById = contact.CreatedById; + updatedContact.UpdatedById = loggedInEmployee.Id; + updatedContact.UpdatedAt = DateTime.UtcNow; + + // Attach updated contact (tracked entity) + context.Contacts.Update(updatedContact); + + // Prepare parallel tasks for retrieving related collections in independent DbContext instances + var phonesTask = Task.Run(async () => + { + using var ctx = _dbContextFactory.CreateDbContext(); + return await ctx.ContactsPhones.AsNoTracking().Where(p => p.ContactId == contact.Id).ToListAsync(); + }); + + var emailsTask = Task.Run(async () => + { + using var ctx = _dbContextFactory.CreateDbContext(); + return await ctx.ContactsEmails.AsNoTracking().Where(e => e.ContactId == contact.Id).ToListAsync(); + }); + + var tagsTask = Task.Run(async () => + { + using var ctx = _dbContextFactory.CreateDbContext(); + return await ctx.ContactTagMappings.AsNoTracking().Where(t => t.ContactId == contact.Id).ToListAsync(); + }); + + var projectsTask = Task.Run(async () => + { + using var ctx = _dbContextFactory.CreateDbContext(); + return await ctx.ContactProjectMappings.AsNoTracking().Where(p => p.ContactId == contact.Id).ToListAsync(); + }); + + // Await all tasks to complete + await Task.WhenAll(phonesTask, emailsTask, tagsTask, projectsTask); + + var phones = phonesTask.Result; + var emails = emailsTask.Result; + var contactTags = tagsTask.Result; + var contactProjects = projectsTask.Result; + + var phoneIds = phones.Select(p => p.Id).ToHashSet(); + var emailIds = emails.Select(e => e.Id).ToHashSet(); + var tagIds = contactTags.Select(t => t.ContactTagId).Distinct().ToHashSet(); + var projectIds = contactProjects.Select(p => p.ProjectId).Distinct().ToHashSet(); + + // Fetch all tags for this tenant for name checks + 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 phoneUpdateLogs = new List(); + List emailUpdateLogs = new List(); + + // ---------------------- Update Phones ----------------------- + if (updateContact.ContactPhones != null) + { + var updatedPhoneIds = updateContact.ContactPhones.Select(p => p.Id).Where(id => id != null && id != Guid.Empty).ToHashSet(); + + foreach (var phoneDto in updateContact.ContactPhones) + { + var phoneEntity = _mapper.Map(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 + foreach (var phone in phones) + { + 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); + } + } + } + else if (phones.Any()) + { + context.ContactsPhones.RemoveRange(phones); + } + + // ---------------------- Update Emails ----------------------- + if (updateContact.ContactEmails != null) + { + var updatedEmailIds = updateContact.ContactEmails.Select(e => e.Id).Where(id => id != null && id != Guid.Empty).ToHashSet(); + + foreach (var emailDto in updateContact.ContactEmails) + { + var emailEntity = _mapper.Map(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 + foreach (var email in emails) + { + 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); + } + } + } + else if (emails.Any()) + { + context.ContactsEmails.RemoveRange(emails); + } + + // ---------------------- Update Buckets ----------------------- + if (updateContact.BucketIds != null) + { + var incomingBucketIds = updateContact.BucketIds.ToHashSet(); + + // Add newly added bucket mappings if accessible by user + foreach (var bucketId in incomingBucketIds) + { + if (!accessibleBucketIds.Contains(bucketId) && bucketIds.Contains(bucketId)) + { + context.ContactBucketMappings.Add(new ContactBucketMapping + { + BucketId = bucketId, + ContactId = contact.Id + }); + } + } + + // Remove bucket mappings removed in payload + foreach (var contactBucket in contactBuckets) + { + if (!incomingBucketIds.Contains(contactBucket.BucketId)) + { + + context.ContactBucketMappings.Remove(contactBucket); + } + } + } + else if (contactBuckets.Any()) + { + context.ContactBucketMappings.RemoveRange(contactBuckets); + } + + // ---------------------- Update Projects ----------------------- + if (updateContact.ProjectIds != null) + { + var incomingProjectIds = updateContact.ProjectIds.ToHashSet(); + + foreach (var projectId in incomingProjectIds) + { + if (!projectIds.Contains(projectId)) + { + context.ContactProjectMappings.Add(new ContactProjectMapping + { + ProjectId = projectId, + ContactId = contact.Id, + TenantId = tenantId + }); + } + } + + foreach (var projectMapping in contactProjects) + { + if (!incomingProjectIds.Contains(projectMapping.ProjectId)) + { + context.ContactProjectMappings.Remove(projectMapping); + } + } + } + else if (contactProjects.Any()) + { + context.ContactProjectMappings.RemoveRange(contactProjects); + } + + // ---------------------- Update Tags ----------------------- + if (updateContact.Tags != null) + { + var updatedTagIds = updateContact.Tags.Select(t => t.Id).Where(id => id != null && id != Guid.Empty).ToHashSet(); + + foreach (var tagDto in updateContact.Tags) + { + var lowerName = tagDto.Name.Trim().ToLowerInvariant(); + + bool existsByName = !string.IsNullOrWhiteSpace(lowerName) && tagNameLookup.ContainsKey(lowerName); + bool idNotExistsInMapping = !updatedTagIds.Contains(tagDto.Id ?? Guid.Empty); + + if (existsByName && idNotExistsInMapping) + { + // Use existing tag by name + var existingTag = tagNameLookup[lowerName]; + context.ContactTagMappings.Add(new ContactTagMapping + { + ContactId = contact.Id, + ContactTagId = tagDto.Id ?? existingTag.Id + }); + } + else if (tagDto.Id == null || tagDto.Id == Guid.Empty) + { + // Create new tag master and mapping + var newTagMaster = new ContactTagMaster + { + Name = tagDto.Name, + Description = tagDto.Name, + TenantId = tenantId + }; + context.ContactTagMasters.Add(newTagMaster); + + // Mapping will use newTagMaster.Id once saved (EF will fix after SaveChanges) + context.ContactTagMappings.Add(new ContactTagMapping + { + ContactId = contact.Id, + ContactTagId = newTagMaster.Id + }); + } + } + + // Remove tag mappings no longer present + foreach (var contactTagMapping in contactTags) + { + if (!updatedTagIds.Contains(contactTagMapping.ContactTagId)) + { + context.ContactTagMappings.Remove(contactTagMapping); + } + } + } + else if (contactTags.Any()) + { + context.ContactTagMappings.RemoveRange(contactTags); + } + + // ---------------------- Add Update Log ----------------------- + context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog + { + RefereanceId = contact.Id, + UpdatedById = loggedInEmployee.Id, + UpdateAt = DateTime.UtcNow + }); + + // Save all changes once here + try + { + await context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException ex) + { + _logger.LogError(ex, "Concurrency conflict while employee {EmployeeId} was updating contact {ContactId}", loggedInEmployee.Id, contact.Id); + return ApiResponse.ErrorResponse("Concurrency conflict", "This contact was updated by another user. Please reload and try again.", 409); + } + + // Reload updated contact and related data for response, using a fresh context + var responseTask = Task.Run(async () => + { + 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 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); + + return ApiResponse.SuccessResponse(contactVM, "Contact Updated Successfully", 200); + } + + #endregion + + #region =================================================================== Contact Delete APIs =================================================================== + + public async Task> DeleteContactAsync(Guid id, bool active, Guid tenantId, Employee loggedInEmployee) + { + // Validate the contact id + if (id == Guid.Empty) + { + _logger.LogWarning("Employee ID {EmployeeId} attempted to delete with an empty contact ID.", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Contact ID is empty", "Contact ID is empty", 400); + } + + // Try to find the contact for the given tenant + Contact? contact = await _context.Contacts.FirstOrDefaultAsync(c => c.Id == id && c.TenantId == tenantId); + + if (contact == null) + { + _logger.LogWarning("Employee ID {EmployeeId} attempted to delete contact ID {ContactId}, but it was not found.", loggedInEmployee.Id, id); + return ApiResponse.ErrorResponse("Contact not found", "Contact not found", 404); + } + + var contactObject = _updateLogsHelper.EntityToBsonDocument(contact); + + // Update the contact's active status (soft delete or activate) + contact.IsActive = active; + + // Log the update in the directory update logs + _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog + { + RefereanceId = contact.Id, + UpdatedById = loggedInEmployee.Id, + UpdateAt = DateTime.UtcNow + }); + // Save changes to the database + await _context.SaveChangesAsync(); + + await _updateLogsHelper.PushToUpdateLogsAsync(new UpdateLogsObject + { + EntityId = contact.Id.ToString(), + UpdatedById = loggedInEmployee.Id.ToString(), + OldObject = contactObject, + UpdatedAt = DateTime.UtcNow + }, contactCollection); + + _logger.LogInfo("Contact ID {ContactId} has been {(DeletedOrActivated)} by Employee ID {EmployeeId}.", id, active ? "activated" : "deleted", loggedInEmployee.Id); + + return ApiResponse.SuccessResponse(new { }, active ? "Contact is activated successfully" : "Contact is deleted successfully", 200); + } + + + #endregion + + #endregion + + #region =================================================================== Contact Notes APIs =================================================================== + + public async Task> GetListOFAllNotesAsync(Guid? projectId, string? searchString, string? filter, int pageSize, int pageNumber, Guid tenantId, Employee loggedInEmployee) + { + _logger.LogInfo("Initiating GetListOFAllNotesAsync. TenantId: {TenantId}, ProjectId: {ProjectId}, PageSize: {PageSize}, PageNumber: {PageNumber}, EmployeeId: {EmployeeId}", + tenantId, projectId ?? Guid.Empty, pageSize, pageNumber, loggedInEmployee.Id); + + // Ensure user context is present + if (loggedInEmployee.Id == Guid.Empty || tenantId == Guid.Empty) + { + _logger.LogWarning("Unauthorized: LoggedInEmployee is null."); + return ApiResponse.ErrorResponse("Unauthorized", "Employee not found.", 403); + } + + try + { + // Use a context instance per method call for safety in parallel scenarios + await using var context = _dbContextFactory.CreateDbContext(); + + // Permission checks (parallel as they're independent) + var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id); + + // Access control + if (!hasAdminPermission && !hasManagerPermission && !hasUserPermission) + { + _logger.LogWarning("Access Denied. EmployeeId: {EmployeeId}, TenantId: {TenantId}", loggedInEmployee.Id, tenantId); + return ApiResponse.ErrorResponse("Access Denied", "You don't have access to view notes.", 403); + } + + // Build base query + IQueryable notesQuery = context.ContactNotes + .Include(cn => cn.UpdatedBy) + .ThenInclude(e => e!.JobRole) + .Include(cn => cn.Createdby) + .ThenInclude(e => e!.JobRole) + .Include(cn => cn.Contact) + .Where(cn => cn.TenantId == tenantId); + + // Fetch associated contact IDs for project (if filtering by project) + List? projectContactIds = null; + if (projectId.HasValue) + { + projectContactIds = await context.ContactProjectMappings + .Where(pc => pc.ProjectId == projectId.Value) + .Select(pc => pc.ContactId) + .ToListAsync(); + } + + if (!hasAdminPermission) // Manager/User filtering + { + _logger.LogInfo("Non-admin user. Applying bucket-based filtering. EmployeeId: {EmployeeId}", loggedInEmployee.Id); + // Get assigned bucket IDs + var assignedBucketIds = await context.EmployeeBucketMappings + .Where(eb => eb.EmployeeId == loggedInEmployee.Id) + .Select(eb => eb.BucketId) + .ToListAsync(); + + if (!assignedBucketIds.Any()) + { + _logger.LogInfo("No assigned buckets for user: {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.SuccessResponse(new + { + CurrentPage = pageNumber, + TotalPages = 0, + Data = new List() + }, "No notes found based on assigned buckets.", 200); + } + + // Contacts based on assigned buckets, further filtered by project (if provided) + var contactBucketQuery = context.ContactBucketMappings + .Where(cb => assignedBucketIds.Contains(cb.BucketId)); + + if (projectContactIds != null) + { + contactBucketQuery = contactBucketQuery.Where(cb => projectContactIds.Contains(cb.ContactId)); + } + + var contactIds = await contactBucketQuery.Select(cb => cb.ContactId).Distinct().ToListAsync(); + + if (!contactIds.Any()) + { + _logger.LogInfo("No contacts found for assigned buckets for user: {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.SuccessResponse(new + { + CurrentPage = pageNumber, + TotalPages = 0, + Data = new List() + }, "No notes found for associated contacts.", 200); + } + + notesQuery = notesQuery.Where(cn => contactIds.Contains(cn.ContactId)); + } + else + { + // Admin: If project specified, filter notes further + if (projectContactIds != null) + notesQuery = notesQuery.Where(cn => projectContactIds.Contains(cn.ContactId)); + } + + // --- Advanced Filtering from 'filter' parameter --- + ContactNoteFilter? contactNoteFilter = TryDeserializeContactNoteFilter(filter); + if (contactNoteFilter != null) + { + if (contactNoteFilter.CreatedByIds?.Any() ?? false) + { + notesQuery = notesQuery.Where(cn => contactNoteFilter.CreatedByIds.Contains(cn.CreatedById)); + } + if (contactNoteFilter.Organizations?.Any() ?? false) + { + notesQuery = notesQuery.Where(cn => cn.Contact != null && contactNoteFilter.Organizations.Contains(cn.Contact.Organization)); + } + } + + // --- Search Term Filtering --- + if (!string.IsNullOrWhiteSpace(searchString)) + { + var searchTermLower = searchString.ToLower(); + notesQuery = notesQuery.Where(c => + (c.Contact != null && c.Contact.Name.ToLower().Contains(searchTermLower)) || + (c.Contact != null && c.Contact.Organization != null && c.Contact.Organization.ToLower().Contains(searchTermLower)) || + c.Note.ToLower().Contains(searchTermLower) + ); + } + + // Pagination safeguard + pageSize = pageSize < 1 ? 25 : pageSize; + pageNumber = pageNumber < 1 ? 1 : pageNumber; + + // Accurate pagination metadata + int totalRecords = await notesQuery.CountAsync(); + int totalPages = (int)Math.Ceiling(totalRecords / (double)pageSize); + + // Fetch paginated, ordered results + List notes = await notesQuery + .OrderByDescending(cn => cn.UpdatedAt ?? cn.CreatedAt) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); + + _logger.LogInfo("Notes fetched: {Count}, Page: {PageNumber}/{TotalPages}, EmployeeId: {EmployeeId}, TenantId: {TenantId}", + notes.Count, pageNumber, totalPages, loggedInEmployee.Id, tenantId); + + // In-memory mapping to ViewModel + var noteVms = _mapper.Map>(notes); + + var response = new + { + CurrentPage = pageNumber, + PageSize = pageSize, + TotalPages = totalPages, + TotalRecords = totalRecords, + Data = noteVms + }; + + _logger.LogInfo("Notes mapped to ViewModel for TenantId: {TenantId}, EmployeeId: {EmployeeId}", tenantId, loggedInEmployee.Id); + return ApiResponse.SuccessResponse(response, $"{noteVms.Count} notes fetched successfully.", 200); + + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception occurred in GetListOFAllNotesAsync. TenantId: {TenantId}, EmployeeId: {EmployeeId}", tenantId, loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Internal Server Error", "An error occurred while fetching notes. Please try again later.", 500); + } + } + + /// + /// Fetches all notes associated with a given contact, subject to permission checks and contact-bucket mappings. + /// + /// The contact ID. + /// The tenant ID of the current user. + /// Whether to filter for active notes only. + /// The currently logged in employee object. + /// Returns a list of contact notes wrapped in ApiResponse. + public async Task> GetNoteListByContactIdAsync(Guid id, bool active, Guid tenantId, Employee loggedInEmployee) + { + // Step 1: Permission Validation + var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id); + if (!hasAdminPermission && !hasManagerPermission && !hasUserPermission) + { + _logger.LogWarning( + "Access Denied. EmployeeId: {EmployeeId}, TenantId: {TenantId}. No permissions granted.", + loggedInEmployee.Id, tenantId); + + return ApiResponse.ErrorResponse( + "Access Denied", + "You don't have access to view notes.", + StatusCodes.Status403Forbidden); + } + + // Step 2: Validate Contact Exists + Contact? contact = await _context.Contacts + .AsNoTracking() // optimization: no tracking needed + .FirstOrDefaultAsync(c => c.Id == id && c.IsActive && c.TenantId == tenantId); + + if (contact == null) + { + _logger.LogWarning( + "Employee {EmployeeId} attempted to fetch notes for Contact {ContactId}, but the contact was not found. TenantId: {TenantId}", + loggedInEmployee.Id, id, tenantId); + + return ApiResponse.ErrorResponse( + "Contact not found", + "Contact not found", + StatusCodes.Status404NotFound); + } + + // Step 3: Bucket-level Security Checks (Non-admin users) + if (!hasAdminPermission) + { + var employeeBucketIds = await _context.EmployeeBucketMappings + .AsNoTracking() + .Where(em => em.EmployeeId == loggedInEmployee.Id) + .Select(em => em.BucketId) + .ToListAsync(); + + bool hasContactAccess = await _context.ContactBucketMappings + .AsNoTracking() + .AnyAsync(cb => employeeBucketIds.Contains(cb.BucketId) && cb.ContactId == contact.Id); + + if (!hasContactAccess) + { + _logger.LogWarning( + "Access Denied. EmployeeId: {EmployeeId}, TenantId: {TenantId}. No bucket access for ContactId {ContactId}", + loggedInEmployee.Id, tenantId, contact.Id); + + return ApiResponse.ErrorResponse( + "Access Denied", + "You don't have access to view notes.", + StatusCodes.Status403Forbidden); + } + } + + // Step 4: Fetch Notes + var notesQuery = _context.ContactNotes + .Include(n => n.Createdby) // Eager load creator + .ThenInclude(e => e!.JobRole) + .Include(n => n.UpdatedBy) // Eager load updater + .ThenInclude(e => e!.JobRole) + .Where(n => n.ContactId == contact.Id && n.TenantId == tenantId); + + if (active) + notesQuery = notesQuery.Where(n => n.IsActive); + + List notes = await notesQuery + .AsNoTracking() // reduce EF overhead + .ToListAsync(); + + // Step 5: Fetch Update Logs in one DB call + var noteIds = notes.Select(n => n.Id).ToList(); + List updateLogs = new(); + + if (noteIds.Count > 0) // only fetch logs if needed + { + updateLogs = await _context.DirectoryUpdateLogs + .Include(l => l.Employee) + .ThenInclude(e => e!.JobRole) + .AsNoTracking() + .Where(l => noteIds.Contains(l.RefereanceId)) + .ToListAsync(); + } + + // Step 6: Map Entities to ViewModels + List noteVMs = _mapper.Map>(notes); + + // Step 7: Final Log + Response + _logger.LogInfo( + "Employee {EmployeeId} successfully fetched {Count} notes for Contact {ContactId} in Tenant {TenantId}", + loggedInEmployee.Id, noteVMs.Count, id, tenantId); + + return ApiResponse.SuccessResponse( + noteVMs, + $"{noteVMs.Count} contact-notes record(s) fetched successfully", + StatusCodes.Status200OK); + } + + /// + /// Fetches filter objects (CreatedBy employees and Organizations) for Contact Notes + /// accessible by the logged-in employee, based on permissions. + /// + /// The tenant ID. + /// The employee requesting filters. + /// ApiResponse containing CreatedBy and Organizations filter options. + public async Task> GetContactNotesFilterObjectAsync(Guid tenantId, Employee loggedInEmployee) + { + try + { + _logger.LogInfo("Started fetching Contact Notes filters for TenantId: {TenantId}, EmployeeId: {EmployeeId}", + tenantId, loggedInEmployee.Id); + + // Step 1: Fetch accessible bucket IDs based on permissions + var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id); + List? accessibleBucketIds = null; + + if (hasAdminPermission) + { + // Admin → Access to all buckets in the tenant + accessibleBucketIds = await _context.Buckets + .Where(b => b.TenantId == tenantId) + .Select(b => b.Id) + .ToListAsync(); + + _logger.LogDebug("Admin access granted. Found {Count} buckets.", accessibleBucketIds.Count); + } + else if (hasManagerPermission || hasUserPermission) + { + // Manager/User → Access to mapped buckets only + accessibleBucketIds = await _context.EmployeeBucketMappings + .Where(eb => eb.EmployeeId == loggedInEmployee.Id) + .Select(eb => eb.BucketId) + .ToListAsync(); + + _logger.LogDebug("Manager/User access granted. Found {Count} mapped buckets for EmployeeId: {EmployeeId}.", + accessibleBucketIds.Count, loggedInEmployee.Id); + } + else + { + _logger.LogWarning("No permissions found for EmployeeId: {EmployeeId}. Returning empty filters.", loggedInEmployee.Id); + } + + // Step 2: Fetch Contact IDs from ContactBucketMappings + var contactIds = await _context.ContactBucketMappings + .Where(cb => accessibleBucketIds != null && accessibleBucketIds.Contains(cb.BucketId)) + .Select(cb => cb.ContactId) + .Distinct() // ensures no duplicate contact IDs + .ToListAsync(); + + _logger.LogInfo("Fetched {Count} contact Ids from accessible buckets.", contactIds.Count); + + // Step 3: Fetch Contacts related to Contact Notes for those contactIds + var contacts = await _context.ContactNotes + .AsNoTracking() // no need to track since it’s for read-only filters + .Include(cn => cn.Contact) + .ThenInclude(c => c!.CreatedBy) + .Where(cn => + contactIds.Contains(cn.ContactId) && + cn.Contact != null && + cn.Contact.CreatedBy != null && + cn.TenantId == tenantId) + .Select(cn => cn.Contact!) + .Distinct() // avoid duplicate contacts from multiple notes + .ToListAsync(); + + _logger.LogInfo("Fetched {Count} unique contacts with notes.", contacts.Count); + + // Step 4: Build organization filters + var organizations = contacts + .Where(c => !string.IsNullOrEmpty(c.Organization)) // filter out null/empty orgs + .Select(c => new + { + Id = c.Organization, // Using organization string as unique identifier + Name = c.Organization + }) + .Distinct() + .OrderBy(o => o.Name) + .ToList(); + + _logger.LogInfo("Extracted {Count} unique organizations from contacts.", organizations.Count); + + // Step 5: Build CreatedBy filters (employees who created the contacts) + var createdBy = contacts + .Select(c => new + { + Id = c.CreatedBy!.Id, + Name = $"{c.CreatedBy.FirstName} {c.CreatedBy.LastName}".Trim() + }) + .Distinct() + .OrderBy(e => e.Name) + .ToList(); + + _logger.LogInfo("Extracted {Count} unique CreatedBy employees from contacts.", createdBy.Count); + + // Step 6: Build response + var response = new + { + CreatedBy = createdBy, + Organizations = organizations + }; + + _logger.LogInfo("Successfully returning Contact Notes filters for TenantId: {TenantId}, EmployeeId: {EmployeeId}", + tenantId, loggedInEmployee.Id); + + return ApiResponse.SuccessResponse(response, "Filters for contact notes fetched successfully", 200); + } + catch (Exception ex) + { + _logger.LogError(ex, + "Error occurred while fetching Contact Notes filters for TenantId: {TenantId}, EmployeeId: {EmployeeId}", + tenantId, loggedInEmployee.Id); + + return ApiResponse.ErrorResponse("An error occurred while fetching filters", 500); + } + } + + + /// + /// Creates a note for a given contact under the specified tenant. + /// Ensures that the contact exists and belongs to the tenant before adding the note. + /// + /// The DTO containing the note details. + /// The tenant identifier to which the contact belongs. + /// The logged-in employee attempting the action. + /// ApiResponse containing the created note details or error information. + public async Task> CreateContactNoteAsync(CreateContactNoteDto noteDto, Guid tenantId, Employee loggedInEmployee) + { + // Validate request payload + if (noteDto == null) + { + _logger.LogWarning( + "Employee {EmployeeId} attempted to create a note with an empty payload.", + loggedInEmployee.Id); + + return ApiResponse.ErrorResponse("Empty payload.", "Request body cannot be null.", 400); + } + + try + { + // Check if the contact exists and is active for this tenant + Contact? contact = await _context.Contacts + .AsNoTracking() // optimization for read-only query + .FirstOrDefaultAsync(c => c.Id == noteDto.ContactId && c.IsActive && c.TenantId == tenantId); + + if (contact == null) + { + _logger.LogWarning( + "Employee {EmployeeId} attempted to add a note to Contact {ContactId}, but it was not found for tenant {TenantId}.", + loggedInEmployee.Id, noteDto.ContactId, tenantId); + + return ApiResponse.ErrorResponse("Contact not found.", "The specified contact does not exist.", 404); + } + + // Map DTO -> Entity using AutoMapper + ContactNote note = _mapper.Map(noteDto); + note.CreatedById = loggedInEmployee.Id; + note.TenantId = tenantId; + + // Save new note + await _context.ContactNotes.AddAsync(note); + await _context.SaveChangesAsync(); + + // Map Entity -> ViewModel + ContactNoteVM noteVM = _mapper.Map(note); + + _logger.LogInfo( + "Employee {EmployeeId} successfully added a note (NoteId: {NoteId}) to Contact {ContactId} for Tenant {TenantId}.", + loggedInEmployee.Id, note.Id, contact.Id, tenantId); + + return ApiResponse.SuccessResponse(noteVM, "Note added successfully.", 200); + } + catch (Exception ex) + { + // Log unexpected errors to troubleshoot + _logger.LogError( + ex, + "Unexpected error occurred while Employee {EmployeeId} attempted to add a note for Contact {ContactId} in Tenant {TenantId}.", + loggedInEmployee.Id, noteDto.ContactId, tenantId); + + return ApiResponse.ErrorResponse("An unexpected error occurred.", ex.Message, 500); + } + } + + /// + /// Updates an existing contact note and logs changes + /// both in relational DB (SQL) and update logs (possibly MongoDB). + /// + /// The note ID that needs to be updated. + /// DTO with updated note data. + /// Standardized ApiResponse with updated note or error details. + public async Task> UpdateContactNoteAsync(Guid id, UpdateContactNoteDto noteDto, Guid tenantId, Employee loggedInEmployee) + { + // Validation: check null payload or mismatched ID + if (noteDto == null || id != noteDto.Id) + { + _logger.LogWarning("Employee {EmployeeId} sent invalid or null payload. RouteId: {RouteId}, PayloadId: {PayloadId}", + loggedInEmployee.Id, id, noteDto?.Id ?? Guid.Empty); + + return ApiResponse.ErrorResponse("Invalid or empty payload", "Invalid or empty payload", 400); + } + + // Check if the contact belongs to this tenant + Contact? contact = await _context.Contacts + .AsNoTracking() + .FirstOrDefaultAsync(c => c.Id == noteDto.ContactId && c.TenantId == tenantId); + + if (contact == null) + { + _logger.LogWarning("Employee {EmployeeId} attempted to update note {NoteId} for Contact {ContactId}, but the contact was not found in Tenant {TenantId}.", + loggedInEmployee.Id, noteDto.Id, noteDto.ContactId, tenantId); + + return ApiResponse.ErrorResponse("Contact not found", "Contact not found", 404); + } + + // Fetch the contact note to be updated + ContactNote? contactNote = await _context.ContactNotes + .Include(cn => cn.Createdby) + .ThenInclude(e => e!.JobRole) + .Include(cn => cn.Contact) + .FirstOrDefaultAsync(n => + n.Id == noteDto.Id && + n.ContactId == contact.Id && + n.IsActive); + + if (contactNote == null) + { + _logger.LogWarning("Employee {EmployeeId} attempted to update Note {NoteId} for Contact {ContactId}, but the note was not found or inactive.", + loggedInEmployee.Id, noteDto.Id, noteDto.ContactId); + + return ApiResponse.ErrorResponse("Note not found", "Note not found", 404); + } + + // Capture old state for change-log before updating + var oldObject = _updateLogsHelper.EntityToBsonDocument(contactNote); + + // Apply updates + contactNote.Note = noteDto.Note; + contactNote.UpdatedById = loggedInEmployee.Id; + contactNote.UpdatedAt = DateTime.UtcNow; + + // Save change log into relational logs + _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog + { + RefereanceId = id, + UpdatedById = loggedInEmployee.Id, + UpdateAt = DateTime.UtcNow + }); + + // Wrap in try-catch for robustness + try + { + await _context.SaveChangesAsync(); + + // Map to ViewModel for output + ContactNoteVM noteVM = _mapper.Map(contactNote); + noteVM.UpdatedAt = contactNote.UpdatedAt; + noteVM.UpdatedBy = _mapper.Map(loggedInEmployee); + + // Push audit log asynchronously (Mongo / NoSQL Logs) + await _updateLogsHelper.PushToUpdateLogsAsync(new UpdateLogsObject + { + EntityId = contactNote.Id.ToString(), + UpdatedById = loggedInEmployee.Id.ToString(), + OldObject = oldObject, + UpdatedAt = DateTime.UtcNow + }, contactNoteCollection); + + // Success log + _logger.LogInfo("Employee {EmployeeId} successfully updated Note {NoteId} for Contact {ContactId} at {UpdatedAt}", + loggedInEmployee.Id, noteVM.Id, contact.Id, noteVM.UpdatedAt); + + return ApiResponse.SuccessResponse(noteVM, "Note updated successfully", 200); + } + catch (DbUpdateException ex) + { + _logger.LogError(ex, "Database Exception occurred while updating Note {NoteId} for Contact {ContactId} by Employee {EmployeeId}", + noteDto.Id, noteDto.ContactId, loggedInEmployee.Id); + + return ApiResponse.ErrorResponse("Failed to update note", "An unexpected error occurred while saving note.", 500); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception occurred while updating Note {NoteId} for Contact {ContactId} by Employee {EmployeeId}", + noteDto.Id, noteDto.ContactId, loggedInEmployee.Id); + + return ApiResponse.ErrorResponse("Failed to update note", "An unexpected error occurred while saving note.", 500); + } + } + + /// + /// Soft deletes (or restores) a contact note by updating its active status. + /// Also pushes an update log entry in SQL and Mongo (audit trail). + /// + /// ID of the contact note to delete/restore. + /// Flag to set note as active or inactive. + /// Tenant identifier of the logged-in user. + /// The employee performing this action. + /// ApiResponse with success or error details. + public async Task> DeleteContactNoteAsync(Guid id, bool active, Guid tenantId, Employee loggedInEmployee) + { + // Lookup note within the tenant + ContactNote? note = await _context.ContactNotes + .FirstOrDefaultAsync(n => n.Id == id && n.TenantId == tenantId); + + if (note == null) + { + // Log missing resource + _logger.LogWarning("Employee {EmployeeId} attempted to delete Note {NoteId}, but it was not found in Tenant {TenantId}", + loggedInEmployee.Id, id, tenantId); + + return ApiResponse.ErrorResponse("Note not found", "Note not found", 404); + } + + // Capture old state for audit logging + var oldObject = _updateLogsHelper.EntityToBsonDocument(note); + + // Update note metadata + var currentTime = DateTime.UtcNow; + note.IsActive = active; // soft delete (false) or restore (true) + note.UpdatedById = loggedInEmployee.Id; + note.UpdatedAt = currentTime; + + // Add relational update log entry + _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog + { + RefereanceId = id, + UpdatedById = loggedInEmployee.Id, + UpdateAt = currentTime + }); + + try + { + // Save SQL changes + await _context.SaveChangesAsync(); + + // Push audit log (Mongo / NoSQL) + await _updateLogsHelper.PushToUpdateLogsAsync(new UpdateLogsObject + { + EntityId = note.Id.ToString(), + UpdatedById = loggedInEmployee.Id.ToString(), + OldObject = oldObject, + UpdatedAt = currentTime + }, contactNoteCollection); + + // Log success — distinguish delete vs restore + if (!active) + { + _logger.LogInfo("Employee {EmployeeId} soft deleted Note {NoteId} at {Timestamp}", + loggedInEmployee.Id, id, currentTime); + } + else + { + _logger.LogInfo("Employee {EmployeeId} restored Note {NoteId} at {Timestamp}", + loggedInEmployee.Id, id, currentTime); + } + + return ApiResponse.SuccessResponse(new { }, + active ? "Note restored successfully" : "Note deleted successfully", + 200); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while updating Note {NoteId} (delete/restore) in Tenant {TenantId} by Employee {EmployeeId}", + id, tenantId, loggedInEmployee.Id); + + return ApiResponse.ErrorResponse("Failed to delete note", + "An unexpected error occurred while deleting/restoring the note.", + 500); + } + } + + #endregion + + #region =================================================================== Bucket APIs =================================================================== + public async Task> GetBucketListAsync(Guid tenantId, Employee loggedInEmployee) + { + _logger.LogInfo("Started fetching bucket list for Employee {EmployeeId} in Tenant {TenantId}", loggedInEmployee.Id, tenantId); + + // Check permissions early + var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id); + + if (!hasAdminPermission && !hasManagerPermission && !hasUserPermission) + { + _logger.LogWarning("Employee {EmployeeId} attempted to access buckets without permission", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("You don't have permission", "You don't have permission", 403); + } + + List bucketList; + List bucketIds; + + if (hasAdminPermission) + { + // Admin gets all buckets for the tenant + bucketList = await _context.Buckets + .Include(b => b.CreatedBy) + .ThenInclude(e => e!.JobRole) + .Where(b => b.TenantId == tenantId) + .ToListAsync(); + + bucketIds = bucketList.Select(b => b.Id).ToList(); + } + else + { + // Manager or user: fetch employee bucket mappings and buckets accordingly + + var employeeBuckets = await _context.EmployeeBucketMappings + .Where(b => b.EmployeeId == loggedInEmployee.Id) + .ToListAsync(); + + bucketIds = employeeBuckets.Select(b => b.BucketId).ToList(); + + bucketList = await _context.Buckets + .Include(b => b.CreatedBy) + .ThenInclude(e => e!.JobRole) + .Where(b => bucketIds.Contains(b.Id) || b.CreatedByID == loggedInEmployee.Id) + .ToListAsync(); + + bucketIds = bucketList.Select(b => b.Id).ToList(); + } + + if (!bucketList.Any()) + { + _logger.LogInfo("No buckets found for Employee {EmployeeId}", loggedInEmployee.Id); + return ApiResponse.SuccessResponse(new List(), "No buckets found", 200); + } + + // Fetch related data in parallel using Task.Run with separate contexts to avoid concurrency issues + var employeeBucketMappingsTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + return await context.EmployeeBucketMappings + .Where(b => bucketIds.Contains(b.BucketId)) + .ToListAsync(); + }); + + var contactBucketMappingsTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + return await context.ContactBucketMappings + .Where(cb => bucketIds.Contains(cb.BucketId)) + .ToListAsync(); + }); + + await Task.WhenAll(employeeBucketMappingsTask, contactBucketMappingsTask); + + var employeeBucketMappings = employeeBucketMappingsTask.Result; + var contactBucketMappings = contactBucketMappingsTask.Result; + + var bucketVMs = new List(); + + // Prepare view models for each bucket + foreach (var bucket in bucketList) + { + var mappedEmployees = employeeBucketMappings + .Where(eb => eb.BucketId == bucket.Id) + .Select(eb => eb.EmployeeId) + .ToList(); + + if (bucket.CreatedBy != null) + { + mappedEmployees.Add(bucket.CreatedBy.Id); + } + + var contactCount = contactBucketMappings.Count(cb => cb.BucketId == bucket.Id); + + var bucketVM = bucket.ToAssignBucketVMFromBucket(); + bucketVM.EmployeeIds = mappedEmployees.Distinct().ToList(); + bucketVM.NumberOfContacts = contactCount; + + bucketVMs.Add(bucketVM); + } + + _logger.LogInfo("Fetched {BucketCount} buckets for Employee {EmployeeId} successfully", bucketVMs.Count, loggedInEmployee.Id); + + return ApiResponse.SuccessResponse(bucketVMs, $"{bucketVMs.Count} buckets fetched successfully", 200); + } + public async Task> CreateBucketAsync(CreateBucketDto bucketDto, Guid tenantId, Employee loggedInEmployee) + { + if (bucketDto == null) + { + _logger.LogWarning("Employee with ID {LoggedInEmployeeId} sent empty payload", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("User sent empty Payload", "User sent empty Payload", 400); + } + + try + { + // Check permissions for the logged-in employee in parallel + var permissionTask = CheckPermissionsAsync(loggedInEmployee.Id); + + // Check if a bucket with the same name already exists (case insensitive) + var existingBucketTask = _context.Buckets.FirstOrDefaultAsync(b => b.Name.ToLower() == bucketDto.Name.ToLower()); + + await Task.WhenAll(permissionTask, existingBucketTask); + + var (hasAdminPermission, hasManagerPermission, hasUserPermission) = permissionTask.Result; + var existingBucket = existingBucketTask.Result; + + // If the user does not have any of the required permissions, deny access + if (!hasAdminPermission && !hasManagerPermission && !hasUserPermission) + { + _logger.LogWarning("Employee {EmployeeId} attempted to create bucket without permission", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("You don't have permission", "You don't have permission", 403); + } + + // If bucket with the same name exists, return conflict response + if (existingBucket != null) + { + _logger.LogWarning("Employee {EmployeeId} attempted to create an existing bucket with name '{BucketName}'", loggedInEmployee.Id, bucketDto.Name); + return ApiResponse.ErrorResponse("Bucket already exists", "Bucket already exists", 409); + } + + // Create new bucket entity + var newBucket = new Bucket + { + Name = bucketDto.Name, + Description = bucketDto.Description, + CreatedAt = DateTime.UtcNow, + CreatedByID = loggedInEmployee.Id, + TenantId = tenantId + }; + + // Add bucket to context + _context.Buckets.Add(newBucket); + + // Create mapping between employee and bucket + var employeeBucketMapping = new EmployeeBucketMapping + { + EmployeeId = loggedInEmployee.Id, + BucketId = newBucket.Id + }; + + // Add employee-bucket mapping to context + _context.EmployeeBucketMappings.Add(employeeBucketMapping); + + // Save changes to DB + await _context.SaveChangesAsync(); + + // Load the newly created bucket including creator info for response + var createdBucket = await _context.Buckets + .Include(b => b.CreatedBy) + .ThenInclude(e => e!.JobRole) + .FirstOrDefaultAsync(b => b.Id == newBucket.Id); + + var bucketVM = _mapper.Map(createdBucket); + + _logger.LogInfo("Employee {EmployeeId} successfully created bucket {BucketId}", loggedInEmployee.Id, newBucket.Id); + + return ApiResponse.SuccessResponse(bucketVM, "Bucket created successfully", 200); + } + catch (Exception ex) + { + // Log unexpected exceptions + _logger.LogError(ex, "Error occurred while employee {EmployeeId} was creating a bucket", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Internal server error", "Internal server error", 500); + } + } + public async Task> UpdateBucketAsync(Guid id, UpdateBucketDto bucketDto, Guid tenantId, Employee loggedInEmployee) + { + if (bucketDto == null || id != bucketDto.Id) + { + _logger.LogWarning("Employee with ID {LoggedInEmployeeId} sent invalid or empty payload for bucket update.", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Invalid or empty payload", "Invalid or empty payload", 400); + } + + try + { + // Check user permissions in parallel + var permissionTask = CheckPermissionsAsync(loggedInEmployee.Id); + + // Use IDbContextFactory to create separate contexts for parallel DB calls + using var employeeBucketContext = _dbContextFactory.CreateDbContext(); + using var bucketContext = _dbContextFactory.CreateDbContext(); + using var contactBucketContext = _dbContextFactory.CreateDbContext(); + + // Load employee buckets where BucketId == id in parallel + var employeeBucketsTask = employeeBucketContext.EmployeeBucketMappings + .Where(eb => eb.EmployeeId == loggedInEmployee.Id) + .ToListAsync(); + + // Load the bucket with CreatedBy for given id and tenantId + var bucketTask = bucketContext.Buckets + .Include(b => b.CreatedBy) + .ThenInclude(e => e!.JobRole) + .FirstOrDefaultAsync(b => b.Id == bucketDto.Id && b.TenantId == tenantId); + + // Await all parallel tasks + await Task.WhenAll(permissionTask, employeeBucketsTask, bucketTask); + + var (hasAdminPermission, hasManagerPermission, hasUserPermission) = permissionTask.Result; + var employeeBuckets = employeeBucketsTask.Result; + var bucket = bucketTask.Result; + + // Validate bucket exists + if (bucket == null) + { + _logger.LogWarning("Employee ID {LoggedInEmployeeId} attempted to update bucket {BucketId}, but it was not found in database.", loggedInEmployee.Id, bucketDto.Id); + return ApiResponse.ErrorResponse("Bucket not found", "Bucket not found", 404); + } + + // Determine if employee has access to update the bucket + var bucketIdsForEmployee = employeeBuckets.Select(eb => eb.BucketId).ToList(); + + bool hasAccess = false; + if (hasAdminPermission) + { + hasAccess = true; + } + else if (hasManagerPermission && bucketIdsForEmployee.Contains(id)) + { + hasAccess = true; + } + else if (hasUserPermission && bucket.CreatedByID == loggedInEmployee.Id) + { + hasAccess = true; + } + + if (!hasAccess) + { + _logger.LogWarning("Employee ID {LoggedInEmployeeId} attempted to update bucket {BucketId} without sufficient permissions.", loggedInEmployee.Id, bucket.Id); + return ApiResponse.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 403); + } + + var bucketObject = _updateLogsHelper.EntityToBsonDocument(bucket); + + // Update bucket properties safely + bucket.Name = bucketDto.Name ?? string.Empty; + bucket.Description = bucketDto.Description ?? string.Empty; + + // Log the update attempt in directory update logs + _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog + { + RefereanceId = bucketDto.Id, + UpdatedById = loggedInEmployee.Id, + UpdateAt = DateTime.UtcNow + }); + + // Save changes to bucket and logs + await _context.SaveChangesAsync(); + + // Now load contacts related to the bucket using a separate context for parallelism + var contactBuckets = await contactBucketContext.ContactBucketMappings + .Where(cb => cb.BucketId == bucket.Id) + .ToListAsync(); + + // Prepare view model to return + AssignBucketVM bucketVM = _mapper.Map(bucket); + bucketVM.EmployeeIds = employeeBuckets.Where(eb => eb.BucketId == bucket.Id).Select(eb => eb.EmployeeId).ToList(); + bucketVM.NumberOfContacts = contactBuckets.Count; + + await _updateLogsHelper.PushToUpdateLogsAsync(new UpdateLogsObject + { + EntityId = bucket.Id.ToString(), + UpdatedById = loggedInEmployee.Id.ToString(), + OldObject = bucketObject, + UpdatedAt = DateTime.UtcNow + }, bucketCollection); + + _logger.LogInfo("Employee ID {LoggedInEmployeeId} successfully updated bucket ID {BucketId}.", loggedInEmployee.Id, bucket.Id); + + return ApiResponse.SuccessResponse(bucketVM, "Bucket updated successfully", 200); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while employee ID {LoggedInEmployeeId} attempted to update bucket ID {BucketId}.", loggedInEmployee.Id, id); + return ApiResponse.ErrorResponse("An unexpected error occurred. Please try again later.", "Internal server error", 500); + } + } + public async Task> AssignBucketAsync(Guid bucketId, List assignBuckets, Guid tenantId, Employee loggedInEmployee) + { + // Validate input payload + if (assignBuckets == null || bucketId == Guid.Empty) + { + _logger.LogWarning("Employee with ID {EmployeeId} sent empty or invalid payload.", loggedInEmployee.Id); + return ApiResponse.ErrorResponse("User sent empty or invalid payload", "User sent empty or invalid payload", 400); + } + + // Check permissions of the logged-in employee + var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id); + + + // Load the bucket with related CreatedBy and validate tenant + var bucket = await _context.Buckets + .Include(b => b.CreatedBy) + .ThenInclude(e => e!.JobRole) + .FirstOrDefaultAsync(b => b.Id == bucketId && b.TenantId == tenantId); + + if (bucket == null) + { + _logger.LogWarning("Employee ID {EmployeeId} attempted to update bucket {BucketId} but it was not found.", loggedInEmployee.Id, bucketId); + return ApiResponse.ErrorResponse("Bucket not found", "Bucket not found", 404); + } + + // Load EmployeeBucketMappings related to the bucket + var employeeBuckets = await _context.EmployeeBucketMappings + .Where(eb => eb.BucketId == bucketId) + .ToListAsync(); + + var employeeBucketIds = employeeBuckets.Select(eb => eb.EmployeeId).ToHashSet(); + + // Check access permissions to the bucket + bool hasAccess = false; + if (hasAdminPermission) + { + hasAccess = true; + } + else if (hasManagerPermission && employeeBucketIds.Contains(loggedInEmployee.Id)) + { + hasAccess = true; + } + else if (hasUserPermission && bucket.CreatedByID == loggedInEmployee.Id) + { + hasAccess = true; + } + + if (!hasAccess) + { + _logger.LogWarning("Employee {EmployeeId} attempted to access bucket {BucketId} without permission.", loggedInEmployee.Id, bucketId); + return ApiResponse.ErrorResponse("You don't have permission to access this bucket", "You don't have permission to access this bucket", 403); + } + + // Load active employees tenant-wide + var activeEmployeeIds = await _context.Employees + .Where(e => e.TenantId == tenantId && e.IsActive) + .Select(e => e.Id) + .ToListAsync(); + + int assignedEmployeesCount = 0; + int removedEmployeesCount = 0; + + // Process each assignment request + foreach (var assignBucket in assignBuckets) + { + if (!activeEmployeeIds.Contains(assignBucket.EmployeeId)) + { + // Skip employee IDs that are not active or not in tenant + _logger.LogWarning("Skipping inactive or non-tenant employee ID {EmployeeId} in assignment.", assignBucket.EmployeeId); + continue; + } + + if (assignBucket.IsActive) + { + // Add mapping if not already exists + if (!employeeBucketIds.Contains(assignBucket.EmployeeId)) + { + var newMapping = new EmployeeBucketMapping + { + EmployeeId = assignBucket.EmployeeId, + BucketId = bucketId + }; + _context.EmployeeBucketMappings.Add(newMapping); + assignedEmployeesCount++; + } + } + else + { + // Remove mapping if it exists + var existingMapping = employeeBuckets.FirstOrDefault(eb => eb.EmployeeId == assignBucket.EmployeeId); + if (existingMapping != null && bucket.CreatedByID != assignBucket.EmployeeId) + { + _context.EmployeeBucketMappings.Remove(existingMapping); + removedEmployeesCount++; + } + } + } + + // Add directory update log + _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog + { + RefereanceId = bucketId, + UpdatedById = loggedInEmployee.Id, + UpdateAt = DateTime.UtcNow + }); + + // Save changes with error handling + try + { + await _context.SaveChangesAsync(); + } + catch (DbUpdateException ex) + { + _logger.LogError(ex, "Failed to save changes while assigning bucket {BucketId} by employee {EmployeeId}.", bucketId, loggedInEmployee.Id); + return ApiResponse.ErrorResponse("Failed to update bucket assignments", "An error occurred while updating bucket assignments", 500); + } + + // Reload mappings and contacts for the updated bucket + var employeeBucketMappingTask = Task.Run(async () => + { + using (var context = _dbContextFactory.CreateDbContext()) + { + return await context.EmployeeBucketMappings.Where(eb => eb.BucketId == bucket.Id).ToListAsync(); + } + }); + var contactBucketMappingTask = Task.Run(async () => + { + using (var context = _dbContextFactory.CreateDbContext()) + { + return await context.ContactBucketMappings.Where(eb => eb.BucketId == bucket.Id).ToListAsync(); + } + }); + await Task.WhenAll(employeeBucketMappingTask, contactBucketMappingTask); + var employeeBucketMappings = employeeBucketMappingTask.Result; + var contactBucketMappings = contactBucketMappingTask.Result; + + // Prepare view model response + var bucketVm = _mapper.Map(bucket); + bucketVm.EmployeeIds = employeeBucketMappings.Select(eb => eb.EmployeeId).ToList(); + bucketVm.NumberOfContacts = contactBucketMappings.Count; + + // Log assignment and removal actions + if (assignedEmployeesCount > 0) + { + _logger.LogInfo("Employee {EmployeeId} assigned bucket {BucketId} to {Count} employees.", loggedInEmployee.Id, bucketId, assignedEmployeesCount); + } + if (removedEmployeesCount > 0) + { + _logger.LogInfo("Employee {EmployeeId} removed {Count} employees from bucket {BucketId}.", loggedInEmployee.Id, removedEmployeesCount, bucketId); + } + + return ApiResponse.SuccessResponse(bucketVm, "Bucket details updated successfully", 200); + } + public async Task> DeleteBucketAsync(Guid id, Guid tenantId, Employee loggedInEmployee) + { + try + { + // Fetch the bucket in the main context to verify existence and tenant scope + var bucket = await _context.Buckets.FirstOrDefaultAsync(b => b.Id == id && b.TenantId == tenantId); + + if (bucket == null) + { + _logger.LogWarning("Employee {EmployeeId} attempted to delete bucket {BucketId}, bucket not found for tenant {TenantId}.", + loggedInEmployee.Id, id, tenantId); + + // Returning success response to maintain idempotency + return ApiResponse.SuccessResponse(new { }, "Bucket deleted successfully", 200); + } + + // Run parallel tasks to fetch related mappings using separate DbContext instances + var employeeBucketMappingsTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + return await context.EmployeeBucketMappings + .Where(eb => eb.BucketId == bucket.Id) + .ToListAsync(); + }); + + var contactBucketMappingsTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + return await context.ContactBucketMappings + .Where(cb => cb.BucketId == bucket.Id) + .ToListAsync(); + }); + + // Await both tasks concurrently + await Task.WhenAll(employeeBucketMappingsTask, contactBucketMappingsTask); + + var employeeBucketMappings = employeeBucketMappingsTask.Result; + var contactBucketMappings = contactBucketMappingsTask.Result; + + // Check if bucket has any contacts mapped - cannot delete in this state + if (contactBucketMappings.Any()) + { + _logger.LogInfo("Employee {EmployeeId} attempted to delete bucket {BucketId} but bucket contains contacts, deletion blocked.", + loggedInEmployee.Id, bucket.Id); + + return ApiResponse.ErrorResponse("This bucket cannot be deleted because it contains contacts.", + "This bucket cannot be deleted", 400); + } + + // Check permissions of the logged-in employee + var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id); + + var accessibleBucket = (Bucket?)null; + + // Get bucket IDs for which the employee has mapping association + var employeeBucketIds = employeeBucketMappings + .Where(eb => eb.EmployeeId == loggedInEmployee.Id) + .Select(eb => eb.BucketId) + .ToList(); + + // Determine if employee has permission to delete this bucket + if (hasAdminPermission) + { + accessibleBucket = bucket; + } + else if (hasManagerPermission && employeeBucketIds.Contains(bucket.Id)) + { + accessibleBucket = bucket; + } + else if (hasUserPermission && bucket.CreatedByID == loggedInEmployee.Id) + { + accessibleBucket = bucket; + } + + if (accessibleBucket == null) + { + _logger.LogWarning("Employee {EmployeeId} attempted to delete bucket {BucketId} without sufficient permissions.", + loggedInEmployee.Id, bucket.Id); + + return ApiResponse.ErrorResponse("You don't have permission to access this bucket.", + "Permission denied", 403); + } + + var bucketObject = _updateLogsHelper.EntityToBsonDocument(bucket); + + // Remove related employee bucket mappings + _context.EmployeeBucketMappings.RemoveRange(employeeBucketMappings); + // Remove the bucket itself + _context.Buckets.Remove(bucket); + // Log deletion action + _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog + { + RefereanceId = bucket.Id, + UpdatedById = loggedInEmployee.Id, + UpdateAt = DateTime.UtcNow + }); + + await _context.SaveChangesAsync(); + + _logger.LogInfo("Employee {EmployeeId} deleted bucket {BucketId} along with related employee bucket mappings.", + loggedInEmployee.Id, bucket.Id); + + await _updateLogsHelper.PushToUpdateLogsAsync(new UpdateLogsObject + { + EntityId = bucket.Id.ToString(), + UpdatedById = loggedInEmployee.Id.ToString(), + OldObject = bucketObject, + UpdatedAt = DateTime.UtcNow + }, bucketCollection); + + return ApiResponse.SuccessResponse(new { }, "Bucket deleted successfully", 200); + } + catch (Exception ex) + { + // Log the exception with error level + _logger.LogError(ex, "An error occurred while employee {EmployeeId} attempted to delete bucket {BucketId}.", + loggedInEmployee.Id, id); + + return ApiResponse.ErrorResponse("An unexpected error occurred while deleting the bucket.", + "Internal Server Error", 500); + } + } + + #endregion + + #region =================================================================== Helper Functions =================================================================== + + private async Task<(bool hasAdmin, bool hasManager, bool hasUser)> CheckPermissionsAsync(Guid employeeId) + { + // Scoping the service provider ensures services are disposed of correctly. + using var scope = _serviceScopeFactory.CreateScope(); + var permissionService = scope.ServiceProvider.GetRequiredService(); + + // Run all permission checks in parallel. + var hasAdminTask = permissionService.HasPermission(PermissionsMaster.DirectoryAdmin, employeeId); + var hasManagerTask = permissionService.HasPermission(PermissionsMaster.DirectoryManager, employeeId); + var hasUserTask = permissionService.HasPermission(PermissionsMaster.DirectoryUser, employeeId); + + await Task.WhenAll(hasAdminTask, hasManagerTask, hasUserTask); + + return (hasAdminTask.Result, hasManagerTask.Result, hasUserTask.Result); + } + private bool Compare(string sentence, string search) + { + sentence = sentence.Trim().ToLower(); + search = search.Trim().ToLower(); + + // Check for exact substring + bool result = sentence.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0; + return result; + } + + private ContactFilterDto? TryDeserializeContactFilter(string? filter) + { + if (string.IsNullOrWhiteSpace(filter)) + { + return null; + } + + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + ContactFilterDto? expenseFilter = null; + + try + { + // First, try to deserialize directly. This is the expected case (e.g., from a web client). + expenseFilter = JsonSerializer.Deserialize(filter, options); + } + catch (JsonException ex) + { + _logger.LogError(ex, "[{MethodName}] Failed to directly deserialize filter. Attempting to unescape and re-parse. Filter: {Filter}", nameof(TryDeserializeContactFilter), filter); + + // If direct deserialization fails, it might be an escaped string (common with tools like Postman or some mobile clients). + try + { + // Unescape the string first, then deserialize the result. + string unescapedJsonString = JsonSerializer.Deserialize(filter, options) ?? ""; + if (!string.IsNullOrWhiteSpace(unescapedJsonString)) + { + expenseFilter = JsonSerializer.Deserialize(unescapedJsonString, options); + } + } + catch (JsonException ex1) + { + // If both attempts fail, log the final error and return null. + _logger.LogError(ex1, "[{MethodName}] All attempts to deserialize the filter failed. Filter will be ignored. Filter: {Filter}", nameof(TryDeserializeContactFilter), filter); + return null; + } + } + return expenseFilter; + } + private ContactNoteFilter? TryDeserializeContactNoteFilter(string? filter) + { + if (string.IsNullOrWhiteSpace(filter)) + { + return null; + } + + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + ContactNoteFilter? expenseFilter = null; + + try + { + // First, try to deserialize directly. This is the expected case (e.g., from a web client). + expenseFilter = JsonSerializer.Deserialize(filter, options); + } + catch (JsonException ex) + { + _logger.LogError(ex, "[{MethodName}] Failed to directly deserialize filter. Attempting to unescape and re-parse. Filter: {Filter}", nameof(TryDeserializeContactNoteFilter), filter); + + // If direct deserialization fails, it might be an escaped string (common with tools like Postman or some mobile clients). + try + { + // Unescape the string first, then deserialize the result. + string unescapedJsonString = JsonSerializer.Deserialize(filter, options) ?? ""; + if (!string.IsNullOrWhiteSpace(unescapedJsonString)) + { + expenseFilter = JsonSerializer.Deserialize(unescapedJsonString, options); + } + } + catch (JsonException ex1) + { + // If both attempts fail, log the final error and return null. + _logger.LogError(ex1, "[{MethodName}] All attempts to deserialize the filter failed. Filter will be ignored. Filter: {Filter}", nameof(TryDeserializeContactNoteFilter), filter); + return null; + } + } + return expenseFilter; + } + + private static object ExceptionMapper(Exception ex) + { + return new + { + Message = ex.Message, + StackTrace = ex.StackTrace, + Source = ex.Source, + InnerException = new + { + Message = ex.InnerException?.Message, + StackTrace = ex.InnerException?.StackTrace, + Source = ex.InnerException?.Source, + } + }; + } + + // --- Helper Methods for Readability and Separation of Concerns --- + + private List ProcessContactPhones(CreateContactDto dto, Contact contact, ISet existingPhones) + { + if (!(dto.ContactPhones?.Any() ?? false)) return new List(); + + var newPhones = dto.ContactPhones + .Where(p => !string.IsNullOrWhiteSpace(p.PhoneNumber) && existingPhones.Add(p.PhoneNumber)) // .Add returns true if the item was added (i.e., not present) + .Select(pDto => + { + var phone = _mapper.Map(pDto); + phone.ContactId = contact.Id; + phone.TenantId = contact.TenantId; // Ensure tenant is set on child entities + return phone; + }).ToList(); + + _context.ContactsPhones.AddRange(newPhones); + _logger.LogInfo("Adding {Count} new phone numbers for Contact {ContactId}.", newPhones.Count, contact.Id); + + return newPhones.Select(p => _mapper.Map(p)).ToList(); + } + private List ProcessContactEmails(CreateContactDto dto, Contact contact, ISet existingEmails) + { + if (!(dto.ContactEmails?.Any() ?? false)) return new List(); + + var newEmails = dto.ContactEmails + .Where(e => !string.IsNullOrWhiteSpace(e.EmailAddress) && existingEmails.Add(e.EmailAddress)) // HashSet handles case-insensitivity set in constructor + .Select(eDto => + { + var email = _mapper.Map(eDto); + email.ContactId = contact.Id; + email.TenantId = contact.TenantId; + return email; + }).ToList(); + + _context.ContactsEmails.AddRange(newEmails); + _logger.LogInfo("Adding {Count} new email addresses for Contact {ContactId}.", newEmails.Count, contact.Id); + + return newEmails.Select(e => _mapper.Map(e)).ToList(); + } + private List ProcessTags(CreateContactDto dto, Contact contact, IDictionary tenantTags) + { + if (!(dto.Tags?.Any() ?? false)) return new List(); + + var tagVMs = new List(); + var newTagMappings = new List(); + + foreach (var tagName in dto.Tags.Select(t => t.Name).Distinct(StringComparer.OrdinalIgnoreCase)) + { + if (string.IsNullOrWhiteSpace(tagName)) continue; + + var normalizedTag = tagName.ToLowerInvariant(); + if (!tenantTags.TryGetValue(normalizedTag, out var tagMaster)) + { + // Tag does not exist, create it. + tagMaster = new ContactTagMaster { Name = tagName, Description = tagName, TenantId = contact.TenantId }; + _context.ContactTagMasters.Add(tagMaster); + tenantTags.Add(normalizedTag, tagMaster); // Add to dictionary to handle duplicates in the input list + _logger.LogDebug("Creating new tag '{TagName}' for Tenant {TenantId}.", tagName, contact.TenantId); + } + + newTagMappings.Add(new ContactTagMapping { ContactId = contact.Id, ContactTag = tagMaster }); + tagVMs.Add(_mapper.Map(tagMaster)); + } + + _context.ContactTagMappings.AddRange(newTagMappings); + _logger.LogInfo("Adding {Count} tag mappings for Contact {ContactId}.", newTagMappings.Count, contact.Id); + + return tagVMs; + } + private async Task> ProcessBucketMappingsAsync(CreateContactDto dto, Contact contact, Guid tenantId) + { + if (!(dto.BucketIds?.Any() ?? false)) return new List(); + + var validBucketIds = await _context.Buckets + .Where(b => dto.BucketIds.Contains(b.Id) && b.TenantId == tenantId) + .Select(b => b.Id) + .ToListAsync(); + + var mappings = validBucketIds.Select(bucketId => new ContactBucketMapping + { + BucketId = bucketId, + ContactId = contact.Id + }).ToList(); + + _context.ContactBucketMappings.AddRange(mappings); + _logger.LogInfo("Adding {Count} bucket mappings for Contact {ContactId}.", mappings.Count, contact.Id); + + return mappings; + } + private async Task> ProcessProjectMappingsAsync(CreateContactDto dto, Contact contact, Guid tenantId) + { + if (!(dto.ProjectIds?.Any() ?? false)) return new List(); + + var validProjectIds = await _context.Projects + .Where(p => dto.ProjectIds.Contains(p.Id) && p.TenantId == tenantId) + .Select(p => p.Id) + .ToListAsync(); + + var mappings = validProjectIds.Select(projectId => new ContactProjectMapping + { + ProjectId = projectId, + ContactId = contact.Id, + TenantId = tenantId + }).ToList(); + + _context.ContactProjectMappings.AddRange(mappings); + _logger.LogInfo("Adding {Count} project mappings for Contact {ContactId}.", mappings.Count, contact.Id); + + return mappings; + } + + #endregion + } +} diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IDirectoryService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IDirectoryService.cs new file mode 100644 index 0000000..d499d1e --- /dev/null +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IDirectoryService.cs @@ -0,0 +1,38 @@ +using Marco.Pms.Model.Dtos.Directory; +using Marco.Pms.Model.Employees; +using Marco.Pms.Model.Utilities; + +namespace Marco.Pms.Services.Service.ServiceInterfaces +{ + public interface IDirectoryService + { + Task> GetListOfContactsAsync(string? search, string? filter, Guid? projectId, bool active, int pageSize, int pageNumber, Guid tenantId, Employee loggedInEmployee); + Task> GetListOfContactsOld(string? search, bool active, ContactFilterDto? filterDto, Guid? projectId); + Task> GetContactsListByBucketIdAsync(Guid bucketId, Guid tenantId, Employee loggedInEmployee); + Task> GetContactProfileAsync(Guid id, Guid tenantId, Employee loggedInEmployee); + Task> GetOrganizationListAsync(Guid tenantId, Employee loggedInEmployee); + Task> GetDesignationListAsync(Guid tenantId, Employee loggedInEmployee); + Task> GetContactFilterObjectAsync(Guid tenantId, Employee loggedInEmployee); + Task> CreateContactAsync(CreateContactDto createContact, Guid tenantId, Employee loggedInEmployee); + Task> UpdateContactAsync(Guid id, UpdateContactDto updateContact, Guid tenantId, Employee loggedInEmployee); + Task> DeleteContactAsync(Guid id, bool active, Guid tenantId, Employee loggedInEmployee); + + + + + + Task> GetListOFAllNotesAsync(Guid? projectId, string? searchString, string? filter, int pageSize, int pageNumber, Guid tenantId, Employee loggedInEmployee); + Task> GetNoteListByContactIdAsync(Guid id, bool active, Guid tenantId, Employee loggedInEmployee); + Task> GetContactNotesFilterObjectAsync(Guid tenantId, Employee loggedInEmployee); + Task> CreateContactNoteAsync(CreateContactNoteDto noteDto, Guid tenantId, Employee loggedInEmployee); + Task> UpdateContactNoteAsync(Guid id, UpdateContactNoteDto noteDto, Guid tenantId, Employee loggedInEmployee); + Task> DeleteContactNoteAsync(Guid id, bool active, Guid tenantId, Employee loggedInEmployee); + + + Task> GetBucketListAsync(Guid tenantId, Employee loggedInEmployee); + Task> CreateBucketAsync(CreateBucketDto bucketDto, Guid tenantId, Employee loggedInEmployee); + Task> UpdateBucketAsync(Guid id, UpdateBucketDto bucketDto, Guid tenantId, Employee loggedInEmployee); + Task> AssignBucketAsync(Guid bucketId, List assignBuckets, Guid tenantId, Employee loggedInEmployee); + Task> DeleteBucketAsync(Guid id, Guid tenantId, Employee loggedInEmployee); + } +}