2970 lines
147 KiB
C#
2970 lines
147 KiB
C#
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<ApplicationDbContext> _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<ApplicationDbContext> 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<UtilityMongoDBHelper>();
|
||
}
|
||
#region =================================================================== Contact APIs ===================================================================
|
||
|
||
#region =================================================================== Contact Get APIs ===================================================================
|
||
|
||
/// <summary>
|
||
/// Retrieves a paginated list of contacts based on permissions, search criteria, and filters.
|
||
/// </summary>
|
||
/// <param name="search">A search term to filter contacts by name, organization, email, phone, or tag.</param>
|
||
/// <param name="filter">A JSON string representing ContactFilterDto for advanced filtering.</param>
|
||
/// <param name="projectId">Optional project ID to filter contacts assigned to a specific project.</param>
|
||
/// <param name="active">Boolean to filter for active or inactive contacts.</param>
|
||
/// <param name="pageSize">The number of records per page.</param>
|
||
/// <param name="pageNumber">The current page number.</param>
|
||
/// <param name="tenantId">The ID of the tenant to which the contacts belong.</param>
|
||
/// <param name="loggedInEmployee">The employee making the request, used for permission checks.</param>
|
||
/// <returns>An ApiResponse containing the paginated list of contacts or an error.</returns>
|
||
public async Task<ApiResponse<object>> 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<Contact> 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<object>.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<object>.SuccessResponse(new { TotalPages = 0, CurrentPage = pageNumber, PageSize = pageSize, Data = new List<ContactVM>() }, "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<ContactVM>(c);
|
||
contactVM.ContactPhones = _mapper.Map<List<ContactPhoneVM>>(phonesLookup[c.Id]);
|
||
contactVM.ContactEmails = _mapper.Map<List<ContactEmailVM>>(emailsLookup[c.Id]);
|
||
contactVM.Tags = _mapper.Map<List<ContactTagVM>>(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<object>.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<object>.ErrorResponse("An internal error occurred.", ExceptionMapper(ex), 500);
|
||
}
|
||
}
|
||
public async Task<ApiResponse<object>> 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<EmployeeBucketMapping>? employeeBuckets = await _context.EmployeeBucketMappings.Where(eb => eb.EmployeeId == LoggedInEmployee.Id).ToListAsync();
|
||
List<Guid> 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<object>.ErrorResponse("You don't have permission", "You don't have permission", 403);
|
||
}
|
||
|
||
List<Guid> filterbucketIds = bucketIds;
|
||
if (filterDto != null && filterDto.BucketIds != null && filterDto.BucketIds.Count > 0)
|
||
{
|
||
filterbucketIds = filterDto.BucketIds;
|
||
}
|
||
List<ContactBucketMapping>? contactBuckets = await _context.ContactBucketMappings.Where(cb => bucketIds.Contains(cb.BucketId)).ToListAsync();
|
||
List<Guid> contactIds = contactBuckets.Where(b => filterbucketIds.Contains(b.BucketId)).Select(cb => cb.ContactId).ToList();
|
||
List<Contact> contacts = new List<Contact>();
|
||
|
||
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<Guid> 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<Guid> filteredContactIds = new List<Guid>();
|
||
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<ContactVM> list = new List<ContactVM>();
|
||
|
||
foreach (var contact in contacts)
|
||
{
|
||
|
||
ContactVM contactVM = new ContactVM();
|
||
List<ContactEmailVM> contactEmailVms = new List<ContactEmailVM>();
|
||
List<ContactPhoneVM> contactPhoneVms = new List<ContactPhoneVM>();
|
||
|
||
List<ContactTagVM> conatctTagVms = new List<ContactTagVM>();
|
||
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<object>.SuccessResponse(list, System.String.Format("{0} contacts fetched successfully", list.Count), 200);
|
||
|
||
}
|
||
public async Task<ApiResponse<object>> 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<object>.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<object>.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<object>.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<object>.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<object>.SuccessResponse(new List<ContactVM>(), "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<ContactTagMaster> tagMasters = new List<ContactTagMaster>();
|
||
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<ContactVM>();
|
||
|
||
// 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<ContactPhoneVM>(p)).ToList();
|
||
|
||
// Transform emails
|
||
var emailVMs = emails.Where(e => e.ContactId == contact.Id).Select(e => _mapper.Map<ContactEmailVM>(e)).ToList();
|
||
|
||
// Transform tags
|
||
var contactTagMappings = tags.Where(t => t.ContactId == contact.Id);
|
||
var tagVMs = new List<ContactTagVM>();
|
||
foreach (var ct in contactTagMappings)
|
||
{
|
||
var tagMaster = tagMasters.Find(tm => tm.Id == ct.ContactTagId);
|
||
if (tagMaster != null)
|
||
{
|
||
tagVMs.Add(_mapper.Map<ContactTagVM>(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<ContactVM>(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<object>.SuccessResponse(contactVMs, $"{contactVMs.Count} contacts fetched successfully.", 200);
|
||
}
|
||
public async Task<ApiResponse<object>> 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<object>.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<object>.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<object>.ErrorResponse("Contact not found", "Contact not found", 404);
|
||
}
|
||
ContactProfileVM contactVM = _mapper.Map<ContactProfileVM>(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<ContactPhoneVM>(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<ContactEmailVM>(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<BucketVM>(cb.Bucket))
|
||
.ToListAsync();
|
||
}
|
||
List<Guid> 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<BucketVM>(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<ContactTagVM>(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<ContactNoteVM>(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<object>.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<object>.ErrorResponse("An internal error occurred.", ExceptionMapper(ex), 500);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Asynchronously retrieves a distinct list of organization names for a given tenant.
|
||
/// </summary>
|
||
/// <param name="tenantId">The unique identifier of the tenant.</param>
|
||
/// <param name="loggedInEmployee">The employee making the request, used for permission checks.</param>
|
||
/// <returns>
|
||
/// 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).
|
||
/// </returns>
|
||
public async Task<ApiResponse<object>> 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<object>.ErrorResponse("Access Denied", "You do not have permission to perform this action.", 403);
|
||
}
|
||
|
||
// --- 2. Database Query ---
|
||
// Build and execute the database query efficiently.
|
||
_logger.LogDebug("Fetching 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<object>.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<object>.ErrorResponse("An internal server error occurred.", ExceptionMapper(ex), 500);
|
||
}
|
||
}
|
||
public async Task<ApiResponse<object>> GetDesignationListAsync(Guid tenantId, Employee loggedInEmployee)
|
||
{
|
||
// --- Parameter Validation ---
|
||
// Fail fast if essential parameters are not provided.
|
||
ArgumentNullException.ThrowIfNull(loggedInEmployee);
|
||
|
||
var employeeId = loggedInEmployee.Id;
|
||
|
||
try
|
||
{
|
||
// --- 1. Permission Check ---
|
||
// Verify that the employee has at least one of the required permissions to view this data.
|
||
// This prevents unauthorized data access early in the process.
|
||
var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(employeeId);
|
||
|
||
if (!hasAdminPermission && !hasManagerPermission && !hasUserPermission)
|
||
{
|
||
// Log the specific denial reason for security auditing.
|
||
_logger.LogWarning("Access DENIED for employee {EmployeeId} attempting to get designations list for tenant {TenantId} due to lack of permissions.", employeeId, tenantId);
|
||
// Return a strongly-typed error response.
|
||
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to perform this action.", 403);
|
||
}
|
||
|
||
// --- 2. Database Query ---
|
||
// Build and execute the database query efficiently.
|
||
_logger.LogDebug("Fetching designations list for Tenant {TenantId} by Employee {EmployeeId}", tenantId, employeeId);
|
||
|
||
var designationsList = await _context.Contacts
|
||
// Filter contacts by the specified tenant to ensure data isolation and Filter out contacts that do not have an designations name to ensure data quality.
|
||
.Where(c => c.TenantId == tenantId && !string.IsNullOrEmpty(c.Designation))
|
||
|
||
// Project only the 'Designation' column. This is a major performance optimization
|
||
// as it avoids loading entire 'Contact' entities into memory.
|
||
.Select(c => c.Designation)
|
||
// Let the database perform the distinct operation, which is highly efficient.
|
||
.Distinct()
|
||
// Execute the query asynchronously and materialize the results into a list.
|
||
.ToListAsync();
|
||
|
||
// --- 3. Success Response ---
|
||
// Log the successful operation with key details.
|
||
_logger.LogInfo("Successfully fetched {DesignationsCount} distinct designations for Tenant {TenantId} for employee {EmployeeId}", designationsList.Count, tenantId, employeeId);
|
||
|
||
// Return a strongly-typed success response with the data and a descriptive message.
|
||
return ApiResponse<object>.SuccessResponse(
|
||
designationsList,
|
||
$"{designationsList.Count} unique designation(s) found.",
|
||
200
|
||
);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// --- 4. Exception Handling ---
|
||
// Log the full exception details for effective debugging, including context.
|
||
_logger.LogError(ex, "An unexpected error occurred while fetching designation list for Tenant {TenantId} by Employee {EmployeeId}", tenantId, employeeId);
|
||
|
||
// Return a generic, strongly-typed error response to the client to avoid leaking implementation details.
|
||
return ApiResponse<object>.ErrorResponse("An internal server error occurred.", ExceptionMapper(ex), 500);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Fetches filter options (Buckets and Contact Categories) based on user permissions
|
||
/// for a given tenant.
|
||
/// </summary>
|
||
/// <param name="tenantId">The tenant ID.</param>
|
||
/// <param name="loggedInEmployee">The employee making the request.</param>
|
||
/// <returns>ApiResponse with Buckets and Contact Categories.</returns>
|
||
public async Task<ApiResponse<object>> 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<Guid>? 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<object>.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<object>.ErrorResponse("An error occurred while fetching filters", 500);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region =================================================================== Contact Post APIs ===================================================================
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
/// <param name="createContact">The DTO containing the details for the new contact.</param>
|
||
/// <param name="tenantId">The ID of the tenant to which the contact belongs.</param>
|
||
/// <param name="loggedInEmployee">The employee performing the action.</param>
|
||
/// <returns>An ApiResponse containing the newly created contact's view model or an error.</returns>
|
||
public async Task<ApiResponse<object>> 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<object>.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<Contact>(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<string>(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<string>(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<object>.ErrorResponse("An internal database error occurred.", ExceptionMapper(dbEx), 500);
|
||
}
|
||
|
||
// --- Construct and Return Response ---
|
||
var contactVM = _mapper.Map<ContactVM>(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<object>.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<object>.ErrorResponse("An unexpected internal error occurred.", ExceptionMapper(ex), 500);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region =================================================================== Contact Put APIs ===================================================================
|
||
|
||
public async Task<ApiResponse<object>> 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<object>.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<object>.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<object>.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<Guid> 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<object>.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<Contact>(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<UpdateLogsObject> phoneUpdateLogs = new List<UpdateLogsObject>();
|
||
List<UpdateLogsObject> emailUpdateLogs = new List<UpdateLogsObject>();
|
||
|
||
// ---------------------- 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<ContactPhone>(phoneDto);
|
||
phoneEntity.TenantId = tenantId;
|
||
phoneEntity.ContactId = contact.Id;
|
||
var existingPhone = phones.FirstOrDefault(p => p.Id == phoneEntity.Id);
|
||
if (phoneDto.Id != null && phoneDto.Id != Guid.Empty && existingPhone != null)
|
||
{
|
||
var phoneObject = _updateLogsHelper.EntityToBsonDocument(existingPhone);
|
||
phoneUpdateLogs.Add(new UpdateLogsObject
|
||
{
|
||
EntityId = existingPhone.Id.ToString(),
|
||
UpdatedById = loggedInEmployee.Id.ToString(),
|
||
OldObject = phoneObject,
|
||
UpdatedAt = DateTime.UtcNow
|
||
});
|
||
context.ContactsPhones.Update(phoneEntity);
|
||
}
|
||
else
|
||
{
|
||
context.ContactsPhones.Add(phoneEntity);
|
||
}
|
||
}
|
||
|
||
// Remove phones not updated in payload
|
||
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<ContactEmail>(emailDto);
|
||
emailEntity.TenantId = tenantId;
|
||
emailEntity.ContactId = contact.Id;
|
||
|
||
var existingEmail = emails.FirstOrDefault(e => e.Id == emailEntity.Id);
|
||
|
||
if (emailDto.Id != null && emailDto.Id != Guid.Empty && existingEmail != null)
|
||
{
|
||
var emailObject = _updateLogsHelper.EntityToBsonDocument(existingEmail);
|
||
emailUpdateLogs.Add(new UpdateLogsObject
|
||
{
|
||
EntityId = existingEmail.Id.ToString(),
|
||
UpdatedById = loggedInEmployee.Id.ToString(),
|
||
OldObject = emailObject,
|
||
UpdatedAt = DateTime.UtcNow
|
||
});
|
||
|
||
context.ContactsEmails.Update(emailEntity);
|
||
}
|
||
else
|
||
{
|
||
context.ContactsEmails.Add(emailEntity);
|
||
}
|
||
}
|
||
|
||
// Remove emails not updated in payload
|
||
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<object>.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<object>.SuccessResponse(contactVM, "Contact Updated Successfully", 200);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region =================================================================== Contact Delete APIs ===================================================================
|
||
|
||
public async Task<ApiResponse<object>> 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<object>.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<object>.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<object>.SuccessResponse(new { }, active ? "Contact is activated successfully" : "Contact is deleted successfully", 200);
|
||
}
|
||
|
||
|
||
#endregion
|
||
|
||
#endregion
|
||
|
||
#region =================================================================== Contact Notes APIs ===================================================================
|
||
|
||
public async Task<ApiResponse<object>> 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<object>.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<object>.ErrorResponse("Access Denied", "You don't have access to view notes.", 403);
|
||
}
|
||
|
||
// Build base query
|
||
IQueryable<ContactNote> 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<Guid>? 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<object>.SuccessResponse(new
|
||
{
|
||
CurrentPage = pageNumber,
|
||
TotalPages = 0,
|
||
Data = new List<ContactNoteVM>()
|
||
}, "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<object>.SuccessResponse(new
|
||
{
|
||
CurrentPage = pageNumber,
|
||
TotalPages = 0,
|
||
Data = new List<ContactNoteVM>()
|
||
}, "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<ContactNote> 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<List<ContactNoteVM>>(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<object>.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<object>.ErrorResponse("Internal Server Error", "An error occurred while fetching notes. Please try again later.", 500);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Fetches all notes associated with a given contact, subject to permission checks and contact-bucket mappings.
|
||
/// </summary>
|
||
/// <param name="id">The contact ID.</param>
|
||
/// <param name="tenantId">The tenant ID of the current user.</param>
|
||
/// <param name="active">Whether to filter for active notes only.</param>
|
||
/// <param name="loggedInEmployee">The currently logged in employee object.</param>
|
||
/// <returns>Returns a list of contact notes wrapped in ApiResponse.</returns>
|
||
public async Task<ApiResponse<object>> 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<object>.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<object>.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<object>.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<ContactNote> 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<DirectoryUpdateLog> 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<ContactNoteVM> noteVMs = _mapper.Map<List<ContactNoteVM>>(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<object>.SuccessResponse(
|
||
noteVMs,
|
||
$"{noteVMs.Count} contact-notes record(s) fetched successfully",
|
||
StatusCodes.Status200OK);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Fetches filter objects (CreatedBy employees and Organizations) for Contact Notes
|
||
/// accessible by the logged-in employee, based on permissions.
|
||
/// </summary>
|
||
/// <param name="tenantId">The tenant ID.</param>
|
||
/// <param name="loggedInEmployee">The employee requesting filters.</param>
|
||
/// <returns>ApiResponse containing CreatedBy and Organizations filter options.</returns>
|
||
public async Task<ApiResponse<object>> 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<Guid>? 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<object>.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<object>.ErrorResponse("An error occurred while fetching filters", 500);
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
/// <param name="noteDto">The DTO containing the note details.</param>
|
||
/// <param name="tenantId">The tenant identifier to which the contact belongs.</param>
|
||
/// <param name="loggedInEmployee">The logged-in employee attempting the action.</param>
|
||
/// <returns>ApiResponse containing the created note details or error information.</returns>
|
||
public async Task<ApiResponse<object>> 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<object>.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<object>.ErrorResponse("Contact not found.", "The specified contact does not exist.", 404);
|
||
}
|
||
|
||
// Map DTO -> Entity using AutoMapper
|
||
ContactNote note = _mapper.Map<ContactNote>(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<ContactNoteVM>(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<object>.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<object>.ErrorResponse("An unexpected error occurred.", ex.Message, 500);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Updates an existing contact note and logs changes
|
||
/// both in relational DB (SQL) and update logs (possibly MongoDB).
|
||
/// </summary>
|
||
/// <param name="id">The note ID that needs to be updated.</param>
|
||
/// <param name="noteDto">DTO with updated note data.</param>
|
||
/// <returns>Standardized ApiResponse with updated note or error details.</returns>
|
||
public async Task<ApiResponse<object>> 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<object>.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<object>.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<object>.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<ContactNoteVM>(contactNote);
|
||
noteVM.UpdatedAt = contactNote.UpdatedAt;
|
||
noteVM.UpdatedBy = _mapper.Map<BasicEmployeeVM>(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<object>.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<object>.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<object>.ErrorResponse("Failed to update note", "An unexpected error occurred while saving note.", 500);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Soft deletes (or restores) a contact note by updating its active status.
|
||
/// Also pushes an update log entry in SQL and Mongo (audit trail).
|
||
/// </summary>
|
||
/// <param name="id">ID of the contact note to delete/restore.</param>
|
||
/// <param name="active">Flag to set note as active or inactive.</param>
|
||
/// <param name="tenantId">Tenant identifier of the logged-in user.</param>
|
||
/// <param name="loggedInEmployee">The employee performing this action.</param>
|
||
/// <returns>ApiResponse with success or error details.</returns>
|
||
public async Task<ApiResponse<object>> 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<object>.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<object>.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<object>.ErrorResponse("Failed to delete note",
|
||
"An unexpected error occurred while deleting/restoring the note.",
|
||
500);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region =================================================================== Bucket APIs ===================================================================
|
||
public async Task<ApiResponse<object>> 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<object>.ErrorResponse("You don't have permission", "You don't have permission", 403);
|
||
}
|
||
|
||
List<Bucket> bucketList;
|
||
List<Guid> 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<object>.SuccessResponse(new List<AssignBucketVM>(), "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<AssignBucketVM>();
|
||
|
||
// 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<object>.SuccessResponse(bucketVMs, $"{bucketVMs.Count} buckets fetched successfully", 200);
|
||
}
|
||
public async Task<ApiResponse<object>> CreateBucketAsync(CreateBucketDto bucketDto, Guid tenantId, Employee loggedInEmployee)
|
||
{
|
||
if (bucketDto == null)
|
||
{
|
||
_logger.LogWarning("Employee with ID {LoggedInEmployeeId} sent empty payload", loggedInEmployee.Id);
|
||
return ApiResponse<object>.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<object>.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<object>.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<BucketVM>(createdBucket);
|
||
|
||
_logger.LogInfo("Employee {EmployeeId} successfully created bucket {BucketId}", loggedInEmployee.Id, newBucket.Id);
|
||
|
||
return ApiResponse<object>.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<object>.ErrorResponse("Internal server error", "Internal server error", 500);
|
||
}
|
||
}
|
||
public async Task<ApiResponse<object>> 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<object>.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<object>.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<object>.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<AssignBucketVM>(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<object>.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<object>.ErrorResponse("An unexpected error occurred. Please try again later.", "Internal server error", 500);
|
||
}
|
||
}
|
||
public async Task<ApiResponse<object>> AssignBucketAsync(Guid bucketId, List<AssignBucketDto> 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<object>.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<object>.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<object>.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<object>.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<AssignBucketVM>(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<object>.SuccessResponse(bucketVm, "Bucket details updated successfully", 200);
|
||
}
|
||
public async Task<ApiResponse<object>> 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<object>.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<object>.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<object>.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<object>.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<object>.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<PermissionServices>();
|
||
|
||
// 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<ContactFilterDto>(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<string>(filter, options) ?? "";
|
||
if (!string.IsNullOrWhiteSpace(unescapedJsonString))
|
||
{
|
||
expenseFilter = JsonSerializer.Deserialize<ContactFilterDto>(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<ContactNoteFilter>(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<string>(filter, options) ?? "";
|
||
if (!string.IsNullOrWhiteSpace(unescapedJsonString))
|
||
{
|
||
expenseFilter = JsonSerializer.Deserialize<ContactNoteFilter>(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<ContactPhoneVM> ProcessContactPhones(CreateContactDto dto, Contact contact, ISet<string> existingPhones)
|
||
{
|
||
if (!(dto.ContactPhones?.Any() ?? false)) return new List<ContactPhoneVM>();
|
||
|
||
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<ContactPhone>(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<ContactPhoneVM>(p)).ToList();
|
||
}
|
||
private List<ContactEmailVM> ProcessContactEmails(CreateContactDto dto, Contact contact, ISet<string> existingEmails)
|
||
{
|
||
if (!(dto.ContactEmails?.Any() ?? false)) return new List<ContactEmailVM>();
|
||
|
||
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<ContactEmail>(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<ContactEmailVM>(e)).ToList();
|
||
}
|
||
private List<ContactTagVM> ProcessTags(CreateContactDto dto, Contact contact, IDictionary<string, ContactTagMaster> tenantTags)
|
||
{
|
||
if (!(dto.Tags?.Any() ?? false)) return new List<ContactTagVM>();
|
||
|
||
var tagVMs = new List<ContactTagVM>();
|
||
var newTagMappings = new List<ContactTagMapping>();
|
||
|
||
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<ContactTagVM>(tagMaster));
|
||
}
|
||
|
||
_context.ContactTagMappings.AddRange(newTagMappings);
|
||
_logger.LogInfo("Adding {Count} tag mappings for Contact {ContactId}.", newTagMappings.Count, contact.Id);
|
||
|
||
return tagVMs;
|
||
}
|
||
private async Task<List<ContactBucketMapping>> ProcessBucketMappingsAsync(CreateContactDto dto, Contact contact, Guid tenantId)
|
||
{
|
||
if (!(dto.BucketIds?.Any() ?? false)) return new List<ContactBucketMapping>();
|
||
|
||
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<List<ContactProjectMapping>> ProcessProjectMappingsAsync(CreateContactDto dto, Contact contact, Guid tenantId)
|
||
{
|
||
if (!(dto.ProjectIds?.Any() ?? false)) return new List<ContactProjectMapping>();
|
||
|
||
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
|
||
}
|
||
}
|