Added an API to get list of delivery challan by purchase invoice ID
This commit is contained in:
parent
41feb58d45
commit
0fe59223e2
@ -40,7 +40,7 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
/// <param name="cancellationToken">Token to propagate notification that operations should be canceled.</param>
|
/// <param name="cancellationToken">Token to propagate notification that operations should be canceled.</param>
|
||||||
/// <returns>A HTTP 200 OK response with a list of purchase invoices or an appropriate HTTP error code.</returns>
|
/// <returns>A HTTP 200 OK response with a list of purchase invoices or an appropriate HTTP error code.</returns>
|
||||||
[HttpGet("list")]
|
[HttpGet("list")]
|
||||||
public async Task<IActionResult> GetPurchaseInvoiceListAsync([FromQuery] string? searchString, [FromQuery] string? filter, CancellationToken cancellationToken, [FromQuery] bool isActive = true,
|
public async Task<IActionResult> GetPurchaseInvoiceList([FromQuery] string? searchString, [FromQuery] string? filter, CancellationToken cancellationToken, [FromQuery] bool isActive = true,
|
||||||
[FromQuery] int pageSize = 20, [FromQuery] int pageNumber = 1)
|
[FromQuery] int pageSize = 20, [FromQuery] int pageNumber = 1)
|
||||||
{
|
{
|
||||||
// Get the currently logged-in employee
|
// Get the currently logged-in employee
|
||||||
@ -54,7 +54,7 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("details/{id}")]
|
[HttpGet("details/{id}")]
|
||||||
public async Task<IActionResult> GetPurchaseInvoiceDetailsAsync(Guid id, CancellationToken cancellationToken)
|
public async Task<IActionResult> GetPurchaseInvoiceDetails(Guid id, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Get the currently logged-in employee
|
// Get the currently logged-in employee
|
||||||
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
@ -125,6 +125,14 @@ namespace Marco.Pms.Services.Controllers
|
|||||||
|
|
||||||
#region =================================================================== Delivery Challan Functions ===================================================================
|
#region =================================================================== Delivery Challan Functions ===================================================================
|
||||||
|
|
||||||
|
[HttpGet("delivery-challan/list/{purchaseInvoiceId}")]
|
||||||
|
public async Task<IActionResult> GetDeliveryChallans(Guid purchaseInvoiceId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
|
||||||
|
var response = await _purchaseInvoiceService.GetDeliveryChallansAsync(purchaseInvoiceId, loggedInEmployee, tenantId, cancellationToken);
|
||||||
|
return StatusCode(response.StatusCode, response);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a delivery challan.
|
/// Adds a delivery challan.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -116,7 +116,7 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
using var context = _dbContextFactory.CreateDbContext();
|
using var context = _dbContextFactory.CreateDbContext();
|
||||||
return await context.ProjectAllocations
|
return await context.ProjectAllocations
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.CountAsync(pa => pa.ProjectId == project.Id && pa.IsActive); // Server-side count is efficient
|
.CountAsync(pa => pa.ProjectId == project.Id && pa.TenantId == project.TenantId && pa.IsActive); // Server-side count is efficient
|
||||||
});
|
});
|
||||||
|
|
||||||
// This task fetches the entire infrastructure hierarchy and performs aggregations in the database.
|
// This task fetches the entire infrastructure hierarchy and performs aggregations in the database.
|
||||||
@ -127,26 +127,26 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
// 1. Fetch all hierarchical data using projections.
|
// 1. Fetch all hierarchical data using projections.
|
||||||
// This is still a chain, but it's inside one task and much faster due to projections.
|
// This is still a chain, but it's inside one task and much faster due to projections.
|
||||||
var buildings = await context.Buildings.AsNoTracking()
|
var buildings = await context.Buildings.AsNoTracking()
|
||||||
.Where(b => b.ProjectId == project.Id)
|
.Where(b => b.ProjectId == project.Id && b.TenantId == project.TenantId)
|
||||||
.Select(b => new { b.Id, b.ProjectId, b.Name, b.Description })
|
.Select(b => new { b.Id, b.ProjectId, b.Name, b.Description })
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
var buildingIds = buildings.Select(b => b.Id).ToList();
|
var buildingIds = buildings.Select(b => b.Id).ToList();
|
||||||
|
|
||||||
var floors = await context.Floor.AsNoTracking()
|
var floors = await context.Floor.AsNoTracking()
|
||||||
.Where(f => buildingIds.Contains(f.BuildingId))
|
.Where(f => buildingIds.Contains(f.BuildingId) && f.TenantId == project.TenantId)
|
||||||
.Select(f => new { f.Id, f.BuildingId, f.FloorName })
|
.Select(f => new { f.Id, f.BuildingId, f.FloorName })
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
var floorIds = floors.Select(f => f.Id).ToList();
|
var floorIds = floors.Select(f => f.Id).ToList();
|
||||||
|
|
||||||
var workAreas = await context.WorkAreas.AsNoTracking()
|
var workAreas = await context.WorkAreas.AsNoTracking()
|
||||||
.Where(wa => floorIds.Contains(wa.FloorId))
|
.Where(wa => floorIds.Contains(wa.FloorId) && wa.TenantId == project.TenantId)
|
||||||
.Select(wa => new { wa.Id, wa.FloorId, wa.AreaName })
|
.Select(wa => new { wa.Id, wa.FloorId, wa.AreaName })
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
var workAreaIds = workAreas.Select(wa => wa.Id).ToList();
|
var workAreaIds = workAreas.Select(wa => wa.Id).ToList();
|
||||||
|
|
||||||
// 2. THE KEY OPTIMIZATION: Aggregate work items in the database.
|
// 2. THE KEY OPTIMIZATION: Aggregate work items in the database.
|
||||||
var workSummaries = await context.WorkItems.AsNoTracking()
|
var workSummaries = await context.WorkItems.AsNoTracking()
|
||||||
.Where(wi => workAreaIds.Contains(wi.WorkAreaId))
|
.Where(wi => workAreaIds.Contains(wi.WorkAreaId) && wi.TenantId == project.TenantId)
|
||||||
.GroupBy(wi => wi.WorkAreaId) // Group by parent on the DB server
|
.GroupBy(wi => wi.WorkAreaId) // Group by parent on the DB server
|
||||||
.Select(g => new // Let the DB do the SUM
|
.Select(g => new // Let the DB do the SUM
|
||||||
{
|
{
|
||||||
@ -281,6 +281,7 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
var projectStatusIds = projects.Select(p => p.ProjectStatusId).Distinct().ToList();
|
var projectStatusIds = projects.Select(p => p.ProjectStatusId).Distinct().ToList();
|
||||||
var promotorIds = projects.Select(p => p.PromoterId).Distinct().ToList();
|
var promotorIds = projects.Select(p => p.PromoterId).Distinct().ToList();
|
||||||
var pmcsIds = projects.Select(p => p.PMCId).Distinct().ToList();
|
var pmcsIds = projects.Select(p => p.PMCId).Distinct().ToList();
|
||||||
|
var tenantIds = projects.Select(p => p.TenantId).Distinct().ToList();
|
||||||
|
|
||||||
// --- Step 1: Fetch all required data in maximum parallel ---
|
// --- Step 1: Fetch all required data in maximum parallel ---
|
||||||
// Each task uses its own DbContext and selects only the required columns (projection).
|
// Each task uses its own DbContext and selects only the required columns (projection).
|
||||||
@ -320,7 +321,7 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
// Server-side aggregation and projection into a dictionary
|
// Server-side aggregation and projection into a dictionary
|
||||||
return await context.ProjectAllocations
|
return await context.ProjectAllocations
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(pa => projectIds.Contains(pa.ProjectId) && pa.IsActive)
|
.Where(pa => projectIds.Contains(pa.ProjectId) && tenantIds.Contains(pa.TenantId) && pa.IsActive)
|
||||||
.GroupBy(pa => pa.ProjectId)
|
.GroupBy(pa => pa.ProjectId)
|
||||||
.Select(g => new { ProjectId = g.Key, Count = g.Count() })
|
.Select(g => new { ProjectId = g.Key, Count = g.Count() })
|
||||||
.ToDictionaryAsync(x => x.ProjectId, x => x.Count);
|
.ToDictionaryAsync(x => x.ProjectId, x => x.Count);
|
||||||
@ -331,7 +332,7 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
using var context = _dbContextFactory.CreateDbContext();
|
using var context = _dbContextFactory.CreateDbContext();
|
||||||
return await context.Buildings
|
return await context.Buildings
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(b => projectIds.Contains(b.ProjectId))
|
.Where(b => projectIds.Contains(b.ProjectId) && tenantIds.Contains(b.TenantId))
|
||||||
.Select(b => new { b.Id, b.ProjectId, b.Name, b.Description }) // Projection
|
.Select(b => new { b.Id, b.ProjectId, b.Name, b.Description }) // Projection
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
});
|
});
|
||||||
@ -345,7 +346,7 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
using var context = _dbContextFactory.CreateDbContext();
|
using var context = _dbContextFactory.CreateDbContext();
|
||||||
return await context.Floor
|
return await context.Floor
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(f => buildingIds.Contains(f.BuildingId))
|
.Where(f => buildingIds.Contains(f.BuildingId) && tenantIds.Contains(f.TenantId))
|
||||||
.Select(f => new { f.Id, f.BuildingId, f.FloorName }) // Projection
|
.Select(f => new { f.Id, f.BuildingId, f.FloorName }) // Projection
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
});
|
});
|
||||||
@ -359,7 +360,7 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
using var context = _dbContextFactory.CreateDbContext();
|
using var context = _dbContextFactory.CreateDbContext();
|
||||||
return await context.WorkAreas
|
return await context.WorkAreas
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(wa => floorIds.Contains(wa.FloorId))
|
.Where(wa => floorIds.Contains(wa.FloorId) && tenantIds.Contains(wa.TenantId))
|
||||||
.Select(wa => new { wa.Id, wa.FloorId, wa.AreaName }) // Projection
|
.Select(wa => new { wa.Id, wa.FloorId, wa.AreaName }) // Projection
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
});
|
});
|
||||||
@ -376,7 +377,7 @@ namespace Marco.Pms.Services.Helpers
|
|||||||
// Let the DB do the SUM. This is much faster and transfers less data.
|
// Let the DB do the SUM. This is much faster and transfers less data.
|
||||||
return await context.WorkItems
|
return await context.WorkItems
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(wi => workAreaIds.Contains(wi.WorkAreaId))
|
.Where(wi => workAreaIds.Contains(wi.WorkAreaId) && tenantIds.Contains(wi.TenantId))
|
||||||
.GroupBy(wi => wi.WorkAreaId)
|
.GroupBy(wi => wi.WorkAreaId)
|
||||||
.Select(g => new
|
.Select(g => new
|
||||||
{
|
{
|
||||||
|
|||||||
@ -886,6 +886,86 @@ namespace Marco.Pms.Services.Service
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region =================================================================== Delivery Challan Functions ===================================================================
|
#region =================================================================== Delivery Challan Functions ===================================================================
|
||||||
|
|
||||||
|
public async Task<ApiResponse<List<DeliveryChallanVM>>> GetDeliveryChallansAsync(Guid purchaseInvoiceId, Employee loggedInEmployee, Guid tenantId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
// 1. Setup Context
|
||||||
|
// Using a factory ensures a clean context for this specific unit of work.
|
||||||
|
await using var context = await _dbContextFactory.CreateDbContextAsync(ct);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInfo("GetDeliveryChallans: Fetching challans. InvoiceId: {InvoiceId}, Tenant: {TenantId}", purchaseInvoiceId, tenantId);
|
||||||
|
|
||||||
|
// 2. Optimized Validation
|
||||||
|
// Use AnyAsync() instead of FirstOrDefaultAsync().
|
||||||
|
// We only need to know if it *exists*, we don't need to load the data into memory.
|
||||||
|
var isInvoiceValid = await context.PurchaseInvoiceDetails
|
||||||
|
.AsNoTracking()
|
||||||
|
.AnyAsync(pid => pid.Id == purchaseInvoiceId && pid.TenantId == tenantId, ct);
|
||||||
|
|
||||||
|
if (!isInvoiceValid)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("GetDeliveryChallans: Invoice not found. InvoiceId: {InvoiceId}", purchaseInvoiceId);
|
||||||
|
return ApiResponse<List<DeliveryChallanVM>>.ErrorResponse("Invalid Purchase Invoice", "The specified purchase invoice does not exist.", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Data Retrieval
|
||||||
|
// Fetch only valid records with necessary related data.
|
||||||
|
var deliveryChallanEntities = await context.DeliveryChallanDetails
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(dc => dc.PurchaseInvoice)
|
||||||
|
.Include(dc => dc.Attachment).ThenInclude(pia => pia!.Document)
|
||||||
|
.Where(dc => dc.PurchaseInvoiceId == purchaseInvoiceId
|
||||||
|
&& dc.TenantId == tenantId
|
||||||
|
&& dc.Attachment != null
|
||||||
|
&& dc.Attachment.Document != null) // Ensure strict data integrity
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
// 4. Early Exit for Empty Lists
|
||||||
|
// Returns an empty list with 200 OK immediately, avoiding unnecessary mapping/looping.
|
||||||
|
if (!deliveryChallanEntities.Any())
|
||||||
|
{
|
||||||
|
_logger.LogInfo("GetDeliveryChallans: No challans found for InvoiceId: {InvoiceId}", purchaseInvoiceId);
|
||||||
|
return ApiResponse<List<DeliveryChallanVM>>.SuccessResponse(new List<DeliveryChallanVM>(), "No delivery challans found.", 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Mapping and Transformation
|
||||||
|
// We map the entities to View Models first, then apply business logic (S3 URLs).
|
||||||
|
// Using Map<List<T>> is generally more efficient than mapping inside a Select loop for complex objects.
|
||||||
|
|
||||||
|
// Enhance VMs with Signed URLs
|
||||||
|
// We iterate through the already-mapped list to populate non-database fields.
|
||||||
|
// Zip or standard for-loop could be used, but since we mapped a list, we need to match them up.
|
||||||
|
// Note: Automapper preserves order, so index matching works, but iterating the Source Entity to populate the Dest VM is safer.
|
||||||
|
|
||||||
|
var responseList = deliveryChallanEntities.Select(dc =>
|
||||||
|
{
|
||||||
|
var result = _mapper.Map<DeliveryChallanVM>(dc);
|
||||||
|
if (dc.Attachment?.Document != null)
|
||||||
|
{
|
||||||
|
result.Attachment!.PreSignedUrl = _s3Service.GeneratePreSignedUrl(dc.Attachment.Document.S3Key);
|
||||||
|
|
||||||
|
// Fallback logic for thumbnail
|
||||||
|
var thumbKey = !string.IsNullOrEmpty(dc.Attachment.Document.ThumbS3Key)
|
||||||
|
? dc.Attachment.Document.ThumbS3Key
|
||||||
|
: dc.Attachment.Document.S3Key;
|
||||||
|
|
||||||
|
result.Attachment.ThumbPreSignedUrl = _s3Service.GeneratePreSignedUrl(thumbKey);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
_logger.LogInfo("GetDeliveryChallans: Successfully returned {Count} items.", responseList.Count);
|
||||||
|
|
||||||
|
return ApiResponse<List<DeliveryChallanVM>>.SuccessResponse(responseList, "List of delivery challans fetched successfully.", 200);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "GetDeliveryChallans: An error occurred. InvoiceId: {InvoiceId}", purchaseInvoiceId);
|
||||||
|
return ApiResponse<List<DeliveryChallanVM>>.ErrorResponse("Internal Server Error", "An unexpected error occurred while fetching delivery challans.", 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
public async Task<ApiResponse<DeliveryChallanVM>> AddDeliveryChallanAsync(DeliveryChallanDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct)
|
public async Task<ApiResponse<DeliveryChallanVM>> AddDeliveryChallanAsync(DeliveryChallanDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
// 1. Input Validation - Fail Fast
|
// 1. Input Validation - Fail Fast
|
||||||
@ -923,7 +1003,7 @@ namespace Marco.Pms.Services.Service
|
|||||||
// Note: We project only what we need or map later to avoid EF translation issues with complex Mappers.
|
// Note: We project only what we need or map later to avoid EF translation issues with complex Mappers.
|
||||||
var purchaseInvoiceEntity = await context.PurchaseInvoiceDetails
|
var purchaseInvoiceEntity = await context.PurchaseInvoiceDetails
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.FirstOrDefaultAsync(pid => pid.Id == model.PurchaseInvoiceId && pid.TenantId == tenantId, ct);
|
.FirstOrDefaultAsync(pid => pid.Id == model.PurchaseInvoiceId && pid.IsActive && pid.TenantId == tenantId, ct);
|
||||||
|
|
||||||
if (purchaseInvoiceEntity == null)
|
if (purchaseInvoiceEntity == null)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -18,6 +18,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region =================================================================== Delivery Challan Functions ===================================================================
|
#region =================================================================== Delivery Challan Functions ===================================================================
|
||||||
|
Task<ApiResponse<List<DeliveryChallanVM>>> GetDeliveryChallansAsync(Guid purchaseInvoiceId, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
|
||||||
Task<ApiResponse<DeliveryChallanVM>> AddDeliveryChallanAsync(DeliveryChallanDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
|
Task<ApiResponse<DeliveryChallanVM>> AddDeliveryChallanAsync(DeliveryChallanDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user