From 019ef5d9974231d8ceb6983528ca88979af1a43a Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Sat, 26 Jul 2025 14:57:05 +0530 Subject: [PATCH] Optimized the contact list API in Directory Controller --- .../Controllers/DirectoryController.cs | 150 +- .../MappingProfiles/MappingProfile.cs | 21 +- Marco.Pms.Services/Program.cs | 1 + .../Service/DirectoryService.cs | 1778 +++++++++++++++++ .../ServiceInterfaces/IDirectoryService.cs | 28 + 5 files changed, 1912 insertions(+), 66 deletions(-) create mode 100644 Marco.Pms.Services/Service/DirectoryService.cs create mode 100644 Marco.Pms.Services/Service/ServiceInterfaces/IDirectoryService.cs diff --git a/Marco.Pms.Services/Controllers/DirectoryController.cs b/Marco.Pms.Services/Controllers/DirectoryController.cs index 9eb06e0..16da931 100644 --- a/Marco.Pms.Services/Controllers/DirectoryController.cs +++ b/Marco.Pms.Services/Controllers/DirectoryController.cs @@ -1,6 +1,7 @@ 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,16 +15,32 @@ 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 Guid tenantId; - - public DirectoryController(DirectoryHelper directoryHelper, ILoggingService logger) + public DirectoryController(IDirectoryService directoryHelper, UserHelper userHelper, ILoggingService logger) { - _directoryHelper = directoryHelper; + _directoryService = directoryHelper; + _userHelper = userHelper; _logger = logger; + tenantId = userHelper.GetTenantId(); } + #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] public async Task GetContactList([FromQuery] string? search, [FromQuery] List? bucketIds, [FromQuery] List? categoryIds, [FromQuery] Guid? projectId, [FromQuery] bool active = true) { @@ -32,7 +49,7 @@ namespace Marco.Pms.Services.Controllers BucketIds = bucketIds, CategoryIds = categoryIds }; - var response = await _directoryHelper.GetListOfContacts(search, active, filterDto, projectId); + var response = await _directoryService.GetListOfContactsOld(search, active, filterDto, projectId); if (response.StatusCode == 200) @@ -53,7 +70,7 @@ namespace Marco.Pms.Services.Controllers [HttpGet("contact-bucket/{bucketId}")] public async Task GetContactsListByBucketId(Guid bucketId) { - var response = await _directoryHelper.GetContactsListByBucketId(bucketId); + var response = await _directoryService.GetContactsListByBucketId(bucketId); if (response.StatusCode == 200) { return Ok(response); @@ -68,55 +85,10 @@ namespace Marco.Pms.Services.Controllers } } - [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); - if (response.StatusCode == 200) - { - return Ok(response); - } - else - { - return BadRequest(response); - } - } - - [HttpPut("{id}")] - public async Task UpdateContact(Guid id, [FromBody] UpdateContactDto updateContact) - { - var response = await _directoryHelper.UpdateContact(id, updateContact); - if (response.StatusCode == 200) - { - return Ok(response); - } - else if (response.StatusCode == 404) - { - return NotFound(response); - } - else if (response.StatusCode == 401) - { - return Unauthorized(response); - } - else - { - return BadRequest(response); - } - } - [HttpGet("profile/{id}")] public async Task GetContactProfile(Guid id) { - var response = await _directoryHelper.GetContactProfile(id); + var response = await _directoryService.GetContactProfile(id); if (response.StatusCode == 200) { return Ok(response); @@ -134,14 +106,61 @@ namespace Marco.Pms.Services.Controllers [HttpGet("organization")] public async Task GetOrganizationList() { - var response = await _directoryHelper.GetOrganizationList(); + var response = await _directoryService.GetOrganizationList(); return Ok(response); } + #endregion + + [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 _directoryService.CreateContact(createContact); + if (response.StatusCode == 200) + { + return Ok(response); + } + else + { + return BadRequest(response); + } + } + + [HttpPut("{id}")] + public async Task UpdateContact(Guid id, [FromBody] UpdateContactDto updateContact) + { + var response = await _directoryService.UpdateContact(id, updateContact); + if (response.StatusCode == 200) + { + return Ok(response); + } + else if (response.StatusCode == 404) + { + return NotFound(response); + } + else if (response.StatusCode == 401) + { + return Unauthorized(response); + } + else + { + return BadRequest(response); + } + } + [HttpDelete("{id}")] public async Task DeleteContact(Guid id, [FromQuery] bool? active) { - var response = await _directoryHelper.DeleteContact(id, active ?? false); + var response = await _directoryService.DeleteContact(id, active ?? false); if (response.StatusCode == 200) { return Ok(response); @@ -155,13 +174,14 @@ namespace Marco.Pms.Services.Controllers return BadRequest(response); } } + #endregion // -------------------------------- Contact Notes -------------------------------- [HttpGet("notes")] public async Task GetListOFAllNotes([FromQuery] Guid? projectId, [FromQuery] int? pageSize, [FromQuery] int pageNumber) { - var response = await _directoryHelper.GetListOFAllNotes(projectId, pageSize ?? 25, pageNumber); + var response = await _directoryService.GetListOFAllNotes(projectId, pageSize ?? 25, pageNumber); return StatusCode(response.StatusCode, response); } @@ -169,7 +189,7 @@ namespace Marco.Pms.Services.Controllers public async Task CreateContactNote([FromBody] CreateContactNoteDto noteDto) { - var response = await _directoryHelper.CreateContactNote(noteDto); + var response = await _directoryService.CreateContactNote(noteDto); if (response.StatusCode == 200) { return Ok(response); @@ -187,7 +207,7 @@ namespace Marco.Pms.Services.Controllers [HttpGet("notes/{ContactId}")] public async Task GetNoteListByContactId(Guid contactId, [FromQuery] bool active = true) { - var response = await _directoryHelper.GetNoteListByContactId(contactId, active); + var response = await _directoryService.GetNoteListByContactId(contactId, active); if (response.StatusCode == 200) { return Ok(response); @@ -205,7 +225,7 @@ namespace Marco.Pms.Services.Controllers [HttpPut("note/{id}")] public async Task UpdateContactNote(Guid id, [FromBody] UpdateContactNoteDto noteDto) { - var response = await _directoryHelper.UpdateContactNote(id, noteDto); + var response = await _directoryService.UpdateContactNote(id, noteDto); if (response.StatusCode == 200) { return Ok(response); @@ -223,7 +243,7 @@ namespace Marco.Pms.Services.Controllers [HttpDelete("note/{id}")] public async Task DeleteContactNote(Guid id, [FromQuery] bool? active) { - var response = await _directoryHelper.DeleteContactNote(id, active ?? false); + var response = await _directoryService.DeleteContactNote(id, active ?? false); return Ok(response); } @@ -232,7 +252,7 @@ namespace Marco.Pms.Services.Controllers [HttpGet("buckets")] public async Task GetBucketList() { - var response = await _directoryHelper.GetBucketList(); + var response = await _directoryService.GetBucketList(); if (response.StatusCode == 200) { return Ok(response); @@ -259,7 +279,7 @@ namespace Marco.Pms.Services.Controllers _logger.LogWarning("User sent Invalid Date while marking attendance"); return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } - var response = await _directoryHelper.CreateBucket(bucketDto); + var response = await _directoryService.CreateBucket(bucketDto); if (response.StatusCode == 200) { return Ok(response); @@ -282,7 +302,7 @@ namespace Marco.Pms.Services.Controllers [HttpPut("bucket/{id}")] public async Task UpdateBucket(Guid id, [FromBody] UpdateBucketDto bucketDto) { - var response = await _directoryHelper.UpdateBucket(id, bucketDto); + var response = await _directoryService.UpdateBucket(id, bucketDto); if (response.StatusCode == 200) { return Ok(response); @@ -304,7 +324,7 @@ namespace Marco.Pms.Services.Controllers [HttpPost("assign-bucket/{bucketId}")] public async Task AssignBucket(Guid bucketId, [FromBody] List assignBuckets) { - var response = await _directoryHelper.AssignBucket(bucketId, assignBuckets); + var response = await _directoryService.AssignBucket(bucketId, assignBuckets); if (response.StatusCode == 200) { return Ok(response); @@ -326,7 +346,7 @@ namespace Marco.Pms.Services.Controllers [HttpDelete("bucket/{id}")] public async Task DeleteBucket(Guid id) { - var response = await _directoryHelper.DeleteBucket(id); + var response = await _directoryService.DeleteBucket(id); if (response.StatusCode == 200) { return Ok(response); diff --git a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs index bf3777c..46b119b 100644 --- a/Marco.Pms.Services/MappingProfiles/MappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/MappingProfile.cs @@ -1,10 +1,13 @@ using AutoMapper; +using Marco.Pms.Model.Directory; using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Master; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Projects; +using Marco.Pms.Model.ViewModels.Directory; using Marco.Pms.Model.ViewModels.Employee; +using Marco.Pms.Model.ViewModels.Master; using Marco.Pms.Model.ViewModels.Projects; namespace Marco.Pms.Services.MappingProfiles @@ -60,9 +63,25 @@ namespace Marco.Pms.Services.MappingProfiles opt => opt.MapFrom(src => src.Comment)); #endregion - #region ======================================================= Projects ======================================================= + #region ======================================================= Employee ======================================================= + CreateMap(); + #endregion + + #region ======================================================= Directory ======================================================= + + CreateMap(); + + + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + + #endregion + + } } } diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index 5549702..c53a212 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -172,6 +172,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); #endregion #region Helpers diff --git a/Marco.Pms.Services/Service/DirectoryService.cs b/Marco.Pms.Services/Service/DirectoryService.cs new file mode 100644 index 0000000..06883c6 --- /dev/null +++ b/Marco.Pms.Services/Service/DirectoryService.cs @@ -0,0 +1,1778 @@ +using AutoMapper; +using Marco.Pms.DataAccess.Data; +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.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.ServiceInterfaces; +using MarcoBMS.Services.Helpers; +using MarcoBMS.Services.Service; +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 PermissionServices _permissionServices; + + public DirectoryService( + IDbContextFactory dbContextFactory, + ApplicationDbContext context, + ILoggingService logger, + IServiceScopeFactory serviceScopeFactory, + UserHelper userHelper, + IMapper mapper, + PermissionServices permissionServices) + { + _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)); + _permissionServices = permissionServices ?? throw new ArgumentNullException(nameof(permissionServices)); + } + #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. + using var scope = _serviceScopeFactory.CreateScope(); + var permissionService = scope.ServiceProvider.GetRequiredService(); + var hasAdminPermissionTask = permissionService.HasPermission(PermissionsMaster.DirectoryAdmin, loggedInEmployee.Id); + var hasManagerPermissionTask = permissionService.HasPermission(PermissionsMaster.DirectoryManager, loggedInEmployee.Id); + var hasUserPermissionTask = permissionService.HasPermission(PermissionsMaster.DirectoryUser, loggedInEmployee.Id); + await Task.WhenAll(hasAdminPermissionTask, hasManagerPermissionTask, hasUserPermissionTask); + + var hasAdminPermission = hasAdminPermissionTask.Result; + var hasManagerPermission = hasManagerPermissionTask.Result; + var hasUserPermission = hasUserPermissionTask.Result; + + // 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 = TryDeserializeFilter(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)) + .ToListAsync(); + }); + + var emailsTask = Task.Run(async () => + { + await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync(); + return await taskDbContext.ContactsEmails + .Where(e => finalContactIds.Contains(e.ContactId)) + .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", 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> GetContactProfile(Guid id) + { + Guid tenantId = _userHelper.GetTenantId(); + var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + 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()) + { + List contactBucketIds = contactBuckets.Select(cb => cb.BucketId).ToList(); + List employeeBucketIds = employeeBuckets.Select(eb => eb.BucketId).ToList(); + List? 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() + { + Guid tenantId = _userHelper.GetTenantId(); + var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + + var organizationList = await _context.Contacts.Where(c => c.TenantId == tenantId).Select(c => c.Organization).Distinct().ToListAsync(); + _logger.LogInfo("Employee {EmployeeId} fetched list of organizations in a tenant {TenantId}", LoggedInEmployee.Id, tenantId); + return ApiResponse.SuccessResponse(organizationList, $"{organizationList.Count} records of organization names fetched from contacts", 200); + } + + #endregion + + #region =================================================================== Contact Post APIs =================================================================== + 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); + } + + #endregion + + #region =================================================================== Contact Put APIs =================================================================== + 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); + } + #endregion + + #region =================================================================== Contact Delete APIs =================================================================== + public async Task> DeleteContact(Guid id, bool active) + { + Guid tenantId = _userHelper.GetTenantId(); + var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + if (id != Guid.Empty) + { + Contact? contact = await _context.Contacts.FirstOrDefaultAsync(c => c.Id == id && c.TenantId == tenantId); + if (contact == null) + { + _logger.LogWarning("Employee with ID {LoggedInEmployeeId} tries to delete contact with ID {ContactId} is not found in database", LoggedInEmployee.Id); + return ApiResponse.ErrorResponse("Contact not found", "Contact not found", 404); + } + contact.IsActive = active; + + _context.DirectoryUpdateLogs.Add(new DirectoryUpdateLog + { + RefereanceId = contact.Id, + UpdatedById = LoggedInEmployee.Id, + UpdateAt = DateTime.UtcNow + }); + + await _context.SaveChangesAsync(); + _logger.LogInfo("Contact {ContactId} has been deleted by Employee {Employee}", id, LoggedInEmployee.Id); + return ApiResponse.SuccessResponse(new { }, "Contact is deleted Successfully", 200); + } + _logger.LogInfo("Employee ID {EmployeeId} sent an empty contact id", LoggedInEmployee.Id); + return ApiResponse.ErrorResponse("Contact ID is empty", "Contact ID is empty", 400); + } + #endregion + + #endregion + + #region =================================================================== Contact Notes APIs =================================================================== + + /// + /// 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((pageNumber - 1) * pageSize) + .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); + } + + #endregion + + #region =================================================================== Bucket APIs =================================================================== + + 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); + } + + #endregion + + #region =================================================================== Helper Functions =================================================================== + + 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? TryDeserializeFilter(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(TryDeserializeFilter), 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(TryDeserializeFilter), 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, + } + }; + } + + #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..1465e15 --- /dev/null +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IDirectoryService.cs @@ -0,0 +1,28 @@ +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> GetContactsListByBucketId(Guid id); + Task> GetContactProfile(Guid id); + Task> GetOrganizationList(); + Task> CreateContact(CreateContactDto createContact); + Task> UpdateContact(Guid id, UpdateContactDto updateContact); + Task> DeleteContact(Guid id, bool active); + Task> GetListOFAllNotes(Guid? projectId, int pageSize, int pageNumber); + Task> GetNoteListByContactId(Guid id, bool active); + Task> CreateContactNote(CreateContactNoteDto noteDto); + Task> UpdateContactNote(Guid id, UpdateContactNoteDto noteDto); + Task> DeleteContactNote(Guid id, bool active); + Task> GetBucketList(); + Task> CreateBucket(CreateBucketDto bucketDto); + Task> UpdateBucket(Guid id, UpdateBucketDto bucketDto); + Task> AssignBucket(Guid bucketId, List assignBuckets); + Task> DeleteBucket(Guid id); + } +}