Optimized the Create contact API in directory module

This commit is contained in:
ashutosh.nehete 2025-07-29 15:00:23 +05:30
parent 53a93542e9
commit 9e15cf0447
9 changed files with 425 additions and 176 deletions

View File

@ -1,6 +1,6 @@
using System.ComponentModel; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
namespace Marco.Pms.Model.Directory namespace Marco.Pms.Model.Directory
{ {

View File

@ -1,6 +1,6 @@
using System.ComponentModel; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
namespace Marco.Pms.Model.Directory namespace Marco.Pms.Model.Directory
{ {

View File

@ -4,6 +4,7 @@
{ {
public string? Label { get; set; } public string? Label { get; set; }
public string? EmailAddress { get; set; } public string? EmailAddress { get; set; }
public bool IsPrimary { get; set; } = false;
} }
} }

View File

@ -4,5 +4,6 @@
{ {
public string? Label { get; set; } public string? Label { get; set; }
public string? PhoneNumber { get; set; } public string? PhoneNumber { get; set; }
public bool IsPrimary { get; set; } = false;
} }
} }

View File

@ -21,6 +21,6 @@ namespace Marco.Pms.Model.ViewModels.Directory
public List<BasicProjectVM> Projects { get; set; } = new List<BasicProjectVM>(); public List<BasicProjectVM> Projects { get; set; } = new List<BasicProjectVM>();
public List<BucketVM> Buckets { get; set; } = new List<BucketVM>(); public List<BucketVM> Buckets { get; set; } = new List<BucketVM>();
public List<ContactTagVM> Tags { get; set; } = new List<ContactTagVM>(); public List<ContactTagVM> Tags { get; set; } = new List<ContactTagVM>();
//public List<ContactNoteVM> Notes { get; set; } = new List<ContactNoteVM>(); public List<ContactNoteVM> Notes { get; set; } = new List<ContactNoteVM>();
} }
} }

View File

@ -97,7 +97,8 @@ namespace Marco.Pms.Services.Controllers
[HttpGet("organization")] [HttpGet("organization")]
public async Task<IActionResult> GetOrganizationList() public async Task<IActionResult> GetOrganizationList()
{ {
var response = await _directoryService.GetOrganizationList(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _directoryService.GetOrganizationListAsync(tenantId, loggedInEmployee);
return Ok(response); return Ok(response);
} }
@ -106,24 +107,9 @@ namespace Marco.Pms.Services.Controllers
[HttpPost] [HttpPost]
public async Task<IActionResult> CreateContact([FromBody] CreateContactDto createContact) public async Task<IActionResult> CreateContact([FromBody] CreateContactDto createContact)
{ {
if (!ModelState.IsValid) var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
{ var response = await _directoryService.CreateContactAsync(createContact, tenantId, loggedInEmployee);
var errors = ModelState.Values return StatusCode(response.StatusCode, response);
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogWarning("User sent Invalid Date while marking attendance");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
var response = await _directoryService.CreateContact(createContact);
if (response.StatusCode == 200)
{
return Ok(response);
}
else
{
return BadRequest(response);
}
} }
[HttpPut("{id}")] [HttpPut("{id}")]

View File

@ -1,5 +1,6 @@
using AutoMapper; using AutoMapper;
using Marco.Pms.Model.Directory; using Marco.Pms.Model.Directory;
using Marco.Pms.Model.Dtos.Directory;
using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Dtos.Project;
using Marco.Pms.Model.Employees; using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Master; using Marco.Pms.Model.Master;
@ -74,15 +75,32 @@ namespace Marco.Pms.Services.MappingProfiles
#region ======================================================= Directory ======================================================= #region ======================================================= Directory =======================================================
CreateMap<Contact, ContactVM>(); CreateMap<Contact, ContactVM>();
CreateMap<CreateContactDto, Contact>();
CreateMap<Contact, ContactProfileVM>(); CreateMap<Contact, ContactProfileVM>();
CreateMap<ContactPhone, ContactPhoneVM>(); CreateMap<ContactPhone, ContactPhoneVM>();
CreateMap<CreateContactPhoneDto, ContactPhone>();
CreateMap<ContactEmail, ContactEmailVM>(); CreateMap<ContactEmail, ContactEmailVM>();
CreateMap<CreateContactEmailDto, ContactEmail>();
CreateMap<ContactCategoryMaster, ContactCategoryVM>(); CreateMap<ContactCategoryMaster, ContactCategoryVM>();
CreateMap<ContactTagMaster, ContactTagVM>(); CreateMap<ContactTagMaster, ContactTagVM>();
CreateMap<Bucket, BucketVM>(); CreateMap<Bucket, BucketVM>();
CreateMap<ContactNote, ContactNoteVM>()
.ForMember(
dest => dest.ContactName,
opt => opt.MapFrom(src => src.Contact != null ? src.Contact.Name : string.Empty)
)
.ForMember(
dest => dest.OrganizationName,
opt => opt.MapFrom(src => src.Contact != null ? src.Contact.Organization : string.Empty)
);
#endregion #endregion

View File

@ -12,6 +12,7 @@ using Marco.Pms.Model.ViewModels.Projects;
using Marco.Pms.Services.Service.ServiceInterfaces; using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service; using MarcoBMS.Services.Service;
using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Text.Json; using System.Text.Json;
@ -382,6 +383,129 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.SuccessResponse(list, System.String.Format("{0} contacts fetched successfully", list.Count), 200); 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)
{
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);
}
var (hasAdminPermission, hasManagerPermission, hasUserPermission) = await CheckPermissionsAsync(loggedInEmployee.Id);
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.", loggedInEmployee.Id, tenantId);
// Return a strongly-typed error response.
return ApiResponse<object>.ErrorResponse("Access Denied", "You do not have permission to perform this action.", 403);
}
Bucket? bucket = await _context.Buckets.FirstOrDefaultAsync(b => b.Id == bucketId && b.TenantId == tenantId);
if (bucket == null)
{
_logger.LogInfo("Employee ID {EmployeeId} attempted access to bucket ID {BucketId}, but not found in database", loggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("Bucket not found", "Bucket not found", 404);
}
List<EmployeeBucketMapping>? employeeBuckets = await _context.EmployeeBucketMappings.Where(em => em.BucketId == bucketId).ToListAsync();
EmployeeBucketMapping? employeeBucket = null;
if (hasAdminPermission)
{
employeeBucket = employeeBuckets.FirstOrDefault();
}
else if (hasManagerPermission || hasUserPermission)
{
employeeBucket = 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);
return ApiResponse<object>.ErrorResponse("You do not have access to this bucket.", "You do not have access to this bucket.", 401);
}
List<ContactBucketMapping> contactBucket = await _context.ContactBucketMappings.Where(cb => cb.BucketId == bucketId).ToListAsync() ?? new List<ContactBucketMapping>();
List<ContactVM> contactVMs = new List<ContactVM>();
if (contactBucket.Count > 0)
{
var contactIds = contactBucket.Select(cb => cb.ContactId).ToList();
List<Contact> contacts = await _context.Contacts.Include(c => c.ContactCategory).Where(c => contactIds.Contains(c.Id) && c.IsActive).ToListAsync();
List<ContactPhone> phones = await _context.ContactsPhones.Where(p => contactIds.Contains(p.ContactId)).ToListAsync();
List<ContactEmail> emails = await _context.ContactsEmails.Where(e => contactIds.Contains(e.ContactId)).ToListAsync();
List<ContactTagMapping>? tags = await _context.ContactTagMappings.Where(ct => contactIds.Contains(ct.ContactId)).ToListAsync();
List<ContactProjectMapping>? contactProjects = await _context.ContactProjectMappings.Where(cp => contactIds.Contains(cp.ContactId)).ToListAsync();
List<ContactBucketMapping>? contactBuckets = await _context.ContactBucketMappings.Where(cp => contactIds.Contains(cp.ContactId)).ToListAsync();
List<Guid> tagIds = new List<Guid>();
List<ContactTagMaster> tagMasters = new List<ContactTagMaster>();
if (tags.Count > 0)
{
tagIds = tags.Select(ct => ct.ContactTagId).ToList();
tagMasters = await _context.ContactTagMasters.Where(t => tagIds.Contains(t.Id)).ToListAsync();
}
if (contacts.Count > 0)
{
foreach (var contact in contacts)
{
List<ContactEmailVM>? emailVMs = new List<ContactEmailVM>();
List<ContactPhoneVM>? phoneVMs = new List<ContactPhoneVM>();
List<ContactTagVM>? tagVMs = new List<ContactTagVM>();
List<ContactPhone> contactPhones = phones.Where(p => p.ContactId == contact.Id).ToList();
List<ContactEmail> contactEmails = emails.Where(e => e.ContactId == contact.Id).ToList();
List<ContactTagMapping>? contactTags = tags.Where(t => t.ContactId == contact.Id).ToList();
List<ContactProjectMapping>? projectMappings = contactProjects.Where(cp => cp.ContactId == contact.Id).ToList();
List<ContactBucketMapping>? bucketMappings = contactBuckets.Where(cb => cb.ContactId == contact.Id).ToList();
if (contactPhones.Count > 0)
{
foreach (var phone in contactPhones)
{
ContactPhoneVM phoneVM = phone.ToContactPhoneVMFromContactPhone();
phoneVMs.Add(phoneVM);
}
}
if (contactEmails.Count > 0)
{
foreach (var email in contactEmails)
{
ContactEmailVM emailVM = email.ToContactEmailVMFromContactEmail();
emailVMs.Add(emailVM);
}
}
if (contactTags.Count > 0)
{
foreach (var contactTag in contactTags)
{
ContactTagMaster? tagMaster = tagMasters.Find(t => t.Id == contactTag.ContactTagId);
if (tagMaster != null)
{
ContactTagVM tagVM = tagMaster.ToContactTagVMFromContactTagMaster();
tagVMs.Add(tagVM);
}
}
}
ContactVM contactVM = contact.ToContactVMFromContact();
contactVM.ContactEmails = emailVMs;
contactVM.ContactPhones = phoneVMs;
contactVM.Tags = tagVMs;
contactVM.ProjectIds = projectMappings.Select(cp => cp.ProjectId).ToList();
contactVM.BucketIds = bucketMappings.Select(cb => cb.BucketId).ToList();
contactVMs.Add(contactVM);
}
}
}
_logger.LogInfo("{count} contact from Bucket {BucketId} fetched by Employee {EmployeeId}", contactVMs.Count, bucketId, loggedInEmployee.Id);
return ApiResponse<object>.SuccessResponse(contactVMs, $"{contactVMs.Count} contacts fetched successfully", 200);
}
public async Task<ApiResponse<object>> GetContactsListByBucketId(Guid id) public async Task<ApiResponse<object>> GetContactsListByBucketId(Guid id)
{ {
Guid tenantId = _userHelper.GetTenantId(); Guid tenantId = _userHelper.GetTenantId();
@ -611,192 +735,199 @@ namespace Marco.Pms.Services.Service
.ToListAsync(); .ToListAsync();
}); });
await Task.WhenAll(phonesTask, emailsTask, contactProjectsTask, contactBucketsTask, contactTagsTask); var contactNotesTask = Task.Run(async () =>
{
await using var taskDbContext = await _dbContextFactory.CreateDbContextAsync();
return await taskDbContext.ContactNotes
.AsNoTracking()
.Include(cn => cn.Createdby)
.Include(cn => cn.UpdatedBy)
.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.ContactPhones = phonesTask.Result;
contactVM.ContactEmails = emailsTask.Result; contactVM.ContactEmails = emailsTask.Result;
contactVM.Tags = contactTagsTask.Result; contactVM.Tags = contactTagsTask.Result;
contactVM.Buckets = contactBucketsTask.Result; contactVM.Buckets = contactBucketsTask.Result;
contactVM.Projects = contactProjectsTask.Result; contactVM.Projects = contactProjectsTask.Result;
_logger.LogInfo("Employee ID {EmployeeId} fetched profile of contact {COntactId}", loggedInEmployeeId, contact.Id); 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"); return ApiResponse<object>.SuccessResponse(contactVM, "Contact profile fetched successfully");
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "An unexpected error occurred while fetching contact list for Tenant {TenantId} by Employee {EmployeeId}", tenantId, loggedInEmployeeId); _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); return ApiResponse<object>.ErrorResponse("An internal error occurred.", ExceptionMapper(ex), 500);
} }
} }
public async Task<ApiResponse<object>> GetOrganizationList()
{
Guid tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var organizationList = await _context.Contacts.Where(c => c.TenantId == tenantId).Select(c => c.Organization).Distinct().ToListAsync(); /// <summary>
_logger.LogInfo("Employee {EmployeeId} fetched list of organizations in a tenant {TenantId}", LoggedInEmployee.Id, tenantId); /// Asynchronously retrieves a distinct list of organization names for a given tenant.
return ApiResponse<object>.SuccessResponse(organizationList, $"{organizationList.Count} records of organization names fetched from contacts", 200); /// </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);
}
} }
#endregion #endregion
#region =================================================================== Contact Post APIs =================================================================== #region =================================================================== Contact Post APIs ===================================================================
public async Task<ApiResponse<object>> CreateContact(CreateContactDto createContact)
/// <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 tenantId = _userHelper.GetTenantId(); Guid loggedInEmployeeId = loggedInEmployee.Id;
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
if (createContact != null) if (string.IsNullOrWhiteSpace(createContact.Name) ||
string.IsNullOrWhiteSpace(createContact.Organization) ||
!(createContact.BucketIds?.Any() ?? false))
{ {
List<ContactPhone> phones = new List<ContactPhone>(); _logger.LogWarning("Validation failed for CreateContactAsync. Payload missing required fields. Triggered by Employee: {LoggedInEmployeeId}", loggedInEmployeeId);
List<ContactEmail> emails = new List<ContactEmail>(); return ApiResponse<object>.ErrorResponse("Payload is missing required fields: Name, Organization, and at least one BucketId are required.", "Invalid Payload", 400);
List<ContactBucketMapping> contactBucketMappings = new List<ContactBucketMapping>(); }
List<ContactTagMapping> contactTagMappings = new List<ContactTagMapping>();
Contact? contact = createContact.ToContactFromCreateContactDto(tenantId, LoggedInEmployee.Id); 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); _context.Contacts.Add(contact);
await _context.SaveChangesAsync();
_logger.LogInfo("Contact with ID {ContactId} created by Employee with ID {LoggedInEmployeeId}", contact.Id, LoggedInEmployee.Id);
var tags = await _context.ContactTagMasters.Where(t => t.TenantId == tenantId).ToListAsync(); // --- Process Phones ---
var tagNames = tags.Select(t => t.Name.ToLower()).ToList(); var existingPhoneList = await _context.ContactsPhones
var buckets = await _context.Buckets.Where(b => b.TenantId == tenantId).Select(b => b.Id).ToListAsync(); .Where(p => p.TenantId == tenantId && p.IsPrimary)
var projects = await _context.Projects.Where(p => p.TenantId == tenantId).Select(p => p.Id).ToListAsync(); .Select(p => p.PhoneNumber)
.ToListAsync();
var existingPhones = new HashSet<string>(existingPhoneList);
var phoneVMs = ProcessContactPhones(createContact, contact, existingPhones);
if (createContact.ContactPhones != null) // --- 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();
foreach (var contactPhone in createContact.ContactPhones) _logger.LogInfo(
{ "Successfully created Contact {ContactId} with {Count} related records for Tenant {TenantId} by Employee {EmployeeId}. Transaction committed.",
ContactPhone phone = contactPhone.ToContactPhoneFromCreateContactPhoneDto(tenantId, contact.Id); contact.Id, changesCount - 1, tenantId, loggedInEmployeeId);
phones.Add(phone);
}
_context.ContactsPhones.AddRange(phones);
_logger.LogInfo("{count} phone number are saved in contact with ID {ContactId} by employee with ID {LoggedEmployeeId}", phones.Count, contact.Id, LoggedInEmployee.Id);
} }
if (createContact.ContactEmails != null) catch (DbUpdateException dbEx)
{ {
await transaction.RollbackAsync();
foreach (var contactEmail in createContact.ContactEmails) _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);
ContactEmail email = contactEmail.ToContactEmailFromCreateContactEmailDto(tenantId, contact.Id);
emails.Add(email);
}
_context.ContactsEmails.AddRange(emails);
_logger.LogInfo("{count} email addresses are saved in contact with ID {ContactId} by employee with ID {LoggedEmployeeId}", emails.Count, contact.Id, LoggedInEmployee.Id);
} }
if (createContact.BucketIds != null) // --- Construct and Return Response ---
{ var contactVM = _mapper.Map<ContactVM>(contact);
foreach (var bucket in createContact.BucketIds)
{
if (buckets.Contains(bucket))
{
ContactBucketMapping bucketMapping = new ContactBucketMapping
{
BucketId = bucket,
ContactId = contact.Id
};
contactBucketMappings.Add(bucketMapping);
}
}
_context.ContactBucketMappings.AddRange(contactBucketMappings);
_logger.LogInfo("Contact with ID {ContactId} added to {count} number of buckets by employee with ID {LoggedEmployeeId}", contact.Id, contactBucketMappings.Count, LoggedInEmployee.Id);
}
if (createContact.ProjectIds != null)
{
List<ContactProjectMapping> projectMappings = new List<ContactProjectMapping>();
foreach (var projectId in createContact.ProjectIds)
{
if (projects.Contains(projectId))
{
ContactProjectMapping projectMapping = new ContactProjectMapping
{
ProjectId = projectId,
ContactId = contact.Id,
TenantId = tenantId
};
projectMappings.Add(projectMapping);
}
}
_context.ContactProjectMappings.AddRange(projectMappings);
_logger.LogInfo("Contact with ID {ContactId} added to {count} number of project by employee with ID {LoggedEmployeeId}", contact.Id, projectMappings.Count, LoggedInEmployee.Id);
}
if (createContact.Tags != null)
{
foreach (var tag in createContact.Tags)
{
if (tagNames.Contains(tag.Name.ToLower()))
{
ContactTagMaster existingTag = tags.Find(t => t.Name == tag.Name) ?? new ContactTagMaster();
_context.ContactTagMappings.Add(new ContactTagMapping
{
ContactId = contact.Id,
ContactTagId = tag.Id ?? existingTag.Id
});
}
else if (tag.Id == null || tags.Where(t => t.Name == tag.Name) == null)
{
var newtag = new ContactTagMaster
{
Name = tag.Name,
TenantId = tenantId
};
_context.ContactTagMasters.Add(newtag);
ContactTagMapping tagMapping = new ContactTagMapping
{
ContactTagId = newtag.Id,
ContactId = contact.Id
};
contactTagMappings.Add(tagMapping);
}
}
_context.ContactTagMappings.AddRange(contactTagMappings);
_logger.LogInfo("{count} number of tags added to Contact with ID {ContactId} by employee with ID {LoggedEmployeeId}", contactTagMappings.Count, contact.Id, LoggedInEmployee.Id);
}
await _context.SaveChangesAsync();
ContactVM contactVM = new ContactVM();
List<ContactPhoneVM> phoneVMs = new List<ContactPhoneVM>();
contact = await _context.Contacts.Include(c => c.ContactCategory).FirstOrDefaultAsync(c => c.Id == contact.Id) ?? new Contact();
var tagIds = contactTagMappings.Select(t => t.ContactTagId).ToList();
tags = await _context.ContactTagMasters.Where(t => t.TenantId == tenantId && tagIds.Contains(t.Id)).ToListAsync();
List<ContactProjectMapping> contactProjects = await _context.ContactProjectMappings.Where(cp => cp.ContactId == contact.Id).ToListAsync();
List<ContactBucketMapping> bucketMappings = await _context.ContactBucketMappings.Where(cb => cb.ContactId == contact.Id).ToListAsync();
foreach (var phone in phones)
{
ContactPhoneVM phoneVM = phone.ToContactPhoneVMFromContactPhone();
phoneVMs.Add(phoneVM);
}
List<ContactEmailVM> emailVMs = new List<ContactEmailVM>();
foreach (var email in emails)
{
ContactEmailVM emailVM = email.ToContactEmailVMFromContactEmail();
emailVMs.Add(emailVM);
}
List<ContactTagVM> tagVMs = new List<ContactTagVM>();
foreach (var contactTagMapping in contactTagMappings)
{
ContactTagVM tagVM = new ContactTagVM();
var tag = tags.Find(t => t.Id == contactTagMapping.ContactTagId);
tagVM = tag != null ? tag.ToContactTagVMFromContactTagMaster() : new ContactTagVM();
tagVMs.Add(tagVM);
}
contactVM = contact.ToContactVMFromContact();
contactVM.ContactPhones = phoneVMs; contactVM.ContactPhones = phoneVMs;
contactVM.ContactEmails = emailVMs; contactVM.ContactEmails = emailVMs;
contactVM.Tags = tagVMs; contactVM.Tags = tagVMs;
contactVM.ProjectIds = contactProjects.Select(cp => cp.ProjectId).ToList(); contactVM.BucketIds = contactBucketMappings.Select(cb => cb.BucketId).ToList();
contactVM.BucketIds = bucketMappings.Select(cb => cb.BucketId).ToList(); contactVM.ProjectIds = projectMappings.Select(cp => cp.ProjectId).ToList();
return ApiResponse<object>.SuccessResponse(contactVM, "Contact Created Successfully", 200); 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);
} }
_logger.LogWarning("Employee with ID {LoggedInEmployeeId} sended empty payload", LoggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("User Send empty Payload", "User Send empty Payload", 400);
} }
#endregion #endregion
@ -1787,6 +1918,118 @@ namespace Marco.Pms.Services.Service
}; };
} }
// --- 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 #endregion
} }
} }

View File

@ -10,8 +10,8 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
Task<ApiResponse<object>> GetListOfContactsOld(string? search, bool active, ContactFilterDto? filterDto, Guid? projectId); Task<ApiResponse<object>> GetListOfContactsOld(string? search, bool active, ContactFilterDto? filterDto, Guid? projectId);
Task<ApiResponse<object>> GetContactsListByBucketId(Guid id); Task<ApiResponse<object>> GetContactsListByBucketId(Guid id);
Task<ApiResponse<object>> GetContactProfileAsync(Guid id, Guid tenantId, Employee loggedInEmployee); Task<ApiResponse<object>> GetContactProfileAsync(Guid id, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> GetOrganizationList(); Task<ApiResponse<object>> GetOrganizationListAsync(Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> CreateContact(CreateContactDto createContact); Task<ApiResponse<object>> CreateContactAsync(CreateContactDto createContact, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<object>> UpdateContact(Guid id, UpdateContactDto updateContact); Task<ApiResponse<object>> UpdateContact(Guid id, UpdateContactDto updateContact);
Task<ApiResponse<object>> DeleteContact(Guid id, bool active); Task<ApiResponse<object>> DeleteContact(Guid id, bool active);
Task<ApiResponse<object>> GetListOFAllNotes(Guid? projectId, int pageSize, int pageNumber); Task<ApiResponse<object>> GetListOFAllNotes(Guid? projectId, int pageSize, int pageNumber);