diff --git a/Marco.Pms.Model/Filters/TaskFilter.cs b/Marco.Pms.Model/Filters/TaskFilter.cs new file mode 100644 index 0000000..2f736bc --- /dev/null +++ b/Marco.Pms.Model/Filters/TaskFilter.cs @@ -0,0 +1,10 @@ +namespace Marco.Pms.Model.Filters +{ + public class TaskFilter + { + public List? BuildingIds { get; set; } + public List? FloorIds { get; set; } + public List? ActivityIds { get; set; } + public List? ServiceIds { get; set; } + } +} diff --git a/Marco.Pms.Services/Controllers/TaskController.cs b/Marco.Pms.Services/Controllers/TaskController.cs index 51660bb..94347e1 100644 --- a/Marco.Pms.Services/Controllers/TaskController.cs +++ b/Marco.Pms.Services/Controllers/TaskController.cs @@ -2,6 +2,7 @@ using Marco.Pms.Model.Activities; using Marco.Pms.Model.Dtos.Activities; using Marco.Pms.Model.Entitlements; +using Marco.Pms.Model.Filters; using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; @@ -17,6 +18,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using Microsoft.CodeAnalysis; using Microsoft.EntityFrameworkCore; +using System.Text.Json; using Document = Marco.Pms.Model.DocumentManager.Document; namespace MarcoBMS.Services.Controllers @@ -428,7 +430,8 @@ namespace MarcoBMS.Services.Controllers } [HttpGet("list")] - public async Task GetTasksList([FromQuery] Guid projectId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null) + public async Task GetTasksList([FromQuery] Guid projectId, [FromQuery] string? filter, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20, + [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null) { _logger.LogInfo("GetTasksList called for projectId: {ProjectId}, dateFrom: {DateFrom}, dateTo: {DateTo}", projectId, dateFrom ?? "", dateTo ?? ""); @@ -436,76 +439,103 @@ namespace MarcoBMS.Services.Controllers DateTime fromDate = new DateTime(); DateTime toDate = new DateTime(); - // Parse and validate dateFrom + // 1. Parse and validate dateFrom if (dateFrom != null && !DateTime.TryParse(dateFrom, out fromDate)) { _logger.LogWarning("Invalid starting date provided: {DateFrom}", dateFrom); return BadRequest(ApiResponse.ErrorResponse("Invalid starting date.", "Invalid starting date.", 400)); } - // Parse and validate dateTo + // 2. Parse and validate dateTo if (dateTo != null && !DateTime.TryParse(dateTo, out toDate)) { _logger.LogWarning("Invalid ending date provided: {DateTo}", dateTo); return BadRequest(ApiResponse.ErrorResponse("Invalid ending date.", "Invalid ending date.", 400)); } - // Set default date range if not provided + // 3. Set default date range if not provided fromDate = dateFrom == null ? DateTime.UtcNow.Date : fromDate; toDate = dateTo == null ? fromDate.AddDays(1) : toDate; - // 1. Get all buildings under this project - _logger.LogInfo("Fetching buildings for projectId: {ProjectId}", projectId); - var buildings = await _context.Buildings - .Where(b => b.ProjectId == projectId && b.TenantId == tenantId) - .ToListAsync(); - - var buildingIds = buildings.Select(b => b.Id).ToList(); - - // 2. Get floors under the buildings - var floors = await _context.Floor - .Where(f => buildingIds.Contains(f.BuildingId) && f.TenantId == tenantId) - .ToListAsync(); - var floorIds = floors.Select(f => f.Id).ToList(); - - // 3. Get work areas under the floors - var workAreas = await _context.WorkAreas - .Where(a => floorIds.Contains(a.FloorId) && a.TenantId == tenantId) - .ToListAsync(); - var workAreaIds = workAreas.Select(a => a.Id).ToList(); - - // 4. Get work items under the work areas - var workItems = await _context.WorkItems - .Where(i => workAreaIds.Contains(i.WorkAreaId) && i.TenantId == tenantId) - .Include(i => i.ActivityMaster) - .ToListAsync(); - var workItemIds = workItems.Select(i => i.Id).ToList(); _logger.LogInfo("Fetching task allocations between {FromDate} and {ToDate}", fromDate, toDate); - // 5. Get task allocations in the specified date range - var taskAllocations = await _context.TaskAllocations + // 4. Get task allocations in the specified date range + var taskAllocationQuery = _context.TaskAllocations .Include(t => t.Employee) .Include(t => t.ReportedBy) .Include(t => t.ApprovedBy) .Include(t => t.WorkStatus) .Include(t => t.WorkItem) - .Where(t => workItemIds.Contains(t.WorkItemId) && + .ThenInclude(wi => wi!.ActivityMaster) + .ThenInclude(a => a!.ActivityGroup) + .ThenInclude(ag => ag!.Service) + .Include(t => t.WorkItem) + .ThenInclude(wi => wi!.WorkCategoryMaster) + .Include(t => t.WorkItem) + .ThenInclude(wi => wi!.WorkArea) + .ThenInclude(wa => wa!.Floor) + .ThenInclude(f => f!.Building) + .Where(t => t.WorkItem != null && + t.WorkItem.WorkArea != null && + t.WorkItem.WorkArea.Floor != null && + t.WorkItem.WorkArea.Floor.Building != null && + t.WorkItem.WorkArea.Floor.Building.ProjectId != projectId && t.AssignmentDate.Date >= fromDate.Date && t.AssignmentDate.Date <= toDate.Date && - t.TenantId == tenantId) + t.TenantId == tenantId); + + var taskFilter = TryDeserializeFilter(filter); + + if (taskFilter != null) + { + if (taskFilter.BuildingIds?.Any() ?? false) + { + taskAllocationQuery = taskAllocationQuery.Where(t => t.WorkItem != null && + t.WorkItem.WorkArea != null && + t.WorkItem.WorkArea.Floor != null && + taskFilter.BuildingIds.Contains(t.WorkItem.WorkArea.Floor.BuildingId)); + } + if (taskFilter.FloorIds?.Any() ?? false) + { + taskAllocationQuery = taskAllocationQuery.Where(t => t.WorkItem != null && + t.WorkItem.WorkArea != null && + taskFilter.FloorIds.Contains(t.WorkItem.WorkArea.FloorId)); + } + if (taskFilter.ActivityIds?.Any() ?? false) + { + taskAllocationQuery = taskAllocationQuery.Where(t => t.WorkItem != null && + taskFilter.ActivityIds.Contains(t.WorkItem.ActivityId)); + } + if (taskFilter.ServiceIds?.Any() ?? false) + { + taskAllocationQuery = taskAllocationQuery.Where(t => t.WorkItem != null && + t.WorkItem.ActivityMaster != null && + t.WorkItem.ActivityMaster.ActivityGroup != null && + taskFilter.ServiceIds.Contains(t.WorkItem.ActivityMaster.ActivityGroup.ServiceId)); + } + } + + + int totalRecords = await taskAllocationQuery.CountAsync(); + int totalPages = (int)Math.Ceiling((double)totalRecords / pageSize); + + var taskAllocations = await taskAllocationQuery + .OrderByDescending(t => t.AssignmentDate) + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) .ToListAsync(); var taskIds = taskAllocations.Select(t => t.Id).ToList(); - // 6. Load team members + // 5. Load team members _logger.LogInfo("Loading task members and related employee data."); var teamMembers = await _context.TaskMembers .Include(t => t.Employee) .Where(t => taskIds.Contains(t.TaskAllocationId)) .ToListAsync(); - // 7. Load task comments + // 6. Load task comments _logger.LogInfo("Fetching comments and attachments."); var allComments = await _context.TaskComments .Include(c => c.Employee) @@ -513,14 +543,14 @@ namespace MarcoBMS.Services.Controllers .ToListAsync(); var commentIds = allComments.Select(c => c.Id).ToList(); - // 8. Load all attachments (task and comment) + // 7. Load all attachments (task and comment) var attachments = await _context.TaskAttachments .Where(t => taskIds.Contains(t.ReferenceId) || commentIds.Contains(t.ReferenceId)) .ToListAsync(); var documentIds = attachments.Select(t => t.DocumentId).ToList(); - // 9. Load actual documents from attachment references + // 8. Load actual documents from attachment references var documents = await _context.Documents .Where(d => documentIds.Contains(d.Id)) .ToListAsync(); @@ -606,9 +636,18 @@ namespace MarcoBMS.Services.Controllers tasks.Add(response); } + var VM = new + { + TotalCount = totalRecords, + TotalPages = totalPages, + CurrentPage = pageNumber, + PageSize = pageSize, + Data = tasks + }; + _logger.LogInfo("Task list constructed successfully. Returning {Count} tasks.", tasks.Count); - return Ok(ApiResponse.SuccessResponse(tasks, "Success", 200)); + return Ok(ApiResponse.SuccessResponse(VM, "Success", 200)); } [HttpGet("get/{taskId}")] @@ -869,5 +908,44 @@ namespace MarcoBMS.Services.Controllers return Ok(ApiResponse.SuccessResponse("Task has been approved", "Task has been approved", 200)); } + + private TaskFilter? TryDeserializeFilter(string? filter) + { + if (string.IsNullOrWhiteSpace(filter)) + { + return null; + } + + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + TaskFilter? expenseFilter = null; + + try + { + // First, try to deserialize directly. This is the expected case (e.g., from a web client). + expenseFilter = JsonSerializer.Deserialize(filter, options); + } + catch (JsonException ex) + { + _logger.LogError(ex, "[{MethodName}] Failed to directly deserialize filter. Attempting to unescape and re-parse. Filter: {Filter}", nameof(TryDeserializeFilter), filter); + + // If direct deserialization fails, it might be an escaped string (common with tools like Postman or some mobile clients). + try + { + // Unescape the string first, then deserialize the result. + string unescapedJsonString = JsonSerializer.Deserialize(filter, options) ?? ""; + if (!string.IsNullOrWhiteSpace(unescapedJsonString)) + { + expenseFilter = JsonSerializer.Deserialize(unescapedJsonString, options); + } + } + catch (JsonException ex1) + { + // If both attempts fail, log the final error and return null. + _logger.LogError(ex1, "[{MethodName}] All attempts to deserialize the filter failed. Filter will be ignored. Filter: {Filter}", nameof(TryDeserializeFilter), filter); + return null; + } + } + return expenseFilter; + } } }