Added get tenant list API
This commit is contained in:
parent
ff0bead3a0
commit
fdac2e06e1
12
Marco.Pms.Model/Utilities/TenantFilter.cs
Normal file
12
Marco.Pms.Model/Utilities/TenantFilter.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
namespace Marco.Pms.Model.Utilities
|
||||||
|
{
|
||||||
|
public class TenantFilter
|
||||||
|
{
|
||||||
|
public List<Guid>? IndustryIds { get; set; }
|
||||||
|
public List<Guid>? CreatedByIds { get; set; }
|
||||||
|
public List<Guid>? TenantStatusIds { get; set; }
|
||||||
|
public List<string>? References { get; set; }
|
||||||
|
public DateTime? StartDate { get; set; }
|
||||||
|
public DateTime? EndDate { get; set; }
|
||||||
|
}
|
||||||
|
}
|
18
Marco.Pms.Model/ViewModels/Tenant/TenantListVM.cs
Normal file
18
Marco.Pms.Model/ViewModels/Tenant/TenantListVM.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using Marco.Pms.Model.Master;
|
||||||
|
|
||||||
|
namespace Marco.Pms.Model.ViewModels.Tenant
|
||||||
|
{
|
||||||
|
public class TenantListVM
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
public string? DomainName { get; set; }
|
||||||
|
public string ContactName { get; set; } = string.Empty;
|
||||||
|
public string ContactNumber { get; set; } = string.Empty;
|
||||||
|
public string? logoImage { get; set; } // Base64
|
||||||
|
public string? OragnizationSize { get; set; }
|
||||||
|
public Industry? Industry { get; set; }
|
||||||
|
public TenantStatus? TenantStatus { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Identity;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
|
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
|
||||||
@ -49,18 +50,154 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
_userHelper = userHelper;
|
_userHelper = userHelper;
|
||||||
}
|
}
|
||||||
// GET: api/<TenantController>
|
#region =================================================================== Tenant APIs ===================================================================
|
||||||
[HttpGet]
|
|
||||||
public IEnumerable<string> Get()
|
/// <summary>
|
||||||
|
/// Retrieves a paginated list of active tenants with optional filtering and searching.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="searchString">A string to search for across various tenant fields.</param>
|
||||||
|
/// <param name="filter">A JSON serialized string containing advanced filter criteria.</param>
|
||||||
|
/// <param name="pageSize">The number of records to return per page.</param>
|
||||||
|
/// <param name="pageNumber">The page number to retrieve.</param>
|
||||||
|
/// <returns>A paginated list of tenants matching the criteria.</returns>
|
||||||
|
[HttpGet("list")]
|
||||||
|
public async Task<IActionResult> GetTenantList([FromQuery] string? searchString, string? filter, int pageSize = 20, int pageNumber = 1)
|
||||||
{
|
{
|
||||||
return new string[] { "value1", "value2" };
|
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
|
||||||
|
var _permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
|
||||||
|
_logger.LogInfo("Attempting to fetch tenant list with pageNumber: {PageNumber} and pageSize: {PageSize}", pageNumber, pageSize);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// --- 1. PERMISSION CHECK ---
|
||||||
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
if (loggedInEmployee == null)
|
||||||
|
{
|
||||||
|
// This case should be handled by the [Authorize] attribute.
|
||||||
|
// This check is a safeguard.
|
||||||
|
_logger.LogWarning("Authentication failed: No logged-in employee found.");
|
||||||
|
return StatusCode(403, ApiResponse<object>.ErrorResponse("Authentication required", "User is not logged in.", 403));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A root user should have access regardless of the specific permission.
|
||||||
|
var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false;
|
||||||
|
var hasPermission = await _permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id);
|
||||||
|
|
||||||
|
if (!hasPermission && !isRootUser)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Permission denied: User {EmployeeId} attempted to list tenants without 'ManageTenants' permission or root access.", loggedInEmployee.Id);
|
||||||
|
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2. QUERY CONSTRUCTION ---
|
||||||
|
// Using a DbContext from the factory ensures a fresh context for this request.
|
||||||
|
await using var _context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
// Start with a base IQueryable. Filters will be appended to this.
|
||||||
|
var tenantQuery = _context.Tenants.Where(t => t.IsActive);
|
||||||
|
|
||||||
|
// Apply advanced filters from the JSON filter object.
|
||||||
|
var tenantFilter = TryDeserializeFilter(filter);
|
||||||
|
if (tenantFilter != null)
|
||||||
|
{
|
||||||
|
// Date range filtering
|
||||||
|
if (tenantFilter.StartDate.HasValue && tenantFilter.EndDate.HasValue)
|
||||||
|
{
|
||||||
|
// OPTIMIZATION: Avoid using .Date on the database column, as it can prevent index usage.
|
||||||
|
// This structure (`>= start` and `< end.AddDays(1)`) is index-friendly and correctly inclusive.
|
||||||
|
var endDateExclusive = tenantFilter.EndDate.Value.AddDays(1);
|
||||||
|
tenantQuery = tenantQuery.Where(t => t.OnBoardingDate >= tenantFilter.StartDate.Value && t.OnBoardingDate < endDateExclusive);
|
||||||
|
}
|
||||||
|
// List-based filtering
|
||||||
|
if (tenantFilter.IndustryIds?.Any() == true)
|
||||||
|
{
|
||||||
|
tenantQuery = tenantQuery.Where(t => t.IndustryId.HasValue && tenantFilter.IndustryIds.Contains(t.IndustryId.Value));
|
||||||
|
}
|
||||||
|
if (tenantFilter.References?.Any() == true)
|
||||||
|
{
|
||||||
|
tenantQuery = tenantQuery.Where(t => tenantFilter.References.Contains(t.Reference));
|
||||||
|
}
|
||||||
|
if (tenantFilter.TenantStatusIds?.Any() == true)
|
||||||
|
{
|
||||||
|
tenantQuery = tenantQuery.Where(t => tenantFilter.TenantStatusIds.Contains(t.TenantStatusId));
|
||||||
|
}
|
||||||
|
if (tenantFilter.CreatedByIds?.Any() == true)
|
||||||
|
{
|
||||||
|
tenantQuery = tenantQuery.Where(t => t.CreatedById.HasValue && tenantFilter.CreatedByIds.Contains(t.CreatedById.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply free-text search string.
|
||||||
|
if (!string.IsNullOrWhiteSpace(searchString))
|
||||||
|
{
|
||||||
|
// OPTIMIZATION: Do not use .ToLower() on the database columns (e.g., `t.Name.ToLower()`).
|
||||||
|
// This makes the query non-SARGable and kills performance by preventing index usage.
|
||||||
|
// This implementation relies on the database collation being case-insensitive (e.g., `SQL_Latin1_General_CP1_CI_AS` in SQL Server).
|
||||||
|
tenantQuery = tenantQuery.Where(t =>
|
||||||
|
t.Name.Contains(searchString) ||
|
||||||
|
t.ContactName.Contains(searchString) ||
|
||||||
|
t.Email.Contains(searchString) ||
|
||||||
|
t.ContactNumber.Contains(searchString) ||
|
||||||
|
t.BillingAddress.Contains(searchString) ||
|
||||||
|
(t.TaxId != null && t.TaxId.Contains(searchString)) ||
|
||||||
|
(t.Description != null && t.Description.Contains(searchString)) ||
|
||||||
|
(t.DomainName != null && t.DomainName.Contains(searchString))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 3. PAGINATION AND EXECUTION ---
|
||||||
|
// First, get the total count of records for pagination metadata.
|
||||||
|
// This executes a separate, efficient `COUNT(*)` query.
|
||||||
|
int totalRecords = await tenantQuery.CountAsync();
|
||||||
|
int totalPages = (int)Math.Ceiling((double)totalRecords / pageSize);
|
||||||
|
|
||||||
|
_logger.LogInfo("Found {TotalRecords} total tenants matching the query.", totalRecords);
|
||||||
|
|
||||||
|
// Now, apply ordering and pagination to fetch only the data for the current page.
|
||||||
|
// This is efficient server-side pagination.
|
||||||
|
var tenantList = await tenantQuery
|
||||||
|
.Include(t => t.Industry) // Eager load related data to prevent N+1 queries.
|
||||||
|
.Include(t => t.TenantStatus)
|
||||||
|
.OrderByDescending(t => t.OnBoardingDate)
|
||||||
|
.Skip((pageNumber - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// Map the entities to a ViewModel/DTO for the response.
|
||||||
|
var vm = _mapper.Map<List<TenantListVM>>(tenantList);
|
||||||
|
|
||||||
|
// --- 4. CONSTRUCT RESPONSE ---
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
TotalCount = totalRecords,
|
||||||
|
TotalPages = totalPages,
|
||||||
|
CurrentPage = pageNumber,
|
||||||
|
PageSize = pageSize,
|
||||||
|
Filter = tenantFilter, // Return the applied filter for context on the client-side.
|
||||||
|
Data = vm
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInfo("Successfully fetched {RecordCount} tenant records.", vm.Count);
|
||||||
|
return Ok(ApiResponse<object>.SuccessResponse(response, $"{totalRecords} records of tenants fetched successfully", 200));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// CRITICAL SECURITY FIX: Do not expose the exception details to the client.
|
||||||
|
// Log the full exception for debugging purposes.
|
||||||
|
_logger.LogError(ex, "An unhandled exception occurred while fetching the tenant list.");
|
||||||
|
|
||||||
|
// Return a generic 500 Internal Server Error response.
|
||||||
|
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.", "An unexpected error prevented the request from completing.", 500));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET api/<TenantController>/5
|
// GET api/<TenantController>/5
|
||||||
[HttpGet("{id}")]
|
[HttpGet("details/{id}")]
|
||||||
public string Get(int id)
|
public async Task<IActionResult> GetDetails(Guid id)
|
||||||
{
|
{
|
||||||
return "value";
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST api/<TenantController>
|
// POST api/<TenantController>
|
||||||
@ -124,25 +261,13 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Create the primary Tenant entity
|
// Create the primary Tenant entity
|
||||||
var tenant = new Tenant
|
|
||||||
{
|
var tenant = _mapper.Map<Tenant>(model);
|
||||||
Name = model.OragnizationName,
|
|
||||||
ContactName = $"{model.FirstName} {model.LastName}",
|
tenant.TenantStatusId = activeStatus;
|
||||||
ContactNumber = model.ContactNumber,
|
tenant.CreatedById = loggedInEmployee.Id;
|
||||||
Email = model.Email,
|
tenant.IsSuperTenant = false;
|
||||||
IndustryId = model.IndustryId,
|
|
||||||
TenantStatusId = activeStatus, // Assuming 'activeStatus' is a defined variable or constant
|
|
||||||
Description = model.Description,
|
|
||||||
OnBoardingDate = model.OnBoardingDate,
|
|
||||||
OragnizationSize = model.OragnizationSize,
|
|
||||||
Reference = model.Reference,
|
|
||||||
CreatedById = loggedInEmployee.Id,
|
|
||||||
BillingAddress = model.BillingAddress,
|
|
||||||
TaxId = model.TaxId,
|
|
||||||
logoImage = model.logoImage,
|
|
||||||
DomainName = model.DomainName,
|
|
||||||
IsSuperTenant = false
|
|
||||||
};
|
|
||||||
_context.Tenants.Add(tenant);
|
_context.Tenants.Add(tenant);
|
||||||
|
|
||||||
// Create the root ApplicationUser for the new tenant
|
// Create the root ApplicationUser for the new tenant
|
||||||
@ -253,6 +378,17 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region =================================================================== Subscription APIs ===================================================================
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region =================================================================== Subscription Plan APIs ===================================================================
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region =================================================================== Helper Functions ===================================================================
|
||||||
|
|
||||||
private static object ExceptionMapper(Exception ex)
|
private static object ExceptionMapper(Exception ex)
|
||||||
{
|
{
|
||||||
@ -298,5 +434,46 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TenantFilter? TryDeserializeFilter(string? filter)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filter))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||||
|
TenantFilter? expenseFilter = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// First, try to deserialize directly. This is the expected case (e.g., from a web client).
|
||||||
|
expenseFilter = JsonSerializer.Deserialize<TenantFilter>(filter, options);
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "[{MethodName}] Failed to directly deserialize filter. Attempting to unescape and re-parse. Filter: {Filter}", nameof(TryDeserializeFilter), filter);
|
||||||
|
|
||||||
|
// If direct deserialization fails, it might be an escaped string (common with tools like Postman or some mobile clients).
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Unescape the string first, then deserialize the result.
|
||||||
|
string unescapedJsonString = JsonSerializer.Deserialize<string>(filter, options) ?? "";
|
||||||
|
if (!string.IsNullOrWhiteSpace(unescapedJsonString))
|
||||||
|
{
|
||||||
|
expenseFilter = JsonSerializer.Deserialize<TenantFilter>(unescapedJsonString, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (JsonException ex1)
|
||||||
|
{
|
||||||
|
// If both attempts fail, log the final error and return null.
|
||||||
|
_logger.LogError(ex1, "[{MethodName}] All attempts to deserialize the filter failed. Filter will be ignored. Filter: {Filter}", nameof(TryDeserializeFilter), filter);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return expenseFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Marco.Pms.Model.Dtos.Project;
|
using Marco.Pms.Model.Dtos.Project;
|
||||||
|
using Marco.Pms.Model.Dtos.Tenant;
|
||||||
using Marco.Pms.Model.Employees;
|
using Marco.Pms.Model.Employees;
|
||||||
using Marco.Pms.Model.Entitlements;
|
using Marco.Pms.Model.Entitlements;
|
||||||
using Marco.Pms.Model.Master;
|
using Marco.Pms.Model.Master;
|
||||||
@ -18,6 +19,16 @@ namespace Marco.Pms.Services.MappingProfiles
|
|||||||
{
|
{
|
||||||
#region ======================================================= Employees =======================================================
|
#region ======================================================= Employees =======================================================
|
||||||
CreateMap<Tenant, TenantVM>();
|
CreateMap<Tenant, TenantVM>();
|
||||||
|
CreateMap<Tenant, TenantListVM>();
|
||||||
|
CreateMap<CreateTenantDto, Tenant>()
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.ContactName,
|
||||||
|
opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}")
|
||||||
|
)
|
||||||
|
.ForMember(
|
||||||
|
dest => dest.Name,
|
||||||
|
opt => opt.MapFrom(src => src.OragnizationName)
|
||||||
|
);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region ======================================================= Projects =======================================================
|
#region ======================================================= Projects =======================================================
|
||||||
|
Loading…
x
Reference in New Issue
Block a user